简介:面向计算机专业毕业生及微信小程序、SSM 技术学习者的一份毕业设计论文文档,围绕微信小程序预约挂号系统展开,可用于选题参考、论文结构模仿和系统设计思路梳理。文档以管理员、医生、用户三类角色为主线,涵盖个人中心、用户与医生管理、科室信息、排班信息、预约与取消预约、调班申请及系统管理等功能模块,并说明 Java SSM 后台、MySQL 数据库与微信开发者工具的实现方案。压缩包内为 1 个 doc 文件,大小约 2.13MB,便于直接查阅和二次整理。已有 404 人学习,读者可从中了解需求分析、可行性分析、功能设计与数据库设计等章节的写作范式,也能对照角色权限和业务流程完善同类预约挂号项目,对准备毕业设计开题、论文撰写与系统功能规划具有一定参考价值。
1. 从一次排班撞车说起:这套基于微信小程序的预约挂号系统要解决什么
去年旁听某高校的课程设计答辩,有个组的演示卡在中途:两位医生被排进同一个诊室的同一时段,前台只能挨个打电话改约。这个尴尬恰好点出基于微信小程序的预约挂号系统真正要解决的东西——号源不能被重复占用,排班、预约、取消、调班得走同一条数据链路,而不是四张各管一段的表。整套方案分三类角色:管理员统管科室、医生、排班与全量预约;医生管自己的号源、预约记录和调班申请;用户在微信小程序里看医生、读公告、下单预约、取消预约。它适合正在做毕设或课设、需要一份能跑起来又能写进论文的完整方案的人,也适合要交付预约类后台的开发者拿来改。
2. 需求与角色建模:三类角色、九个功能域的边界怎么切
动手写代码之前,先把「谁能看见哪个菜单、谁能调哪个接口」定死,比先画页面重要得多。预约类系统出问题,八成不是页面丑,而是权限边界糊了:医生的接口被用户 token 调通、管理员的删除操作没有任何二次确认,这类漏洞在答辩现场被问到会很难圆。这一章把角色、状态和可行性三块拆开讲,都是论文里必须出现、也最容易被写成空话的部分。
2.1 权限矩阵:把「谁在哪个页面做什么」先钉死
原料里给了三份功能清单,直接抄进论文没问题,但要落地成代码,得先做一张权限矩阵。矩阵的行是功能域,列是角色,格子里写「增删改查」四类动作里有哪几项。做完这张表,后台菜单的动态渲染规则和后端拦截器的白名单基本就自动出来了,不用再拍脑袋。
| 功能域 | 管理员 | 医生 | 小程序用户 |
|---|---|---|---|
| 用户管理 | 增删改查 | 无 | 仅本人资料 |
| 医生管理 | 增删改查、审核 | 改本人资料 | 只读列表与详情 |
| 科室管理 | 增删改查 | 只读 | 只读 |
| 排班信息 | 增删改查 | 只读本人 | 只读可约号源 |
| 预约信息 | 查、改状态 | 查本人、改状态 | 增、查本人 |
| 取消预约 | 查、审 | 查本人 | 增、查本人 |
| 调班申请 | 审批 | 增、查本人 | 无 |
| 通知公告 | 增删改查 | 只读 | 只读 |
| 系统管理 | 增删改查 | 无 | 无 |
这张表的用法有两层。前端按角色返回不同的菜单树,登录成功后由后端拼好塞进响应体,小程序端和浏览器端共用一套角色码;后端在每个 Controller 方法上加自定义注解,比如@RequireRole("ADMIN"),由拦截器读取当前会话里的角色码比对。要注意的是,前端隐藏菜单只是体验优化,绝不能当作权限控制——真正的关卡一定在服务端,任何一次越权请求都应该在进入 Service 之前被拦掉。
提示:医生注册后默认状态是「待审核」,未审核的账号可以登录但调不通排班和预约接口,审核动作只留给管理员。这条规则在论文的「系统安全性」一节里是可以展开写的一段。
2.2 业务状态机:预约单从下单到完成要经过几条边
预约单不是一张静态表,它有生命周期。按原料里的功能描述,一条预约至少存在四种状态:待就诊、已完成、已取消、爽约。状态之间的迁移必须有明确的前置条件,否则会出现「已完成的单子还能取消」「取消两次号源加了两回」这类典型 bug。
迁移规则我一般这么定:用户下单成功,状态置为待就诊,同时排班表的剩余号源减一;医生在后台把某条记录标记为已完成,只允许从待就诊迁过去;用户或医生发起取消,只允许待就诊状态,取消后号源加一,并往取消预约表里写一条留痕记录;到了就诊日结束仍未处理的,由定时任务扫成爽约。这里的关键设计是把「取消预约」单独做一张表,而不是只在预约表上改个状态字段。原料里管理员和医生的菜单都单列了「取消预约管理」,说明取消记录本身是要被查询、统计和追责的,只改状态会让历史取消原因、操作人、操作时间全部丢掉,后期统计爽约率时会很难受。
状态迁移的条件最好集中在一个枚举或常量类里,Service 层用if判断当前状态是否允许目标动作。散落在各个方法里写魔法数字(0、1、2)是血泪经验换来的教训:等你加到第五种状态,全项目搜status == 1能搜出四十处,改一处漏三处。
2.3 可行性分析与性能指标:论文里最容易写空的两节
可行性分析在三章里篇幅不大,却是评审必看的部分,因为它是「这个题目值不值得做」的论证。技术可行性不要写成「框架很成熟所以可行」,要落到具体依赖:SSM 三件套都是开源且有大量文档的,小程序端用微信开发者工具调试,数据库用 MySQL 本地实例,整套环境不需要付费授权,一台普通配置的笔记本就能跑通。经济可行性同理,重点在零授权成本和低硬件门槛,而不是空喊「成本低」。操作可行性则要结合角色说:管理员在浏览器后台操作表格,医生用同一套后台但只看到自己的数据,用户在小程序里三步内完成预约——把「三步」写清楚,比写十句「界面友好」有说服力。
性能指标建议直接量化,别怕数字小。接口平均响应控制在 500 毫秒以内,号源列表分页每页 10 条,排班查询走(doctor_id, work_date)联合索引,单表数据量按一个科室日均 200 条预约估算。安全性部分写三条就够:密码加盐哈希存储而不是明文、MyBatis 一律用#{}占位符防注入、越权请求由拦截器统一拦截。数据完整性写约束层面的东西:排班表的剩余号源不允许为负、预约单号唯一、医生账号唯一、外键关联的科室被删除时要么禁止要么级联,写清楚选了哪种。
3. 技术选型与总体架构:SSM 后端与小程序端的职责分界
选型这一章如果只罗列框架简介,读者翻过去就忘了。真正有价值的是把「为什么这么分」讲清楚:哪些逻辑放在服务端,哪些放在小程序端,两端的数据契约长什么样。这套方案的边界其实很清晰——小程序只负责展示和收集输入,所有涉及号源数量、状态变更、权限判断的计算全部落在 Java 后端,因为小程序端的代码是可以被反编译和篡改的,任何「前端判断库存」的设计都撑不住。
3.1 为什么这套方案用 SSM 而不是更时髦的框架
SSM 是 Spring + Spring MVC + MyBatis 的组合,属于 Java Web 里非常成熟的一套。放到毕业设计场景下,它有三个实际好处:XML 配置显式可见,论文里能贴出配置片段说明各层怎么装配;分层边界清楚,Controller 接请求、Service 写业务、DAO 管 SQL,写论文的「系统详细实现」一章时结构天然对齐;社区资料多,遇到报错能搜到同款。
和 Spring Boot 比,SSM 的启动配置要多写不少,但换来的是「每一块配置都看得见」,这对需要讲清架构的文档反而是优势。实际取舍可以这样定:如果项目要长期维护、团队协作,选 Boot;如果是课设或毕设,需要把装配过程写进论文、需要答辩时能解释每个 bean 从哪来,SSM 更合适。前端这边,小程序用微信开发者工具直接开发完全可以;如果还想兼顾 H5 或多端预览,可以引入 uni-app,用 Vue 语法写一套代码编译到多个平台,代价是多一层构建配置,调试链路变长。
数据库选 MySQL 的理由也很实在:本地安装即用,免费,SQL 语法标准,导出建表脚本方便附在论文附录里。要注意字符集统一用utf8mb4,否则用户昵称里带表情符号会直接插入失败,这个坑几乎每届都有人踩。
3.2 分层结构与接口约定:Controller、Service、DAO 各管什么
后端按经典三层加实体层组织,包结构建议这样切:controller只做参数接收、校验和返回封装;service写业务规则,事务注解加在这一层;mapper对应 MyBatis 接口,SQL 写在resources/mapper/*.xml;entity放与表一一对应的实体;common放统一返回体、异常类、拦截器和工具类。分层的判断标准很简单:Controller 里不允许出现if (库存 <= 0)这种业务判断,Mapper 里不允许写业务分支,一旦越界,后面加需求就会到处改。
前后端的数据契约建议统一成一个返回体,code、msg、data三个字段,code用 200 表示成功,401 表示未登录,403 表示越权,500 表示业务异常。小程序端只判断code,不解析 HTTP 状态码,处理逻辑会干净很多。分页参数统一用pageNum和pageSize,返回total加list,所有列表接口都按这个来,前端封装一次就能复用。
<!-- spring-mybatis.xml 关键片段:把数据源、Mapper 扫描、别名包装配起来 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- SQL 映射文件统一放在 classpath:mapper 下,按表名命名 --> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <!-- 实体别名包,XML 里可直接写类名而不写全限定名 --> <property name="typeAliasesPackage" value="com.example.hospital.entity"/> </bean> <!-- Mapper 接口扫描,省去逐个配置 dao 实现 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.hospital.mapper"/> </bean>这段配置解决的是「接口和 XML 怎么对上」的问题。mapperLocations决定 MyBatis 去哪里找 SQL 文件,路径写错的表现是启动不报错、调用时报Invalid bound statement,排查时先看编译后的target/classes/mapper目录里有没有对应的 XML。typeAliasesPackage只是简化书写,不影响功能。MapperScannerConfigurer把接口批量注册成 bean,这样 Service 里@Autowired注入 Mapper 才不会空指针——很多同学手写 DAO 实现类,写到最后发现重复劳动,其实就是漏了这一段。
3.3 数据库设计:从实体关系到核心表字段
实体识别按原料的模块清单走,能抽出七个核心实体:用户、医生、科室、排班、预约、调班申请、公告。医生和科室是一对多,排班同时关联医生和科室,预约关联用户、医生和排班,调班申请关联医生和排班。把关系画清楚之后,表结构基本就是翻译工作。
| 表名 | 作用 | 关键字段 | 索引与约束 |
|---|---|---|---|
user | 小程序用户 | id, openid, nickname, phone, status, create_time | uk_openid唯一 |
doctor | 医生档案 | id, account, password, salt, name, dept_id, title, intro, audit_status | uk_account唯一,idx_dept |
department | 科室 | id, name, sort_no, status | uk_name唯一 |
schedule | 排班与号源 | id, doctor_id, dept_id, work_date, time_slot, source_total, source_left, status | uk_doctor_slot(doctor_id, work_date, time_slot) |
appointment | 预约单 | id, order_no, user_id, doctor_id, schedule_id, status, create_time | uk_order_no唯一,idx_user,idx_schedule |
cancel_record | 取消留痕 | id, appointment_id, operator_type, reason, create_time | idx_appointment |
shift_apply | 调班申请 | id, doctor_id, schedule_id, target_date, target_slot, reason, audit_status, audit_remark | idx_doctor_status |
notice | 通知公告 | id, title, content, publish_time, status | — |
几个字段值得单独说。schedule的source_total和source_left一定要分开存,只存剩余量的话,后台想改放号上限就没法算已经约出去多少。time_slot用 tinyint 表示时段,1 是上午、2 是下午,如果需要精确到半小时,就改成start_time和end_time两个 time 字段,索引结构不用动。doctor表里password和salt分开,是为了让加盐哈希的实现更直观,答辩时也容易讲。uk_doctor_slot这个唯一索引是整个系统的兜底——即使应用层判断漏了,数据库也不允许同一医生同一时段出现两条排班。
3.4 登录鉴权:后台账号体系与小程序身份怎么并存
这套系统有两套身份来源:管理员和医生用账号密码登录浏览器后台,用户在小程序里用微信身份登录。二者不要混在一张表里,否则后面加字段会互相污染。我的做法是后台账号统一放doctor和admin两张表,小程序用户放user表,登录成功后后端签发一个 token(可以就用 UUID 存 Redis 或存会话表),前端每次请求带上,拦截器解析出用户 ID 和角色码塞进当前上下文。
小程序端调用wx.login拿到临时凭证,传给后端换openid,首次登录自动注册一条user记录,昵称和头像由用户授权后补齐。这里不需要把微信身份和后台账号打通,两边各自独立鉴权,业务上只通过user_id关联预约记录。会话过期时间后台设 2 小时,小程序端设 7 天,过期后统一返回 401,前端跳登录页重新走一遍流程。
4. 关键模块实现:号源生成、预约下单、取消回收与调班审批
这一章是整份方案里唯一不能含糊的部分,代码贴出来就要能跑。四个模块按业务顺序串:先有排班才有号源,有号源才能下单,下单之后才谈取消和调班。每个模块给出核心方法,再说明参数怎么改、失败时看哪里,照着改基本不会翻车。
4.1 排班与号源生成:一张排班表怎么变成可预约的时段
排班由管理员在后台录入,选择医生、日期、时段和放号数量。核心逻辑不复杂,但要挡住重复排班,否则同一个医生同一时段会生成两条记录,用户在小程序里会看到两个一模一样的号源。
@Service public class ScheduleServiceImpl implements ScheduleService { @Autowired private ScheduleMapper scheduleMapper; /** * 生成排班号源 * @param schedule doctorId、workDate、timeSlot、sourceTotal 为必填 * 初始剩余号源 = 放号总数,status=1 表示可预约 */ @Override public int generate(Schedule schedule) { if (schedule.getSourceTotal() == null || schedule.getSourceTotal() <= 0) { throw new BizException("放号数量必须大于 0"); } // 同一医生同一天同一时段只允许一条排班 int exist = scheduleMapper.countByDoctorAndSlot( schedule.getDoctorId(), schedule.getWorkDate(), schedule.getTimeSlot()); if (exist > 0) { throw new BizException("该时段已存在排班,请先调整原排班"); } schedule.setSourceLeft(schedule.getSourceTotal()); schedule.setStatus(1); return scheduleMapper.insert(schedule); } }这段的关键在两处。第一处是参数校验提前,sourceTotal为空或非正数直接抛业务异常,不要让脏数据进库,否则后面扣减号源时会算出负数。第二处是先查重再插入,countByDoctorAndSlot对应的 SQL 是select count(1) from schedule where doctor_id = #{doctorId} and work_date = #{workDate} and time_slot = #{timeSlot},配合表上的唯一索引形成双保险。真实并发下「先查后插」仍有窗口期,唯一索引会抛DuplicateKeyException,捕获后转成友好提示即可。
如果要做批量排班(比如一次排一周),把上面的方法包一层循环,逐条调用并收集失败项返回给管理员,不要用一条大 SQL 批量插,出错时定位不到是哪一天冲突。
4.2 预约下单:号源扣减与重复提交怎么处理
下单是整个系统最容易出问题的地方,两个风险:号源被超卖,同一个用户重复提交生成多条预约。前者用条件更新解决,后者用唯一约束加业务校验解决。
-- 号源扣减:把判断和更新合成一条原子语句 UPDATE schedule SET source_left = source_left - 1 WHERE id = #{scheduleId} AND source_left > 0 AND status = 1;这条 SQL 的返回值是受影响行数,Java 里拿到 0 就说明号源已抢完或排班已停用,直接抛异常让事务回滚。它比「先 select 查剩余量再 update」可靠得多,因为后者在两条语句之间可能被另一个请求插进来,典型的超卖来源。
@Transactional(rollbackFor = Exception.class) public String book(Long userId, Long scheduleId) { Schedule sc = scheduleMapper.selectById(scheduleId); if (sc == null) { throw new BizException("号源不存在"); } // 同一用户对同一排班只允许一条待就诊记录 int dup = appointmentMapper.countActive(userId, scheduleId); if (dup > 0) { throw new BizException("您已预约该时段,请勿重复提交"); } int rows = scheduleMapper.reduceSource(scheduleId); if (rows == 0) { throw new BizException("该时段号源已约满"); } Appointment ap = new Appointment(); ap.setOrderNo(IdWorker.next()); // 订单号唯一,作为幂等依据 ap.setUserId(userId); ap.setDoctorId(sc.getDoctorId()); ap.setScheduleId(scheduleId); ap.setStatus(0); // 0=待就诊 appointmentMapper.insert(ap); return ap.getOrderNo(); }@Transactional加在这里是必须的,扣号源和写预约单要么都成功要么都回滚。countActive查的是该用户在该排班下状态为待就诊的记录数,防止用户连点两下提交按钮。orderNo用雪花算法或时间戳加随机数生成,appointment表上对它建唯一索引,即使前面两道判断都被绕过,数据库仍会拦下重复订单。
如果部署在多台机器上,countActive的查重窗口仍然存在,这时候补一个 Redis 锁(key 用book:userId:scheduleId,过期时间 5 秒)就够用了。单机部署没必要上锁,反而增加复杂度。
4.3 取消预约与号源回收:别让号源凭空多出来
取消看着简单,其实是号源一致性的第二个关口。用户取消一次号源加一,如果接口被重复调用,号源就会越加越多,最后排班表上剩余量超过放号总数。
@Transactional(rollbackFor = Exception.class) public void cancel(Long appointmentId, Long operatorId, String operatorType, String reason) { Appointment ap = appointmentMapper.selectById(appointmentId); if (ap == null) { throw new BizException("预约记录不存在"); } // 只有待就诊状态允许取消,已完成/已取消/爽约一律拒绝 if (ap.getStatus() != 0) { throw new BizException("当前状态不支持取消"); } int rows = appointmentMapper.updateStatus(appointmentId, 2); if (rows == 0) { throw new BizException("取消失败,请刷新后重试"); } scheduleMapper.releaseSource(ap.getScheduleId()); // 剩余号源 +1 cancelRecordMapper.insert(appointmentId, operatorType, reason); }updateStatus的 SQL 要带上状态条件:update appointment set status = 2 where id = #{id} and status = 0。这样即使用户在两个设备上同时点取消,只有第一次能更新成功,第二次受影响行数为 0 直接抛出提示,号源不会被重复回收。operatorType区分是用户取消、医生代取消还是管理员强制取消,这个字段在后续统计取消率时非常好用,也是论文里可以单独写一段的设计点。
号源回收之后还要考虑一种边界:如果排班当天已经过期,剩余号源加一其实没有意义。稳妥的做法是在回收前判断work_date是否早于今天,过期就只改状态不加号源,避免历史排班表上出现「过期了还剩号」的怪数据。
4.4 调班申请与审批:医生端的闭环怎么收尾
调班是这套系统里唯一需要两级确认的流程:医生发起,管理员审批。设计上一个容易忽略的点是审批通过之后要做什么——把原排班停用,再按新日期生成一条排班,已经约出去的号源要一并迁移或通知用户取消。偷懒的做法是审批通过只改申请单状态,排班一动不动,那这个功能就只是个记事本。
@Transactional(rollbackFor = Exception.class) public void audit(Long applyId, Integer auditStatus, String remark, Long adminId) { ShiftApply apply = shiftApplyMapper.selectById(applyId); if (apply == null || apply.getAuditStatus() != 0) { throw new BizException("申请不存在或已处理"); } shiftApplyMapper.updateAudit(applyId, auditStatus, remark, adminId); if (auditStatus != 1) { return; // 驳回不改动排班 } // 通过:停用原排班,按目标日期时段生成新排班 scheduleMapper.disable(apply.getScheduleId()); Schedule target = new Schedule(); target.setDoctorId(apply.getDoctorId()); target.setWorkDate(apply.getTargetDate()); target.setTimeSlot(apply.getTargetSlot()); target.setSourceTotal(scheduleMapper.selectById(apply.getScheduleId()).getSourceTotal()); scheduleService.generate(target); }auditStatus用 0 待审批、1 通过、2 驳回三态,updateAudit里同样要带audit_status = 0条件,防止管理员重复点审批按钮把排班改两遍。审批通过后的号源迁移属于增强项:如果原排班已经有用户预约,正确做法是给这些用户发一条小程序订阅消息或者站内通知,让他们改约到新时段,而不是默默把他们的预约挂在一个已停用的排班上。这一块在论文里可以作为「系统不足与改进方向」写,评委会觉得你想过。
4.5 小程序端页面与接口对接:请求封装省掉一半重复代码
小程序端页面按原料的模块划分:首页(公告轮播加科室入口)、医生列表与详情、预约确认、我的(预约记录、取消记录、个人资料)。页面之间共享的是请求逻辑,封装一次比每个页面写一遍wx.request强得多。
// utils/request.js:统一封装域名、token、错误码处理 const BASE_URL = 'https://your-domain/api'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'content-type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(new Error('登录已过期')); } else { reject(new Error(res.data.msg || '请求失败')); } }, fail(err) { reject(new Error(err.errMsg || '网络异常')); } }); }); } module.exports = { request };这段封装做了三件事:拼域名、带 token、按业务码分流。BASE_URL在开发阶段填本机 IP 加端口,真机预览时不能用localhost,必须换成局域网地址或者已配置的合法域名,这是小程序开发最常见的卡点。token从本地存储取,登录成功后写入,退出时清掉。code === 401单独处理成跳登录页,其他错误统一抛给调用方catch,页面里只需要request('/doctor/list', 'GET', { pageNum: 1, pageSize: 10 }).then(...)。
页面渲染上,医生列表用scroll-view加分页加载,每次触底pageNum加一;预约按钮点击后先弹确认框,再调下单接口,成功后用wx.showToast提示并把页面栈退回「我的预约」。这些交互细节写进论文的「界面设计」一节,比贴十张截图有内容。
5. 避坑与常见问题排查:五个最容易翻车的地方
这套系统功能不算多,但把几个点踩全了也够折腾一周。下面这五条是这类项目里出现频率最高的,每条按现象、原因、解决三步走,遇到问题时可以当排查清单用。
5.1 号源超卖:明明只剩一个号,两个人同时下单都成功了
现象是后台看排班剩余量变成了 -1,或者两个用户都收到了预约成功的提示。原因基本可以锁定在扣减写法上:先select查出剩余量在 Java 里判断大于 0,再执行update减一,两条语句之间存在时间窗口,两个请求同时读到 1,都判断通过,都执行了减一。
解决方式是把判断塞进 SQL 的where里,用source_left > 0做条件更新,靠数据库的行锁保证原子性,Java 侧只判断受影响行数。改完之后还要检查一次事务边界:扣减和插入预约单必须在同一个事务里,@Transactional加在 Service 方法上而不是 Controller 上,否则异常被吞掉号源照样会扣。
5.2 重复提交:用户手抖点两下,多出一条预约
现象是同一用户对同一排班出现两条待就诊记录。原因有两个层面,前端按钮没有防抖,点击后没有立即置灰;后端没有做幂等校验,来一次请求就执行一次。
解决要两头都做。前端在按钮点击后立刻setData置成禁用状态,请求结束再恢复;后端在 Service 里查该用户该排班的有效预约条数,大于 0 直接抛业务异常,同时给order_no建唯一索引兜底。如果接口可能被重放攻击,再在请求头里带一个前端生成的随机串,服务端做短期去重。
5.3 越权访问:改一下 URL 里的 id 就能看到别人的预约
现象是用户在小程序里把预约详情接口的id换一个数字,返回了别人的就诊信息。原因是接口只校验了「已登录」,没校验「这条数据是不是你的」。
解决是在 Service 层做数据归属判断,查询预约详情时把user_id作为条件一起传进去,查不到就返回空,而不是先查出来再比较。管理员和医生的接口按角色放行,医生的查询条件强制拼上doctor_id = 当前登录医生,不信任前端传的任何身份参数。这类问题在答辩时被问到会很扣分,但改起来只需要在每个查询方法上多加一个条件。
5.4 中文乱码与表情符号插入失败
现象是公告标题里的中文存进库变成问号,或者用户昵称带 emoji 时插入报错。原因通常是数据库、表、连接串三处字符集不一致,或者用了utf8而不是utf8mb4。
解决是把 MySQL 的库和表字符集统一成utf8mb4,排序规则用utf8mb4_general_ci,JDBC 连接串加上useUnicode=true&characterEncoding=utf8。已经建好的表用alter table ... convert to character set utf8mb4改一遍,改完记得重启应用,连接池里的旧连接还带着老字符集。
5.5 时间字段时区错乱:预约日期总是差一天
现象是用户选的明天,存进库变成今天,或者列表按日期筛选时少一条。原因多半是 Java 的Date和 MySQL 的datetime在时区处理上没对齐,加上 JDBC 连接串没指定时区。
解决是统一在连接串里写serverTimezone=Asia/Shanghai,实体类里的日期字段优先用java.time.LocalDate和LocalDateTime,MyBatis 3.4 以上原生支持。日期比较不要用字符串拼接,用date_format或者直接在 SQL 里传LocalDate参数。改完拿边界日期测一遍:今天、明天、月末、跨年,四个用例跑通基本就没问题。
6. 验收与答辩:把任务书、论文、代码和演示串成一条证据链
到了验收环节,很多人的问题不是系统跑不起来,而是讲不出「我做了什么」。评审关心的无非三件事:需求有没有落地、设计有没有依据、结果能不能复现。把这三件事对应到材料上,就是一张验收清单,答辩前照着过一遍,能省掉大半临场慌乱。
| 验收项 | 对应材料 | 检查方式 |
|---|---|---|
| 三类角色权限隔离 | 权限矩阵表、拦截器代码 | 用医生账号访问管理员接口,应返回 403 |
| 号源不超卖 | schedule表条件更新 SQL | 两个浏览器同时下单最后一个号,只成功一个 |
| 取消可追溯 | cancel_record表 | 取消后查表,操作人、原因、时间齐全 |
| 调班闭环 | 申请单状态与排班变更 | 审批通过后原排班停用、新排班生成 |
| 数据完整性 | 建表脚本与唯一索引 | 重复排班、重复订单号插入应失败 |
演示顺序也有讲究。不要一上来就打开后台,先从小程序端走一遍用户路径:登录、看公告、选科室、看医生、下单、在我的里面看到记录、取消。这条链路走完,评委对整个系统就有了直观印象。然后切到后台,用管理员账号展示医生审核、排班录入、调班审批,最后用医生账号登录,展示只能看到自己的数据。这个顺序的好处是每一步都在回答「权限边界在哪」,而不是让评委自己去猜。
答辩提问里出现频率最高的是「如果两个人同时预约最后一个号会怎样」。这个问题不要背答案,直接把 5.1 那段的 SQL 写在黑板上讲条件更新,讲完评委基本就认可了。第二个高频问题是「你的表和表之间怎么关联的」,把实体关系图拿出来,指着schedule说它是号源中枢,预约和调班都挂在它上面,比逐个念字段名清晰得多。
从那以后我每次带这类项目,都会在验收前强制走一遍「两个浏览器抢最后一个号」和「换个 id 查别人数据」这两个用例,跑通了才敢让人上台。这两个动作花不了十分钟,却能挡住答辩现场最难解释的尴尬。需要整套任务书、开题报告、论文和源程序的,按文末方式取用就行,结构和上面这套是对得上的,拿去改改就能用。希望帮到你。
本文还有配套的精品资源,点击获取