news 2026/9/7 20:30:48

实验室预约小程序开发实战:数据库设计与并发冲突处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
实验室预约小程序开发实战:数据库设计与并发冲突处理

简介:这是一套面向高校实验室管理人员与信息化建设者的微信小程序完整解决方案,聚焦解决传统实验室设备预约依赖微信群、登记混乱、溯源困难等管理痛点。资源采用云开发架构,涵盖实验室动态发布、规章制度展示、基础/专业实验室分级预约、多级审批流、用户权限管理等核心功能,并支持灵活设置预约时段与人数、自定义表单字段、二维码核销签到及Excel数据导出,助力实现有序实验、规范管理和效率提升。压缩包共487个文件,含186个JS逻辑脚本(如qrcode_lib.js、db_util.js)、105个WXSS样式文件、82个WXML页面结构及70个JSON配置,整体3.55MB,结构清晰、模块解耦度高。目前已有290人学习下载,配套提供安装使用手册(.docx)与默认界面资源(.gif/.jpg),便于快速部署与二次开发。 做这个实验室预约小程序之前,我先盘了一下实验室现有的管理方式:一张A4纸贴在门口,学生来了自己填时间段签名,管理员每周五晚上对着Excel核对有没有撞车,碰上毕业季设备集中使用那几天,纸质记录根本对不上账。更烦人的是"基础实验室"和"专业实验室"两套流程完全不是一个逻辑——基础实验室讲究时段开放、先到先得,专业实验室因为涉及大型设备,需要指导老师审批、限定操作资质。硬塞进同一套模板里做,后期必然返工。这篇项目文章我尽量把从需求梳理、数据库设计到前后端联调的完整思路讲清楚,适合正在做同类管理系统的朋友参考。

1. 需求拆解:基础实验室预约和专业实验室预约本质是两种业务

很多人动手写代码前会掉进一个坑:看到"实验室预约"四个字,就直接去建了一张预约表,字段无非是谁预约、哪个时段、哪个实验室、审批状态。等做到一半才会发现,基础实验室和实验室专业预约的流程根本拧不到一起。

基础实验室的场景是"公共自习式"使用。学生个人预约,按时间段抢占资源,系统只需要保证同一个时段同一个实验台不重复分配,管理员无需逐个审批,预约成功即生效。它更像图书馆座位预约,核心是并发控制。

专业实验室的场景完全不同。使用者往往是课题小组,预约单位不是"一个人"而是"一个项目组",不仅要锁定时段,还要绑定指导教师、指定设备、登记实验内容。审批链至少两级:指导教师初审、实验室管理员终审。管理员还要能驳回并填写原因,学生端要能重新编辑申请再提交。

所以数据模型上我拆成了两张预约主表,一张basic_reservation管基础实验室,一张professional_reservation管专业实验室,而不是共用一张大表靠字段区分。这么做有两个好处:一是两张表的字段差异大(专业预约多出项目名称、指导教师、设备ID、安全承诺书等字段),硬并一张表会堆出一大片空字段;二是后续统计和审批逻辑可以各自独立演进,不会改一处崩另一处。

用户角色也重新梳理过,不只有学生和管理员两级,而是四类:

角色核心权限数据可见范围
学生发起预约、查看自己的记录、取消待审批预约仅本人
指导教师审核名下学生的专业实验室申请名下学生提交的记录
实验室管理员审批、驳回、调整预约、发布动态与规章制度所管实验室的全部记录
系统管理员用户管理、角色分配、全部数据管理全部

权限这块用Shiro或者Spring Security都可以,如果项目不大,直接在网关/拦截器里按URL前缀做角色校验也够用。小程序的请求头里带上token,后端解析出用户角色,再判断该接口是否放行。

2. 数据库设计:预约表不能用"开始时间+结束时间"一存了之

下面直接给核心表结构,这些字段都是踩过坑之后调整过的版本。

2.1 用户表

CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '学号/工号', `password` varchar(128) NOT NULL COMMENT 'BCrypt加密存储', `real_name` varchar(50) DEFAULT NULL, `role` tinyint(4) NOT NULL COMMENT '1学生 2指导教师 3实验室管理员 4系统管理员', `college` varchar(100) DEFAULT NULL COMMENT '所属院系', `phone` varchar(20) DEFAULT NULL, `status` tinyint(4) DEFAULT 1 COMMENT '1启用 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

角色字段我用的是tinyint而不是字符串,省空间且索引效率高,但可读性确实差点。真正开发时建议用常量类或枚举来约束,别在代码里到处裸写魔法数字。

2.2 实验室表

CREATE TABLE `lab_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `lab_name` varchar(100) NOT NULL, `lab_type` tinyint(4) NOT NULL COMMENT '1基础实验室 2专业实验室', `location` varchar(100) DEFAULT NULL COMMENT '所在校区/楼栋/房间', `capacity` int(11) DEFAULT NULL COMMENT '可容纳人数', `open_time` varchar(50) DEFAULT NULL COMMENT '开放时间段描述', `manager_id` bigint(20) DEFAULT NULL COMMENT '负责人,关联sys_user', `status` tinyint(4) DEFAULT 1 COMMENT '1开放 0关闭', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

很多系统会忽略manager_id这个字段,导致后期"某个实验室由谁负责审批"这个信息无处安放,管理员只能手工在业务代码里做映射。把负责人挂在实验室表上,审批流就能直接根据lab_info.manager_id动态找到审批人,不用单独维护一套审批人配置表。

2.3 基础实验室预约表

CREATE TABLE `basic_reservation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `lab_id` bigint(20) NOT NULL, `seat_no` varchar(20) DEFAULT NULL COMMENT '座号/台号', `reserve_date` date NOT NULL, `start_time` time NOT NULL, `end_time` time NOT NULL, `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0已预约 1已使用 2已取消 3爽约', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_lab_date_time` (`lab_id`, `reserve_date`, `start_time`, `end_time`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意idx_lab_date_time这个联合索引,它是冲突检测的核心依赖。

2.4 专业实验室预约表

CREATE TABLE `professional_reservation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '申请人', `teacher_id` bigint(20) NOT NULL COMMENT '指导教师', `lab_id` bigint(20) NOT NULL, `device_name` varchar(100) DEFAULT NULL COMMENT '需要使用的设备', `project_name` varchar(200) DEFAULT NULL COMMENT '项目/课题名称', `experiment_content` text COMMENT '实验内容说明', `reserve_date` date NOT NULL, `start_time` time NOT NULL, `end_time` time NOT NULL, `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待教师审批 1教师已通过/待管理员终审 2已通过 3已驳回 4已取消 5已完成', `teacher_comment` varchar(500) DEFAULT NULL, `admin_comment` varchar(500) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_lab_date_time` (`lab_id`, `reserve_date`, `start_time`, `end_time`), KEY `idx_teacher_id` (`teacher_id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

专业预约的状态机比基础预约复杂得多。我最初只设计了"待审批/已通过/已驳回"三个状态,后来发现教师通过之后还需要管理员终审,不得不在中间加了一个状态。这里给个建议:状态字段宁多勿少,因为线上出现"卡住"的时候,多半是状态流转路径没设计完整。

2.5 实验室动态表与规章制度表

这两个表可以合并成一张lab_article,用article_type区分"动态"还是"规章制度":

CREATE TABLE `lab_article` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL, `content` text NOT NULL, `article_type` tinyint(4) NOT NULL COMMENT '1动态 2规章制度', `publisher_id` bigint(20) NOT NULL, `publish_time` datetime DEFAULT CURRENT_TIMESTAMP, `view_count` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

把"实验室动态"和"实验室规章制度"合在一张表里,展示端通过article_type分开渲染。有的系统会做两张表,但实际业务里这两类内容的维护人是同一个角色(实验室管理员),发布流程、富文本格式几乎一致,分表是过度设计。

3. 并发冲突检测:后端预约核心算法

这是整个项目里最需要扣细节的地方。预约冲突的本质是:给定一个时间段[new_start, new_end),判断同一实验室、同一天是否已存在时间重叠的记录。

3.1 重叠判断的正确写法

SQL直觉上会写成:

SELECT COUNT(*) FROM basic_reservation WHERE lab_id = ? AND reserve_date = ? AND start_time < ? AND end_time > ?

这个条件的意思是"已有的开始时间晚于新预约的开始时间,并且已有的结束时间早于新预约的结束时间"——不对,这个逻辑是反的。

正确判断区间重叠的条件是:已有开始时间 < 新结束时间 AND 已有结束时间 > 新开始时间。用Java实现一个工具方法:

public boolean hasConflict(Long labId, LocalDate date, LocalTime start, LocalTime end) { LambdaQueryWrapper<BasicReservation> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(BasicReservation::getLabId, labId) .eq(BasicReservation::getReserveDate, date) .in(BasicReservation::getStatus, 0, 1) // 已预约、已使用都算占用 .lt(BasicReservation::getStartTime, end) .gt(BasicReservation::getEndTime, start); Integer count = basicReservationMapper.selectCount(wrapper); return count != null && count > 0; }

这段代码的核心逻辑就是"新开始时间必须晚于已有结束时间,且新结束时间必须早于已有开始时间"才算不冲突,反过来只要有部分重叠就冲突。这个判断包含四种情况:新预约完全包含已有、新预约被已有包含、新预约与已有首尾部分重叠、新预约恰好挨着已有时间段的边界。边界粘连的情况(比如旧的是9:00-10:00,新的是10:00-11:00),上面用的是严格大于和严格小于,所以不会误判为冲突,这是符合实际场景的。

3.2 真正的坑:并发下的超卖

单条SQL查询没问题,但两个用户同时提交同一个时间段,都先执行SELECT,发现都没记录,然后都执行INSERT,最终就冲突了。解决方式是数据库层面加锁。

第一种方案是悲观锁,在查询时加FOR UPDATE

List<BasicReservation> list = basicReservationMapper.selectListForUpdate(wrapper);

FOR UPDATE依赖事务,而且锁的粒度是表级(InnoDB没有gap lock时很容易锁全表),并发高的时候性能会受影响。

第二种方案是唯一索引约束。在表上加uk_lab_date_start唯一索引(lab_id, reserve_date, start_time),数据库层面保证同一个实验室同一天同一个开始时间只能有一条记录。但这样只能挡住"完全同秒"的冲突,挡不住部分重叠(9:00-10:00和9:30-10:30),所以只能作为辅助兜底。

我最终采用的是第二种方案(唯一索引+代码层面重叠检测),再加一层分布式锁兜底:

String lockKey = "lab:reserve:" + labId + ":" + reserveDate; RLock lock = redissonClient.getLock(lockKey); try { lock.lock(5, TimeUnit.SECONDS); if (hasConflict(...)) { throw new BusinessException("该时间段已被预约"); } // 执行插入 } finally { lock.unlock(); }

labId+date维度加锁,既控制了锁粒度,又保证同一实验室同一天的预约操作串行执行。这是最稳妥的写法,也是我在线上环境验证过的方案。

3.3 审批状态与"占位"的处理

专业实验室预约有一个特殊场景:教师已经通过、管理员还没终审的预约算不算占用时间段?答案是算。如果不把中间状态的记录算作占用,就会出现管理员终审时发现两个已经"教师通过"的申请撞了同一个时间段,尴尬得很。

所以在冲突检测时,专业预约查询的状态集合是0待教师审批、1教师已通过、2已通过。只有状态为3(已驳回)、4(已取消)的记录不参与冲突判断。

4. 预约审批流程设计:从学生端到管理员端的完整闭环

审批流是这个项目的核心业务环节,我用了状态机+事件驱动的思路来实现,而不是简单的if-else。

4.1 状态机定义

专业实验室预约的状态流转:

  • 0待教师审批:学生提交申请后,系统自动通知指导教师
  • 1教师已通过/待管理员终审:教师通过,等待管理员终审
  • 2已通过:管理员终审通过,预约生效
  • 3已驳回:教师或管理员任一环节驳回,流程终止
  • 4已取消:学生主动取消,或管理员强制取消
  • 5已完成:预约时间结束后,系统自动标记

代码里我用枚举来约束状态流转:

public enum ReservationStatus { PENDING_TEACHER(0), PENDING_ADMIN(1), APPROVED(2), REJECTED(3), CANCELLED(4), FINISHED(5); private final int code; // getter... }

审批操作对应的状态迁移方法:

public void approve(ProfessionalReservation reservation, String approverRole, String comment) { if ("teacher".equals(approverRole) && reservation.getStatus() == 0) { reservation.setStatus(1); // 教师通过,流转到管理员 reservation.setTeacherComment(comment); } else if ("admin".equals(approverRole) && reservation.getStatus() == 1) { reservation.setStatus(2); // 管理员终审通过 reservation.setAdminComment(comment); } else { throw new BusinessException("当前状态下不允许该操作"); } }

4.2 通知机制

审批节点的流转需要即时通知用户。如果项目里没有引入消息队列,直接在小程序端轮询是下策,体验也很差。我采用的是微信小程序订阅消息(subscribeMessage)方案。学生提交预约时,先请求用户授权订阅"预约结果通知",后端在审批结果落库后调用微信接口推送通知。

但这个方案有一个必须注意的坑:微信小程序订阅消息属于"一次性订阅",用户每授权一次只能收到一条消息。这意味着学生提交申请时得弹两次订阅授权框(一次给"审批通过通知",一次给"审批驳回通知"),体验稍差。如果你们学校已经接入了企业微信或者自有消息中心,优先级更高。

4.3 取消与爽约处理

基础预约一旦"已预约"即生效,学生可以取消,但需要在实验开始前至少2小时操作,否则后台自动记为"爽约",累计3次爽约则限制预约资格7天。这块逻辑我在定时任务里做了,每天扫一遍预约记录:

@Scheduled(cron = "0 30 1 * * ?") // 每天凌晨1:30执行 public void markNoShow() { List<BasicReservation> list = basicReservationMapper.selectList( new LambdaQueryWrapper<BasicReservation>() .eq(BasicReservation::getStatus, 0) .lt(BasicReservation::getReserveDate, LocalDate.now()) ); for (BasicReservation reservation : list) { reservation.setStatus(3); // 标记为爽约 } }

注意LocalDate.now()只拿到当天日期,如果预约日期早于今天的记录还停留在"已预约"状态,说明学生没来也没取消,直接记为爽约。这里不需要精确到具体时刻,凌晨跑批的时候,昨天和更早的预约都算过期。

5. 后端接口设计:按业务域拆分配置

后端我用的是Spring Boot + MyBatis-Plus,接口设计上遵循RESTful风格,但实操中我不会死扣资源的"名词复数"规范,更多按业务操作来命名。核心接口如下:

5.1 基础实验室预约相关接口

接口方法说明
/api/basic/labsGET获取可预约的基础实验室列表
/api/basic/reservationsPOST提交基础预约
/api/basic/reservations/myGET查询我的预约记录
/api/basic/reservations/{id}/cancelPUT取消预约(2小时前)
/api/basic/reservations/{id}DELETE管理员删除预约记录

5.2 专业实验室预约相关接口

接口方法说明
/api/professional/labsGET获取专业实验室列表(含设备信息)
/api/professional/reservationsPOST提交专业预约申请
/api/professional/reservations/pending/teacherGET教师查询待审核列表
/api/professional/reservations/pending/adminGET管理员查询待终审列表
/api/professional/reservations/{id}/approvePUT审批通过
/api/professional/reservations/{id}/rejectPUT审批驳回

5.3 实验室动态与规章制度接口

接口方法说明
/api/articles?type=1GET获取实验室动态列表(分页)
/api/articles?type=2GET获取规章制度列表
/api/articles/{id}GET获取文章详情
/api/articlesPOST管理员发布文章

分页查询统一使用MyBatis-Plus的Page对象,返回结构封装为{ code, message, data, total }。前端小程序端用uni.request封装了请求函数,自动在header中塞入token,遇到401状态码跳回登录页。

实际开发中的一个小建议:接口路径不要按功能模块分目录,直接按业务域拆Controller。比如BasicReservationController只处理基础预约相关的全部操作,ProfessionalReservationController只处理专业预约的。我在第一个版本里曾按"ReservationController"统一管理两种预约,结果Controller膨胀到两千多行,后面拆的时候异常痛苦。

6. 小程序端实现:页面结构、状态管理与权限控制

小程序端我用的uni-app,一套代码可以发微信小程序、H5和App,后续扩展比较方便。下面是页面结构和核心交互逻辑。

6.1 页面目录结构

pages/ ├── index/ // 首页:实验室动态+规章制度入口 │ ├── index.vue │ └── article-detail.vue ├── reservation/ // 预约 │ ├── basic-lab-list.vue // 基础实验室列表 │ ├── basic-reserve.vue // 基础预约提交 │ ├── professional-lab-list.vue // 专业实验室列表 │ ├── professional-reserve.vue // 专业预约申请表单 │ └── my-reservation.vue // 我的预约 ├── approval/ │ ├── teacher-approval.vue // 教师审批列表 │ └── admin-approval.vue // 管理员终审列表 ├── user/ │ ├── login.vue │ ├── profile.vue │ └── admin-dashboard.vue // 用户管理+数据概览

6.2 登录与角色路由

小程序端首次登录需要获取微信授权手机号(phonenumber.getPhoneNumber),然后后端根据手机号匹配用户库,找不到就跳转绑定页让用户输入学号和姓名。这一步务必注意:

微信小程序的wx.login拿到的code只能用一次,换取openid的接口需要在后端调,不能在小程序端直接调。同时openid要保存到sys_user表里,下次登录直接根据openid匹配用户。

登录后的角色路由,我在uni-appuni.addInterceptor拦截器里统一处理。路由跳转前判断当前用户角色和页面要求权限是否匹配,不匹配直接uni.switchTab回首页。这个方案比每个页面里写if (role !== 'admin')要干净得多。

6.3 专业预约表单的交互细节

专业预约表单是全项目最复杂的页面,字段有实验室、设备、时间、项目名称、指导教师、实验内容、安全承诺书等。其中"指导教师"这个字段,前端不能做成自由输入框,否则学生随便填一个名字,审批流转时后端根本找不到这个人。

我从接口/api/users/teachers拉取可选指导教师列表,表单里做成下拉选择器(picker组件),选中后直接提交teacher_id。后端在创建预约时也会校验teacher_id是否真实存在且角色为教师,双重保险。

时间选择器方面,小程序端我用的是两个并列的picker mode="time",一个选开始时间,一个选结束时间。onChange事件里加了一个快速校验:

onTimeChange() { if (this.form.endTime <= this.form.startTime) { uni.showToast({ title: '结束时间必须晚于开始时间', icon: 'none' }); this.form.endTime = ''; return; } }

6.4 用户管理页面的权限控制

用户管理是系统管理员的专属功能。页面里我放了用户列表、搜索框(按姓名/学号)、角色下拉筛选、启用/禁用按钮和分配角色对话框。后端对应的接口是:

@GetMapping("/admin/users") @PreAuthorize("hasRole('ADMIN')") public Result userList(@RequestParam Integer page, @RequestParam Integer size, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.hasText(keyword), SysUser::getRealName, keyword) .or(StringUtils.hasText(keyword)) .like(StringUtils.hasText(keyword), SysUser::getUsername, keyword); Page<SysUser> result = userService.page(new Page<>(page, size), wrapper); return Result.success(result); }

这里有一个隐患需要注意:在使用LambdaQueryWrapper时,likeor拼接的括号逻辑很绕。上面这段代码如果没有加(A or B) and C这样的括号分隔,很容易查出无关数据。我建议是在这种多条件搜索场景里直接用QueryWrapper配合and(w -> w.like().or().like())的方式构造条件,并在MP的配置里开启wrapper拆分,确保SQL执行时条件分组正确。实操里我踩过一次,查"张三"返回了全表,因为条件被解析成了"张三 or 真 or 管理员"。

6.5 预约记录列表的状态展示

"我的预约"这个页面,我用Tab切换展示"待审批/已通过/已驳回"三类记录。状态用不同颜色的标签标识:

  • 待审批:橙色(基础预约不展示该状态,专业预约展示)
  • 已通过:绿色
  • 已驳回:红色
  • 已取消:灰色

列表卡片上除了基础字段,还加了"取消预约"按钮,仅在状态为待审批(专业)或已预约(基础)且距离开始时间超过2小时时才展示。后端接口同样校验时间差,防止前端绕过限制。

7. 前后端联调的坑:从白屏到数据错乱

开发中最耗时间的不是写代码,而是前后端联调和环境部署。这里记录几个高频问题,遇到了可以直接对照排查。

7.1 跨域与Session问题

如果你在本地用Vite跑前端,后端是Spring Boot,跨域配置是必须的。但不要图省事直接allowedOriginPatterns("*")。如果小程序端需要携带Cookie(虽然我们用的是token),还需要配置allowCredentials(true),此时origin不能为*,必须明确指定。

我的跨域配置最终是这样:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

allowedOriginPatterns("*")allowCredentials(true)在Spring Boot 2.4+版本中可以共存,这要归功于新的CORS处理逻辑。但如果你在旧版本,*会和allowCredentials冲突,启动直接报错。

7.2 时间字段的时区陷阱

这个坑很隐蔽,但也最常见。MySQL连接串里的serverTimezone如果设置成UTC,存进去的时间会比北京时间早8小时,查询出来用户预约的时间就"莫名其妙"变成了凌晨。我刚开始写的时候也没注意,上线第一天学生反馈"预约9:00-10:00,结果展示的是1:00-2:00",排查了半天才锁定是时区问题。

正确设置:

spring.datasource.url=jdbc:mysql://localhost:3306/lab_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true

注意useSSL=falseallowPublicKeyRetrieval=true,这两个参数是MySQL 8.x的必备项,不然首次连接报Public Key Retrieval is not allowed

前端时间展示也需要统一。小程序端使用dayjs进行时间格式化,后端返回的时间统一用yyyy-MM-dd HH:mm:ss字符串格式(通过Jackson配置spring.jackson.date-format),避免前后端因序列化机制不同出现格式错乱。

7.3 小程序真机调试白屏

这个问题在uni-app开发中很常见:模拟器正常,真机预览白屏或部分组件不渲染。我当时逐一排查后发现两个原因:

一是uni-app里的page路由配置中,页面路径写错了,真机环境下路由解析失败就会白屏。模拟器对路径容错更强,真机不行。

二是生命周期问题。我在onLoad里调接口,但接口还没返回时页面就渲染了,字段引用不存在导致组件报错。解决方式是加v-if判断,数据返回后再渲染页面主体。

7.4 部署与Nginx配置

我实际部署用的是Docker Compose部署前后端分离的项目。前端是uni-app构建出来的H5版本(dist/build/h5目录)和微信小程序版本(dist/dev/mp-weixin目录),H5版本用Nginx托管,小程序版本通过微信开发者工具上传。

Nginx的关键配置是后端接口反向代理:

server { listen 80; server_name your-domain.com; root /var/www/lab-frontend; index index.html; location /api/ { proxy_pass http://backend:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

上传小程序前,重点检查小程序的合法域名配置。所有请求的接口域名必须在小程序后台配置授权,且必须是HTTPS。开发模式可以在详情里勾选"不校验合法域名",但正式版本不配置绝对无法使用。

8. 几个容易忽略但必须处理的后端细节

8.1 分页查询与数据脱敏

用户管理列表的分页接口,如果不做脱敏,手机号直接返回给前端会有数据泄露风险。我在后端做了字段级别的掩码处理:

public SysUser maskPhone(SysUser user) { String phone = user.getPhone(); if (StringUtils.hasText(phone) && phone.length() == 11) { user.setPhone(phone.substring(0, 3) + "****" + phone.substring(7)); } return user; }

小程序端展示也做了敏感信息脱敏,比如用户列表里的手机号只保留前3后4位。这种事后端统一做比前端做靠谱,防止有人直接抓包看接口数据。

8.2 预约历史数据自动清理

预约数据是日积月累膨胀的,不清理的话,半年后basic_reservation表就是几十万行,查询会明显变慢。我写了一个定时任务,把已完成的预约记录归档到reservation_bak表:

@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void archiveData() { LocalDate threshold = LocalDate.now().minusMonths(3); List<BasicReservation> finishedList = basicReservationMapper.selectList( new LambdaQueryWrapper<BasicReservation>() .lt(BasicReservation::getReserveDate, threshold) .in(BasicReservation::getStatus, 1, 3, 5) ); if (!finishedList.isEmpty()) { reservationBakMapper.insertBatch(finishedList); basicReservationMapper.deleteBatchIds( finishedList.stream().map(BasicReservation::getId).collect(Collectors.toList()) ); } }

归档后历史数据不一定完全用不上,比如期末统计的时候还是需要往年的数据做同比。所以我是按月份切片来归档,分表名reservation_bak_202501这种形式,后续统计直接按月捞数据,性能也够。

8.3 实验室动态的富文本存储

实验室动态和规章制度的正文内容,如果用普通的textarea存储,用户发一个换行都会存成\n,前端展示时想排版也排不了。我直接接入了wangEditor富文本编辑器(前端H5管理端),后端存储的是HTML片段。展示端用rich-text组件渲染。

注意小程序端rich-text组件不支持所有HTML标签,比如style属性部分不支持,class选择器不支持。如果后期要展示复杂表格或代码块,建议先做一层HTML清洗,只保留基础标签。我是在后端保存时做了一个白名单过滤,把scriptiframe这类有安全风险的标签直接剥离。

9. 项目上线后的真实体验与迭代方向

预约管理平台上线后的第一周,后台统计数据显示:学生端的预约操作数远超管理员端,且高峰集中在实验课截止日的前三天。这也从侧面说明,这类系统的本质是"资源管理器",它真正的价值不在于审批,而在于资源的高效调度。

目前这个版本我还在持续迭代,有几个方向比较明确。

9.1 设备级预约

当前专业实验室的预约粒度还是"整个实验室",但很多课题组的实验只需要用到实验室里的某台设备。如果改成设备级预约,需要新增一张device_info表和一个lab_device_relation关联表,时间冲突检测的维度从lab_id变成device_id。现有代码里的hasConflict方法只需要替换查询目标就能复用。

9.2 考勤核验

基础实验室的"爽约"目前靠定时任务假设"没取消就是没来",其实并不精确。后续可以结合门禁闸机数据,或者在小程序端加入签到码机制(用户到现场扫码签到),后端在预约开始时间后15分钟内收到签到请求才标记"已使用",超过则自动标记"爽约"。

9.3 数据大屏

用户管理、预约统计这块的数据,可以用EChartsDataV做一张实验室运营数据大屏,展示每日预约人数、设备使用率、热门实验时段等指标。这既能给实验室主任看运营效果,也能在学期评优时提供数据支撑。

现在项目主页上已经挂出来的功能就是标题里提到的这些:实验室动态、实验室规章制度、预约审批、用户管理、基础实验室预约、专业实验室预约。每个模块单独看都不复杂,但把流程串起来、把边界情况处理干净,才是这类管理系统真正的工作量所在。

如果后续你有兴趣,我可以把预约冲突检测的压测数据、审批流转的时序设计、以及小程序端消息通知的完整实现分别展开写一写。欢迎评论区交流你们学校实验室预约是怎么管理的,都有哪些我没想到的边界情况。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 7:02:42

汇川AM600+Codesys多轴控制实战:从电子齿轮到指针应用

简介&#xff1a;本资源是一套面向自动化工程师与PLC进阶学习者的新能源领域实战案例&#xff0c;聚焦汇川中大型PLC在多轴协同控制中的Codesys编程实现&#xff0c;解决复杂运动控制逻辑设计、实时同步、指针动态寻址及HMI交互集成等核心问题。压缩包共80个文件&#xff0c;含…

作者头像 李华
网站建设 2026/9/5 19:30:14

通信系统仿真实战:Matlab/Simulink建模与误码率分析指南

简介&#xff1a;本资源是一套面向计算机、电子信息工程及数学等专业本科生的通信系统建模与仿真实践材料&#xff0c;适用于课程设计、期末大作业或毕业设计中通信原理相关模块的参考实现。依托Matlab与Simulink平台&#xff0c;覆盖调制解调、信道编码、噪声建模、误码率分析…

作者头像 李华
网站建设 2026/9/4 1:53:03

基于YOLOv8的煤矿传送带堆煤堵塞预警系统实现

简介&#xff1a;本资源是一套面向计算机、人工智能及自动化等专业学生的毕业设计级项目&#xff0c;聚焦煤矿安全生产场景&#xff0c;基于YOLOv8实现传送带堆煤与堵塞的实时目标检测与智能预警。项目开箱即用&#xff0c;涵盖完整训练流程、可视化交互界面与工业级部署方案&a…

作者头像 李华
网站建设 2026/9/5 18:01:02

HAVN HS420 VGPU vs 酷冷至尊 HAF 500二代:展示与散热如何选?

如果你正站在这两款机箱中间纠结&#xff0c;我猜你不是找不到选择标准&#xff0c;而是被两种完全不同的设计语言同时抓住了。左边是 HAVN HS420 VGPU&#xff0c;名字里就带着垂直显卡和视觉展示的暗示&#xff1b;右边是酷冷至尊 HAF 500二代&#xff0c;延续着 HAF 系列“高…

作者头像 李华
网站建设 2026/9/6 1:28:21

从零搭建背景型智能体:以钢琴键知识问答为例

最近在一个乐器类产品的开发群里&#xff0c;有人问了个很有意思的问题&#xff1a;如果用户反复问“钢琴键到底是谁发明的”&#xff0c;直接调通用大模型的接口&#xff0c;回答经常飘忽不定&#xff0c;有时能把克里斯托弗里说成巴赫时代的管风琴师&#xff0c;有时又答非所…

作者头像 李华
网站建设 2026/9/4 13:59:37

双DGX实测:DeepSeek V4 Flash如何凭性价比屠榜?

之前在帮一个 AI 应用选型时&#xff0c;最头疼的不是模型能力不够&#xff0c;而是“贵”和“慢”这两个问题一起出现。后来看到一位海外人工智能博士晒出他在双 DGX 平台上的大模型对比测试&#xff0c;结果很有意思&#xff1a;DeepSeek 的 Flash 系列模型在价格上几乎是“屠…

作者头像 李华