ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序实验室仪器预约平台设计与实现

微信小程序实验室仪器预约平台设计与实现 在高校和科研单位的实验室管理中仪器预约并不是简单的借还登记。一台高速离心机、一台流式细胞仪往往被多个课题组共用管理员需要掌握谁在用、什么时间段用、用来完成什么实验还要防止不同用户在同一时间段抢占同一台设备。基于微信小程序的实验室仪器预约平台正是为了解决这类问题而出现的典型项目形态。它同时涉及微信小程序前端、后端接口、数据库预约规则和权限审核非常适合作为课程设计、毕业设计或团队内部工具的开源项目方向。这篇文章以“设计 实现 排错”为主线带读者从需求拆解出发完成数据库表设计、后端预约接口、小程序端的列表与预约提交并给出管理员审核、冲突检测和常见问题排查链路。文中代码和配置用于说明实现思路落地到实际项目时需要结合自己的包名、路径、依赖版本和部署环境调整。1. 先拆解预约场景里的角色、痛点和状态1.1 手工登记为什么不能满足实验室需要传统的手工登记方式看起来简单实际上有四个明显问题。第一信息不透明。用户不知道某台仪器在某个时间段是否已经被预约必须到实验室现场或询问管理员才能确认沟通成本高。第二时间冲突难以避免。手工登记表的覆盖范围有限多人同时登记同一时段时冲突只能靠管理员事后发现。第三审批状态和实验用途没有沉淀。如果仪器需要管理员审批申请人通过微信发消息申请审批结果散落在聊天记录里后续无法追溯。第四统计困难。学期末统计每台仪器的使用率、每个用户的使用时长时手工表几乎无法快速完成。这些问题说明仪器预约的核心不是把登记表搬到线上而是把“时间”和“状态”管起来。时间代表设备被谁占用状态代表一次预约走到哪一步。1.2 三类角色与核心功能边界在一个相对完整的实验室预约平台里角色通常分成三类角色主要动作对应页面能力普通用户学生/教师浏览仪器、发起预约、查看我的预约、取消预约仪器列表、预约表单、我的预约管理员/设备管理员审核预约、维护仪器信息、查看预约统计审核列表、仪器管理系统管理员维护用户角色、配置开放时间段、处理异常预约管理端或独立后台功能边界要控制住否则项目会失控。初版不需要做在线支付、设备报修、实验报告上传、积分系统先把“仪器信息展示、时间段预约、管理员审批、我的预约”这条闭环跑通。1.3 一条清晰的项目主线从查询到占用再到审核本文项目主线可以概括成一句话用户看到仪器列表选择某台仪器选择日期和时间段提交预约系统在提交时检查时间冲突管理员审核后该时间段被确认占用普通用户在自己的预约列表里查看状态。这条链路里有两个技术难点一是时间冲突判断二是审核状态流转。后面的数据库设计和接口实现都围绕这两个点展开。2. 总体架构与技术栈选型2.1 分层架构与数据流向从小程序端的角度看整个平台是典型的“前端页面 后端接口 数据库”三层结构微信小程序端负责页面渲染、表单输入、登录态获取和接口调用。后端服务负责微信登录 code 校验、用户注册、预约冲突检测、审批状态更新、数据查询。数据库保存用户、仪器、预约记录、开放时间规则。数据流向可以分成两条。一条是查询链路小程序请求仪器列表后端查询数据库并返回 JSON小程序渲染列表。另一条是写链路小程序提交预约后端执行冲突检测校验通过后写入预约记录状态为待审核管理员审核后状态变更为已通过或已拒绝。2.2 技术栈可以怎么选项目标题没有限定后端语言实际项目中常见搭配是 Spring Boot MyBatis/JPA MySQL也可以使用 Node.js、Python Django 或小程序云开发。下面用 Spring Boot 作为参考实现因为它是这类开源项目和毕业设计中最常见的组合。分层技术选型说明前端小程序微信原生小程序 / uni-app原生小程序学习成本低uni-app 可扩展发行为 H5 和 App后端Spring Boot提供 REST API管理预约流程数据库MySQL 5.7存储用户、仪器、预约与规则数据ORMMyBatis / MyBatis-Plus编写 SQL 与业务代码解耦鉴权JWT 微信登录 code2session无状态 Token适合小程序端部署云服务器 / 本地服务器生产环境必须 HTTPS 域名2.3 小程序端不是简单页面它承担了表单校验与状态交互很多初学者把小程序端当成“套壳页面”把大量业务逻辑放在后端这是对的但小程序端仍要承担至少三件事登录态获取、表单校验、加载态与错误提示。比如用户选择时间段时前端要判断结束时间是否晚于开始时间日期是否早于今天提交预约后要处理后端返回的冲突提示加载列表时要展示 loading 状态。这些都不是后端能替代的后端返回错误信息后前端必须给出可理解的提示。3. 数据库设计用几张表支撑预约闭环3.1 用户表用 openid 而不是用户名做唯一标识微信小程序登录后后端通过 code 换取 openidopenid 是用户在当前小程序下的唯一标识。用户表设计如下。CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信小程序 openid, nickname varchar(50) DEFAULT NULL COMMENT 用户昵称, avatar_url varchar(255) DEFAULT NULL COMMENT 头像地址, phone varchar(20) DEFAULT NULL COMMENT 联系电话, role varchar(20) NOT NULL DEFAULT student COMMENT student/teacher/admin, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意openid必须加唯一索引避免同一用户反复登录时产生重复记录。登录接口的职责是“没有则插入有则返回”不能每次登录都新建用户。3.2 仪器信息表状态字段要能支撑停用和维护仪器表保存设备的基础信息以及是否可预约状态。CREATE TABLE instrument ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 仪器名称, model varchar(100) DEFAULT NULL COMMENT 型号, location varchar(100) DEFAULT NULL COMMENT 存放位置, lab_name varchar(100) DEFAULT NULL COMMENT 所属实验室, manager_id bigint(20) DEFAULT NULL COMMENT 设备管理员用户ID, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1可预约 0停用, image_url varchar(255) DEFAULT NULL, description text COMMENT 设备说明, create_time datetime NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段一定要单独维护不能把“设备故障”和“没有预约记录”混在一起。设备维修时管理员把 status 置为 0前端列表就应该不再允许预约。3.3 预约记录表状态机决定整条业务流程预约表是整个系统的核心表它的状态字段直接决定业务流向。CREATE TABLE reservation ( id bigint(20) NOT NULL AUTO_INCREMENT, instrument_id bigint(20) NOT NULL COMMENT 仪器ID, user_id bigint(20) NOT NULL COMMENT 预约人ID, reserve_date date NOT NULL COMMENT 预约日期, start_time varchar(5) NOT NULL COMMENT 开始时间 HH:mm, end_time varchar(5) NOT NULL COMMENT 结束时间 HH:mm, purpose varchar(500) DEFAULT NULL COMMENT 实验用途, status varchar(20) NOT NULL DEFAULT PENDING COMMENT PENDING/APPROVED/REJECTED/CANCELLED/COMPLETED, reviewer_id bigint(20) DEFAULT NULL COMMENT 审核人ID, review_remark varchar(255) DEFAULT NULL COMMENT 审核备注, create_time datetime NOT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_instrument_date (instrument_id, reserve_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status使用字符串而不是数字可读性更好也不会出现 0、1、2、3 各自含义需要到处查注释的问题。推荐状态流转为当前状态可执行动作目标状态PENDING 待审核用户取消CANCELLEDPENDING 待审核管理员通过APPROVEDPENDING 待审核管理员拒绝REJECTEDAPPROVED 已通过使用完成COMPLETEDAPPROVED 已通过用户取消CANCELLED3.4 开放时间规则表为自动审批留好扩展位如果实验室对每台仪器有固定的开放时间段可以在初版增加一张时间规则表。CREATE TABLE instrument_rule ( id bigint(20) NOT NULL AUTO_INCREMENT, instrument_id bigint(20) NOT NULL, week_day tinyint(4) NOT NULL COMMENT 1-7对应周一至周日, start_time varchar(5) NOT NULL, end_time varchar(5) NOT NULL, PRIMARY KEY (id), KEY idx_instrument_week (instrument_id, week_day) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;初版如果只需要管理员人工审核可以暂时不校验开放规则但表结构先留着后续做自动审批时只需要在预约校验逻辑里增加一次规则查询。注意数据库字段不要使用time、status等过于泛化的命名而不加前缀注释。多表联查时这类字段会让 SQL 可读性迅速下降。建议使用reserve_date、start_time、end_time这样带明确语义的名字。4. 后端接口设计与预约冲突处理4.1 接口清单后端提供的接口可以按领域划分成三类接口方法说明权限/api/auth/loginPOST用微信 code 换取登录 token公开/api/instrumentsGET分页查询仪器列表登录/api/instruments/{id}GET查询仪器详情登录/api/reservationsPOST提交预约申请登录/api/reservations/mineGET查询我的预约登录/api/reservations/{id}/cancelPUT取消预约本人/api/reservations/{id}/approvePUT通过预约管理员/api/reservations/{id}/rejectPUT拒绝预约管理员接口设计时不要把权限都堆到一个方法里。审核类接口要在后端做角色判断不能只依赖前端隐藏按钮。4.2 微信登录与 Token 认证小程序端调用wx.login获取临时 code后端把 code 发给微信接口换取 openid。示例流程如下。PostMapping(/api/auth/login) public Result login(RequestBody LoginRequest request) { String openid wechatClient.code2Session(request.getCode()); User user userService.findOrCreateByOpenid(openid); String token jwtUtil.createToken(user.getId(), user.getRole()); return Result.ok(token); }这里的关键点是code是一次性的使用后立即失效。如果小程序端在短时间内重复提交登录请求第二次会拿到错误提示。因此前端要等wx.login成功后立即请求后端并把返回的 token 存入本地缓存。4.3 预约创建接口冲突检测的顺序不能乱预约创建接口是业务核心。推荐的实现顺序如下。PostMapping(/api/reservations) public Result createReservation(RequestBody ReservationRequest request) { // 1. 校验仪器存在且 status 1 Instrument instrument instrumentMapper.selectById(request.getInstrumentId()); if (instrument null) { return Result.fail(400, 仪器不存在); } if (instrument.getStatus() 0) { return Result.fail(400, 仪器当前不可预约); } // 2. 校验时间参数 if (!validationService.checkTimeRange(request.getStartTime(), request.getEndTime())) { return Result.fail(400, 结束时间必须晚于开始时间); } // 3. 查询该仪器在目标日期状态为 PENDING/APPROVED 的记录 int conflictCount reservationMapper.countConflict( request.getInstrumentId(), request.getReserveDate(), request.getStartTime(), request.getEndTime()); if (conflictCount 0) { return Result.fail(409, 该时间段已被预约); } // 4. 插入预约记录 Reservation reservation buildReservation(request, currentUser()); reservationMapper.insert(reservation); return Result.ok(reservation); }为什么第 3 步的冲突检测最重要因为它决定了同一台设备在重叠时间段不能被重复预约。后面会单独解释 SQL 怎么写以及并发时还需要补什么措施。4.4 冲突检测 SQL 与时间字符串比较的坑判断两个时间段重叠标准条件是新预约的开始时间小于已有预约的结束时间并且新预约的结束时间大于已有预约的开始时间。在 MySQL 中可以写成如下查询。select idcountConflict resultTypeint SELECT COUNT(*) FROM reservation WHERE instrument_id #{instrumentId} AND reserve_date #{date} AND status IN (PENDING, APPROVED) AND #{startTime} lt; end_time AND #{endTime} gt; start_time /select这条 SQL 成立有一个隐含前提start_time和end_time的格式必须统一。建议统一使用两位数的小时和分钟例如09:30不要存9:30。否则字符串比较会得到错误结果。举个例子9:00与10:00比较时字符串逐字符比较9大于1所以系统会认为9:00 10:00预约判断就会错乱。统一成09:00和10:00后字符串比较结果才与时间顺序一致。4.5 审核接口通过时也要查一次冲突有一种容易被忽略的情况两个预约在同一时间段都是 PENDING管理员先通过了 A再通过 B 时如果审核接口不做冲突检查就会把本应冲突的 B 也通过。审核接口的伪代码如下。PutMapping(/api/reservations/{id}/approve) public Result approve(PathVariable Long id) { Reservation reservation reservationMapper.selectById(id); if (!PENDING.equals(reservation.getStatus())) { return Result.fail(400, 当前状态不能审核); } int conflictCount reservationMapper.countConflict( reservation.getInstrumentId(), reservation.getReserveDate(), reservation.getStartTime(), reservation.getEndTime()); // 注意这里要把当前记录排除掉否则会把自己也算成冲突 if (conflictCount 1) { return Result.fail(409, 已有其他预约占用该时间段); } reservationMapper.updateStatus(id, APPROVED, currentAdminId()); return Result.ok(); }countConflict会把当前记录本身一起统计所以判断时要排除自己。更严谨的做法是在 SQL 中加入AND id ! #{currentId}或者业务代码里先查出当前记录再对比。5. 微信小程序端从列表到预约提交5.1 页面目录结构小程序端可以采用以下目录结构miniprogram/ ├─ pages/ │ ├─ index/index 首页 │ ├─ instruments/list 仪器列表 │ ├─ instrument/detail 仪器详情 │ ├─ reservation/create 预约表单 │ ├─ reservation/mine 我的预约 │ ├─ mine/index 个人中心 │ └─ admin/review 管理员审核列表 ├─ utils/ │ ├─ request.js wx.request Promise 封装 │ └─ auth.js 登录态处理 └─ app.js初版页面不要追求多能覆盖查询、预约、我的预约、审核四条核心路径即可。每个页面只做一件事代码会更容易维护。5.2 仪器列表与详情仪器列表页面从后端拉取数据并渲染成卡片。const request require(../../utils/request); Page({ data: { list: [], keyword: , loading: false, }, onShow() { this.loadList(); }, loadList() { this.setData({ loading: true }); request({ url: /api/instruments, data: { keyword: this.data.keyword }, }).then((data) { const list data.list.map((item) ({ ...item, statusText: item.status 1 ? 可预约 : 停用, statusClass: item.status 1 ? online : offline, })); this.setData({ list }); }).finally(() { this.setData({ loading: false }); }); }, goDetail(e) { wx.navigateTo({ url: /pages/instrument/detail?id${e.currentTarget.dataset.id}, }); }, });对应的 WXML 片段view classinstrument-card wx:for{{list}} wx:keyid >picker modedate start{{today}} value{{date}} bindchangeonDateChange view classpicker-value{{date || 请选择预约日期}}/view /picker picker modetime value{{startTime}} bindchangeonStartTimeChange view classpicker-value{{startTime || 请选择开始时间}}/view /picker picker modetime value{{endTime}} bindchangeonEndTimeChange view classpicker-value{{endTime || 请选择结束时间}}/view /picker提交预约时前端要做一次基础校验submit() { const { date, startTime, endTime, purpose } this.data; if (!date || !startTime || !endTime) { wx.showToast({ title: 请选择完整时间, icon: none }); return; } if (endTime startTime) { wx.showToast({ title: 结束时间必须晚于开始时间, icon: none }); return; } wx.showLoading({ title: 提交中 }); request({ url: /api/reservations, method: POST, data: { instrumentId: this.data.instrumentId, reserveDate: date, startTime, endTime, purpose, }, }).then(() { wx.hideLoading(); wx.showToast({ title: 申请已提交 }); setTimeout(() wx.navigateBack(), 1500); }).catch(() { wx.hideLoading(); }); }注意endTime startTime这里只适用于同一天内的预约如果系统未来支持跨天预约比较逻辑要换成日期与时间组合判断。5.4 请求封装与登录态刷新每次请求都手动写wx.request容易导致代码重复。建议封装一个 Promise 版本的请求工具。// utils/request.js const request (options) { return new Promise((resolve, reject) { wx.request({ url: ${getApp().globalData.baseUrl}${options.url}, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: Bearer ${wx.getStorageSync(token)}, }, success: (res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else { wx.showToast({ title: res.data res.data.message ? res.data.message : 请求失败, icon: none, }); reject(res); } }, fail: (err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); }, }); }); }; module.exports request;登录态过期时小程序端需要在收到 401 后重新走wx.login刷新 token 并重放请求。初版可以只弹出“请重新登录”并回到首页生产环境则建议实现静默刷新。6. 联调验证从学习环境到管理员审核6.1 本地联调顺序本地联调推荐按以下顺序进行避免一上来就把所有功能一起测出错后难以定位。先启动后端访问GET /api/instruments确认数据库连接正常、仪器数据能返回。再用小程序开发者工具进入列表页确认接口域名、request 合法域名和 header 能正常通过。然后模拟登录确认能拿到 token。提交一条预约确认能插入记录且没有冲突报错。用管理员账号审核确认状态从 PENDING 变为 APPROVED。再提交一条重叠时间段预约确认返回 409 冲突提示。6.2 小程序开发者工具与真机的差异开发者工具中勾选“不校验合法域名”后可以使用 http 和 localhost但真机预览不会走这个开关必须使用 HTTPS并且域名要在小程序管理后台配置到 request 合法域名列表中。所以本地调试阶段前后端在同一台机器可以直接用开发者工具访问http://localhost:8080进入真机测试阶段需要部署后端到服务器申请 HTTPS 域名后切换 baseUrl。6.3 用一组数据验证核心场景下面是一组可以用来验证的示例数据。仪器日期时间段预期结果高速离心机2025-03-1009:00-10:00提交成功PENDING高速离心机2025-03-1009:30-10:30返回冲突提示高速离心机2025-03-1010:00-11:00提交成功不冲突停用仪器任意任意返回“仪器当前不可预约”边界情况也要测结束时间等于开始时间应该失败预约日期早于今天应该失败同一用户重复预约同一时间段仍然应该失败因为冲突规则与用户无关。7. 常见问题与排查路径7.1 真机预览时请求失败现象开发者工具里正常真机预览后页面加载不出数据。可能原因有三个。一是接口域名是 http微信小程序真机限制必须 HTTPS。二是域名没有在小程序管理后台配置到 request 合法域名或者配置后没有重新编译小程序。三是后端没有部署到公网真机访问不到 localhost。排查顺序先看控制台报错是request:fail还是url not in domain list。前者通常是网络和后端问题后者是合法域名配置问题。生产环境要求后端使用 HTTPS 证书并保证证书链完整。7.2 获取用户昵称和头像失败现象调用旧版wx.getUserProfile后拿不到昵称或者弹窗被取消。这是微信平台隐私接口调整的影响。现在的推荐做法是使用button open-typechooseAvatar选择头像使用input typenickname输入昵称再把结果提交给后端保存。不要在页面的onLoad里主动弹头像授权框也不要把头像昵称作为强制注册条件。7.3 时间冲突判断不准确现象明显重叠的时间段没有提示冲突或者不重叠的时间段被判定为冲突。最常见原因是时间格式不统一。数据库中同时存在9:00和09:00时字符串比较结果不可靠。其次SQL 中的状态条件可能漏掉 PENDING导致待审核的记录不参与冲突判断。检查方向是先把预约表中的start_time、end_time统一成HH:mm再手动执行 countConflict SQL 看结果。7.4 并发重复提交导致同一时段被占两次现象两个人几乎同时提交同一时间段都提示成功。原因是两次请求同时通过了冲突查询都没有查到对方记录然后都执行了插入。解决方式有三种方案说明适用场景事务 唯一约束在预约表增加冲突唯一键但时间段是区间无法简单用唯一索引实现不推荐单独使用事务 悲观锁在查询仪器或时间段记录时SELECT ... FOR UPDATE单库场景可行事务 条件更新配合分布式锁或条件插入生产环境更可靠学习环境可以先不处理并发但生产环境必须在数据库和接口两层都做约束并记录日志。注意生产环境不要只在业务代码里用 if 判断冲突还需要配合数据库级别约束或分布式锁。单纯靠查询再插入遇到并发就会产生竞态条件。7.5 预约状态被覆盖现象审核通过后回到列表页面还是显示旧状态。常见原因是前端列表页使用了本地缓存或者onShow没有重新请求接口。解决方法是每次进入页面或下拉刷新时都重新拉取数据审核操作后调用this.loadList()而不是仅更新当前 item。8. 从可运行项目到生产级开源项目8.1 学习环境与生产环境的差异本地运行只要一个 MySQL 和 Java 环境就能跑通但要作为开源项目发布还需要补全以下内容项目 README说明技术栈、启动步骤、数据库初始化脚本位置。默认账号体系说明手动在数据库插入一个管理员账号。小程序 AppID 替换说明明确 openid 会随 AppID 变化而变化。生产环境配置项外置把数据库密码、小程序 appSecret、HTTPS 证书路径放进配置文件或环境变量。注意小程序的 openid 与 AppID 是绑定关系。换一个 AppID同一微信号的 openid 也会不同所以开源项目文档里一定要写清楚如何在后台替换 AppID 和 appSecret。8.2 权限和安全的强制要求接口层要加角色校验不能只靠前端显示控制。Spring Boot 中可以在拦截器或注解上校验角色。数据库连接账号不要使用 root单独创建业务账号并限制权限。生产环境开启后端日志但不要打印用户 openid 和 token 明文。8.3 扩展方向自动审批、订阅消息与统计报表第一版人工审核跑通后可以根据实际需求扩展自动审批如果仪器不需要管理员人工确认可以在校验通过后直接置为 APPROVED同时保留人工取消入口。订阅消息用户提交预约或审批通过后通过小程序订阅消息通知用户。需要注意订阅消息的模板 ID、用户授权次数限制和下发时机。统计报表按仪器、按日期统计预约次数和使用时长为管理员购置设备和安排维护提供数据支撑。8.4 给新手的下一步练习建议如果这篇文章的核心流程你已经能独立写出来建议再做三个练习第一把预约时间细化成 30 分钟粒度增加时间选择器的分段时间第二加入设备停用和维护状态让管理员可以手动关闭某台仪器的预约第三给预约记录增加分页模拟一段时间内大量预约数据时的查询效果。这三个练习分别覆盖了前端组件封装、状态管理和后端性能优化完成后再回头看数据库设计和冲突检测逻辑会比刚写完核心流程时理解更深。
返回列表