每年到了三月份,就会有一大批同学开始焦虑毕设选题。你要是问我计算机专业做毕设选什么题最稳,我的答案一直很明确:业务清晰、角色分明、能展示完整开发链路的题目就是好题目。健康医疗服务平台恰恰属于这种“标准答案”级别的选题——用户端有预约挂号、健康档案、在线问诊;医生端有排班、接诊、病历记录;管理端有科室管理、医生审核、数据统计,一个系统下来要前后端联动、权限划分、状态流转、数据建模全部练到。而且它不只是个“练习项目”,预约挂号本身就是真实世界的成熟业务,答辩时老师能听懂,你也能讲明白,不会出现“做了一个系统但说不清它解决什么问题”的尴尬。
这篇文章就把我当时做这个毕设的核心思路、表设计、接口实现、踩坑记录全部盘一遍,给正在做同类题目的同学一个可直接参考的完整版本。源码部分的目录结构我也放到后面一起说,如果你正好选了这个题,照着这个思路走,能少走很多弯路。
1. 健康医疗服务平台:这个毕设题到底在做什么
1.1 选题逻辑:为什么医疗类系统是毕设常青树
很多人选毕设题目时有个误区,总觉得题目越“高级”越好,什么基于微服务的商城、基于大数据的推荐系统。实际上,毕设评分的核心不是技术名词多不多,而是你有没有把一个完整的业务闭环做出来。健康医疗服务平台的优势,恰恰在于它的业务模型足够复杂又不至于失控。
业务闭环体现在哪?在一个真实的医院预约挂号场景里,一次完整的就医行为包含这些环节:用户注册登录、选择科室、查看医生排班、预约时间挂号、线下就诊后医生填写病历、开处方、用户查看健康档案和处方记录。每一个环节都涉及数据表的增删改查,而表与表之间又存在明确的关联,这就自然形成了一个有深度、有层次的系统,而不是简单堆功能的CRUD轮子。
再说角色设计。一个像样的毕设系统,必须体现出不同角色之间数据的隔离与协作。健康医疗服务平台天然有三角色:患者、医生、管理员。患者管自己的预约单和档案,医生管接诊表和病历,管理员管基础数据和账号审核。三角色共享同一套底层数据,但站在不同视角看到的操作界面和允许执行的接口是完全不同的,这就是权限设计最好的演练场。
1.2 系统边界:毕设版医疗系统与真实HIS系统的差异
做毕设之前,先把边界划清楚,这是最重要的一步。很多同学一开始就把系统设计成“什么都要做”,结果三个月过去了还在做需求分析。你要清楚,医疗行业真正的核心系统叫HIS(Hospital Information System),它包含挂号、收费、药房、检验、住院、手术等等几十个子系统,是医院级的重量级工程。毕设环节做的是“服务窗口”,也就是面向普通用户和医生的轻量级服务平台。
我当时的做法是只保留四个核心域:
- 就医服务域:科室展示、医生排班、预约挂号、取消预约
- 诊疗记录域:病历录入、处方管理、健康档案查询
- 用户中心域:患者信息维护、就诊历史、账号安全管理
- 平台管理域:科室维护、医生开诊管理、资讯发布、数据统计
每个域控制在两三个核心表以内,总表量控制在十张左右。这样既保证了系统像一个“完整产品”,又能保证个人在两个月内完成开发、测试、写论文这三件套。
1.3 项目技术描述与建议运行环境
我做的版本属于典型的 Spring Boot + Vue 前后端分离项目。运行环境方面,后端 JDK 1.8 或 11 都可以,Spring Boot 使用 2.7.x,前端用 Vue 2 + Element UI,数据库 MySQL 5.7 或 8.0。开发工具我用的 IDEA + VSCode + Navicat。
这里多说一句,Spring Boot 3.x 虽然已经发布很久,但毕设阶段你如果对底层原理不够熟悉,不要盲目追新。3.x 版本要求 JDK 17 起步,而且很多第三方 Starter 的兼容性容易出问题,光环境排查就能耗掉你好几天。用 Spring Boot 2.7.x 配合 JDK 8,是当前搭配最稳、教程最多、遇到问题最容易搜到答案的方案。
2. 技术选型复盘:Spring Boot + Vue 的务实取舍
2.1 后端框架决策:单体应用为什么是毕设最优解
我当时选型时其实在“Spring Boot 单体”和“Spring Cloud 微服务”之间犹豫过一阵子,后来还是选了单体,现在回过头看这个决定非常务实。
微服务架构解决的是大型团队协作、独立部署、弹性伸缩的问题,这些在毕设场景里根本不存在。你一个人开发一个中小型系统,强行拆成网关、注册中心、多个微服务,不仅没有收益,反而要为服务间通信、分布式事务、链路追踪这些额外复杂度买单。答辩时老师说“你这个项目拆了三个服务,各自独立部署,数据是怎么保证一致的”,这就是个很难接住的问题。
单体应用配合合理的包结构,完全能表达出“分层清晰”的工程能力。我的后端包结构是这样拆的:
controller:接收前端请求,做参数校验,返回统一结果service:业务逻辑层,负责流程编排与事务控制mapper:数据访问层,基于 MyBatis Plus,复杂查询再手写 XMLentity:实体类,与表结构一一映射config:统一配置类(跨域、拦截器、MyBatis Plus 分页插件等)common:工具类、统一返回体、异常处理器、常量类
这样分层带来的好处非常直观:改前端接口时只动 controller,改业务逻辑时先看 service,查数据问题直接定位 mapper,代码可读性和可维护性都有了。答辩时把这个结构一摆,已经能体现基本的工程素养。
2.2 为什么坚持用 MyBatis Plus 而不用 JPA
ORM 框架的选择,在 Java 毕设里也是经常被问到的问题。Spring Data JPA 和 MyBatis Plus 都能用,但我强烈推荐 MyBatis Plus,原因就一句话:你在学校学的 SQL 知识不会白费,而且遇到连表查询、条件统计这类复杂 SQL,MyBatis Plus 的兜底能力更强。
JPA 的 Hibernate 自动建表、自动 SQL 看起来很美好,但它的“自动”是一把双刃剑。你控制不好 Lazy Loading 触发时机,就会冒出经典的LazyInitializationException;你搞不懂 N+1 查询,同一个列表接口会莫名其妙慢上半拍。而 MyBatis Plus 是增强版的 MyBatis,单表 CRUD 直接用封装好的BaseMapper,连 SQL 都不用写;多表关联自己写 XML,每一句 SQL 都是可控的,出了问题你能直接定位。
举一个实际例子,查询预约列表时,我需要关联查询患者姓名、医生姓名、科室名称,用 MyBatis Plus 的@Select注解写一条 join SQL 就搞定了。三个字段来自三张表,全部一步查出来,性能也比逐条查再组装高效得多。这种能力在 JPA 里也能实现,但调试成本明显更高,对新手不友好。
2.3 前后端分离的接口约定与统一返回体
前后端分离项目里,最忌讳接口各写各的,前端以为返回{code: 0, data: {...}},后端却返回一个裸对象,联调时寸步难行。我从一开始就定义了统一的返回结构:
{ "code": 200, "message": "操作成功", "data": { } }后端对应一个Result泛型类,所有接口的返回值都是它。code是业务状态码,200 表示成功,400 表示参数错误,401 表示未登录或登录过期,500 表示服务器异常。前端 Axios 的响应拦截器统一判断code,不是 200 就直接弹出message,业务代码里基本不用写重复的错误处理逻辑。
这套约定的好处在做完第一个接口之后再体会。假设前端要拿当前登录用户的信息,接口返回Result<UserVO>,里面既有用户昵称、手机号,又有角色编号。前端只需要:
// 业务请求封装 export function getCurrentUser() { return request({ url: '/api/user/current', method: 'get' }) }调用处直接const res = await getCurrentUser(),有数据没数据、成功还是失败,拦截器已经帮你处理好了。接口联调效率提升一个档次。
3. 数据库设计:把业务流程翻译成表结构
3.1 从业务流程到九张核心表的推演过程
数据库设计是任何管理系统开发的灵魂。我当时拿着业务流程图,把每一个“名词”和“动作”都摘出来,最终落到九张核心表上。这个过程不玄乎,就是“名词变表、动作变状态”的翻译工作。
user:用户表,包含患者和医生两类角色,用role字段区分department:科室表,存储科室名称、位置、简介doctor:医生扩展表,存储医生的职称、擅长领域、所属科室,通过user_id关联用户表schedule:排班表,表示某个医生在某个日期某个时段出诊,同时记录剩余号源appointment:预约挂号表,记录用户预约了哪个医生的哪个时段的号medical_record:病历表,记录医生对一次就诊的诊疗意见prescription:处方表,记录医生开出的药品信息news:健康资讯表,管理员发布的健康科普文章announcement:公告表,平台通知类信息
这个表结构是从“一次就诊流程”这条主线推出来的。用户要先有医生才能挂号,医生要有排班才能被预约,预约完成后用户才有机会被书写病历,病历往往对应处方。每张表的存在都有业务前置条件支撑,答辩时老师问“为什么需要这张表”,你能顺着流程讲清楚,这就是深度思考的体现。
3.2 表字段设计的几个关键决策
每一张表我都遵循了几个共同的约定。首先是主键策略,统一用bigint自增或者雪花算法生成的 ID,避免使用字符串主键,因为字符串主键在索引和关联查询上的性能都比整数主键差。其次是创建时间create_time和更新时间update_time,这是审计字段,所有表都保留,后期查问题非常有帮助。
三个典型表的字段设计放出来给你们做个参考:
user 表(用户表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| username | varchar(50) | 登录账号 |
| password | varchar(100) | 加密密码(BCrypt) |
| real_name | varchar(50) | 真实姓名 |
| phone | varchar(20) | 手机号 |
| id_card | varchar(18) | 身份证号(脱敏展示) |
| role | tinyint | 1-患者 2-医生 3-管理员 |
| avatar | varchar(255) | 头像地址 |
| status | tinyint | 0-禁用 1-正常 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
schedule 表(排班表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| doctor_id | bigint | 医生ID |
| department_id | bigint | 科室ID |
| schedule_date | date | 出诊日期 |
| time_slot | varchar(20) | 时段,如 08:00-08:30 |
| total_count | int | 总号源数 |
| remain_count | int | 剩余号源数 |
| status | tinyint | 0-未开始 1-进行中 2-已结束 3-已取消 |
| create_time | datetime | 创建时间 |
appointment 表(预约挂号表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 预约单号 |
| user_id | bigint | 患者ID |
| doctor_id | bigint | 医生ID |
| schedule_id | bigint | 排班ID |
| appointment_date | date | 预约日期 |
| time_slot | varchar(20) | 预约时段 |
| status | tinyint | 0-待就诊 1-已完成 2-已取消 3-已过期 |
| visit_time | datetime | 实际就诊时间 |
| create_time | datetime | 创建时间 |
这里有个特别实用的设计:schedule表里既存了doctor_id又存了department_id,表面上存在“冗余”,但这是为了查询性能特意做的。排班页面上列表展示需要根据科室过滤,如果每次都要从doctor表关联查出科室,再存排班,查询路径更长;直接冗余科室 ID,列表接口一条 SQL 就能完成过滤。冗余不可怕,可怕的是一堆没必要的冗余,有业务依据的冗余叫“空间换时间”。
3.3 创建表的 SQL 片段:索引和字符集细节
字符集我曾经吃过亏。如果建库时用默认的latin1,存中文会乱码。在 MySQL 8 里,我统一建库用utf8mb4字符集,排序规则用utf8mb4_general_ci,它可以完整支持中文和 Emoji 字符。建表语句示例:
CREATE TABLE `appointment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` varchar(32) NOT NULL COMMENT '预约单号', `user_id` bigint(20) NOT NULL COMMENT '患者ID', `doctor_id` bigint(20) NOT NULL COMMENT '医生ID', `schedule_id` bigint(20) NOT NULL COMMENT '排班ID', `appointment_date` date NOT NULL COMMENT '预约日期', `time_slot` varchar(20) NOT NULL COMMENT '预约时段', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-待就诊 1-已完成 2-已取消 3-已过期', `visit_time` datetime DEFAULT NULL COMMENT '实际就诊时间', `create_time` datetime NOT NULL COMMENT '创建时间', `update_time` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_doctor_id` (`doctor_id`), KEY `idx_schedule_id` (`schedule_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约挂号表';关于索引设计,我的原则是:查询频繁的字段建索引,但不盲目加索引。user_id、doctor_id、schedule_id这三个外键字段是高频查询条件,必须加索引。状态字段status我也建了普通索引,因为后台管理页面会频繁按状态筛选预约单。不过像create_time这种只做排序不一定做筛选的字段,我就没有单独建索引,避免占用额外空间和拖慢写入速度。
3.4 MyBatis Plus 代码生成器的效率优势
手动写实体类和 XML 映射很枯燥,而且容易因为字段名打错字导致运行时报错。我是用 MyBatis Plus 的代码生成器一次性生成的实体类、Mapper 接口和 XML 文件。它读取数据库表结构,自动把下划线字段名转成驼峰属性名,减少了一大批机械劳动。
生成之后只做两件事:第一,在实体类上根据字段含义补充注释;第二,把逻辑删除字段和自动填充字段配置好。MyBatis Plus 支持自动填充create_time和update_time,配置一个MetaObjectHandler实现类,插入和更新时自动赋值,业务代码里完全不用关心这两个字段。
4. 核心功能模块实现与原理复盘
4.1 预约挂号的流程设计与状态流转
预约挂号是整个系统里业务逻辑最重的模块,状态流转也是答辩时的高频问题。我先定义清楚状态的含义:
- 待就诊:用户预约成功,尚未看诊
- 已完成:用户已就诊,医生填完病历
- 已取消:用户自行取消
- 已过期:排班日期已过,用户未取消也未就诊
用户的预约操作是一次完整的前后端数据交互。前端先进入科室列表,选择科室后请求该科室下的医生列表,点击某医生后查看未来七天的排班计划,每个时段有总号源数和剩余号数,用户选择剩余号数大于 0 的时段提交预约。
后端的核心代码如下(简化版):
@Transactional(rollbackFor = Exception.class) public AppointmentResult createAppointment(Long userId, Long scheduleId) { Schedule schedule = scheduleMapper.selectById(scheduleId); if (schedule == null) { throw new BusinessException("排班不存在"); } // 判断排班日期是否已过期 if (schedule.getScheduleDate().isBefore(LocalDate.now())) { throw new BusinessException("该排班已过期"); } // 检查号源 if (schedule.getRemainCount() <= 0) { throw new BusinessException("该时段号源已满"); } // 防止重复预约 Long count = appointmentMapper.selectCount( new LambdaQueryWrapper<Appointment>() .eq(Appointment::getUserId, userId) .eq(Appointment::getScheduleId, scheduleId) .ne(Appointment::getStatus, 2)); if (count > 0) { throw new BusinessException("您已预约该时段,请勿重复操作"); } // 扣减号源 int rows = scheduleMapper.deductRemainCount(scheduleId); if (rows == 0) { throw new BusinessException("该时段号源已被抢完,请更换时段"); } // 创建预约单 Appointment appointment = new Appointment(); appointment.setOrderNo(generateOrderNo()); appointment.setUserId(userId); // ... 设置其他字段 appointmentMapper.insert(appointment); return new AppointmentResult(appointment.getOrderNo()); }注意这段代码里@Transactional注解加在了类级别或方法级别,确保扣减号源和创建预约单是原子操作。一旦第二步创建预约单失败,第一步的扣减会回滚,不会出现“号源少了但预约记录没有建立”的数据不一致。
4.2 扣减号源为什么用 UPDATE 条件判断而不是先查再改
这是整个预约模块最关键的设计,也是区分“会写代码”和“会思考并发”的分水岭。很多最初版本的代码都是先select查询剩余号源,判断大于 0,再执行update扣减。这在单用户测试下没问题,一旦多个用户同时请求同一个时段的号源,就存在超卖风险。
原因是“先查再改”不是一个原子操作。两个请求同时读到剩余号源为 1,都判断大于 0,然后都执行扣减,最终剩余号源变成 -1,而且产生了两个预约记录。正确的做法是把判断条件放进 UPDATE 语句里:
UPDATE schedule SET remain_count = remain_count - 1 WHERE id = #{scheduleId} AND remain_count > 0这行 SQL 的巧妙之处在于,MySQL 的行锁会保证同一时刻只有一个事务能更新这一行。当多个请求同时执行这条 UPDATE 时,第一个请求把remain_count从 1 改成 0,第二个请求再执行时因为remain_count > 0条件不成立,影响行数为 0。此时业务层判断rows == 0,直接提示“号源已被抢完”。这个方案性能高、实现简单,也没有额外的锁机制,是单体应用里处理并发扣减最经典的方式。答辩时把这一条讲清楚,剩余号源的并发问题就能变成你的加分项,而不是扣分项。
4.3 JWT 认证与角色权限控制的完整链路
用户登录之后,前端所有请求都需要携带一个令牌,后端通过令牌识别“当前用户是谁”。比较成熟的方案是 JWT,一段自包含的加密字符串,里面有用户 ID、角色、过期时间等信息。用户请求时在请求头加Authorization: Bearer <token>,后端拦截器解析 token,取用户信息。
我的实现拆成三步:
第一步,登录接口用BCryptPasswordEncoder验证密码。数据库中存储的密码是 BCrypt 加密后的密文,即使数据库泄露,攻击者也无法直接得到明文密码。
第二步,登录成功后生成 JWT:
String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();第三步,写一个JwtInterceptor,实现 Spring MVC 的HandlerInterceptor,在preHandle里解析 token。解析成功后把用户信息存入ThreadLocal中的一个UserContext类,后续的 controller 层和 service 层直接UserContext.getUserId()获取当前用户,不用再层层传递参数。请求结束后在afterCompletion里调用UserContext.clear(),防止线程复用导致的数据串号。
角色控制我用的是拦截器加注解的方式。自己定义一个@RequireRole注解,标注在 controller 方法上,拦截器检查当前用户的角色是否匹配。比如管理员删除用户接口:
@RequireRole(RoleConstant.ADMIN) @DeleteMapping("/admin/user/{id}") public Result<Void> deleteUser(@PathVariable Long id) { userService.deleteUser(id); return Result.success(); }如果当前登录用户不是管理员,拦截器直接返回 401 或者 403,前端弹出“无权限访问”。这种方式比在每个方法里手写 if 判断角色要优雅得多,也更好扩展。
4.4 在线问诊模块:用最轻的方案做完整的功能闭环
在线问诊是锦上添花的模块,但也是很多同学容易“翻车”的地方。一提到实时聊天,很多人第一反应是 WebSocket。WebSocket 本身不复杂,但后续要考虑在线状态、心跳检测、消息持久化、历史记录分页,这些实现起来都不轻松,工作量往往会超出预期。
我的建议是毕设阶段用“半实时”方案:问诊消息存数据库,前端每 5 秒轮询一次新消息。如果时间充裕,再在轮询基础上叠加 STOMP WebSocket 做瞬时通知。这样做的理由很简单:轮询方案能稳定完成需求,接口简单、逻辑直观、数据可靠;WebSocket 虽然实时性好,但它的消息顺序、重连机制、心跳保活任何一个环节出问题,都会成为答辩时被追问的隐患。
问诊消息表的核心字段:id、questioner_id、doctor_id、content、sender_role、create_time。这里 sender_role 区分“患者发的”还是“医生发的”,前端根据它决定气泡展示在左边还是右边。列表查询时按会话两方关联查找,按create_time倒序,前端再正序展示,就能拼出完整对话流。
5. 实战踩坑记录:从报错到解决的完整排查链路
5.1 并发预约导致号源超卖:从测试数据看问题本质
我最早自测时用 Postman 写了两个并发请求同时预约同一个排班的最后一个号,结果两条请求都返回成功,预约记录出现了两条,剩余号源变成了 -1。刚看到这个结果时人都懵了,还以为是数据库没提交,反复刷新确认了几次才接受是并发问题。
排查过程是这样的。我先在预约服务里加了日志,打印每个请求读取到的remain_count值,发现两个请求读到同一个值“1”,分别都走完了判断逻辑,然后各自执行了 UPDATE。问题定位很清楚,就是典型的检查后操作竞态条件。随后我改成上面讲的带条件 UPDATE 语句,WHERE remain_count > 0,再次用并发工具跑 50 个请求,最终只成功了 1 个,其余全部提示号源已满,修复有效。
这个坑值得专门记下来,因为很多教程给的 CRUD 例子里不会考虑并发场景,但现实业务天然存在并发。答辩时老师可能就顺着问“你这系统能抗住多少人同时挂号”,你把这个优化方案讲出来,比背一百个八股文都管用。
5.2 LocalDateTime 序列化成数组:前后端联调遇到的神秘数据
开发预约接口时,我遇到了一个非常诡异的现象:后端返回给前端的appointmentDate字段,在浏览器 Network 面板里看到的不是"2025-03-18"这种字符串,而是一长串数组[2025, 3, 18, 0, 0]。最初我还以为是前端解析问题,后来用 Postman 直接请求接口,返回的 JSON 就是这个数组形态。
原因是 Jackson 在序列化 Java 8 的LocalDateTime时,默认没有启用jsr310模块的格式化支持,把时间对象拆成了具有year、month、day等属性的结构。解决办法有两个:一是在日期字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,二是在全局配置类里注册一个Jackson2ObjectMapperBuilderCustomizer,对全局的所有时间字段统一做格式化。我用了第二种,一次性解决所有实体类的时间字段问题,一劳永逸。
5.3 跨域配置和拦截器顺序:为什么预检请求会报 401
前端项目跑在 8080 端口,后端跑在 9090 端口,浏览器发请求时必然触发跨域问题。解决方式是在 Spring Boot 里配置CorsFilter或者实现WebMvcConfigurer的addCorsMappings。但我当时配置好了之后,前端登录还是报 401,看浏览器的 Network 才发现问题出在预检请求上。
浏览器在发送真正的 POST/PUT/DELETE 请求之前,会先发一个OPTIONS请求做预检。我的拦截器逻辑判断了所有请求都必须带 token,预检请求没有 token,就被拦截器拦下来返回 401,导致后续真实请求也没有发出。解决办法是在拦截器里放行OPTIONS请求:
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; }这个坑在前后端分离项目里非常常见,几乎每届毕设都会有人踩一遍。一旦你记住“预检请求不带业务参数”这个前提,以后遇到类似的 401 问题就能秒定位。
5.4 身份证号的隐私展示:脱敏思路
还有一个细节容易被忽略,用户在“我的健康档案”里查看身份证号时,如果直接返回完整号码,不仅显得产品设计不成熟,答辩时如果被问到隐私保护也会卡壳。我的做法是在后端做脱敏,返回给前端的数据统一把中间八位替换成星号,例如110***********1234。列表和详情都只展示脱敏后的数据,只有用户本人编辑时才允许修改完整号码。
6. 源码组织、演示数据与答辩准备经验
6.1 源码目录结构:让老师一眼看懂你的工程能力
一个清爽的目录结构会直接影响老师的第一印象。后端源码我按 maven 标准结构组织,同时把业务模块独立出来:
health-server ├── src/main/java/com/health │ ├── common │ │ ├── Result.java │ │ ├── BusinessException.java │ │ └── GlobalExceptionHandler.java │ ├── config │ │ ├── CorsConfig.java │ │ ├── MybatisPlusConfig.java │ │ └── JwtInterceptorConfig.java │ ├── controller │ │ ├── UserController.java │ │ ├── DoctorController.java │ │ ├── ScheduleController.java │ │ └── AppointmentController.java │ ├── service │ ├── mapper │ └── entity └── src/main/resources ├── application.yml ├── mapper/*.xml └── sql/init.sql前端部分我用 Vue CLI 创建,src/api目录统一放接口调用,src/views按角色分目录:patient/、doctor/、admin/、common/(登录与首页)。这样的好处是,老师想查看某个功能的代码,可以沿着“角色 → 页面 → 接口 → 后端 controller → service”这条链一路追下去,非常顺畅。
源码中的sql/init.sql很关键,里面除了建表语句,还要附带一部分演示数据。我当时塞了一个管理员账号、两个医生账号、三个患者账号、未来七天的排班数据,以及若干病历和处方记录。这直接决定了你答辩演示时能不能快速进入状态,而不是现场敲一条 INSERT 让老师干等。
6.2 答辩演示的标准路径设计
演示环节的节奏也很重要。我的彩排步骤大概是这样的:
- 注册一个患者账号,登录进入患者端首页
- 浏览健康资讯,查看公告
- 进入预约挂号,选择科室,选择医生,选择明天的排班,成功预约
- 切换医生账号登录,查看排班,进入“待就诊列表”,给刚才的患者填写病历和处方
- 切回患者账号,查看“我的档案”,能看到病历和处方记录
- 切换管理员账号,查看预约统计图表,展示用户列表和号源管理
- 结束前点开某个数据库表,展示数据变化过程
这套路径把三个角色、四个核心模块全串起来了。每一步操作间隔控制在半分钟以内,总时长十分钟左右。演示完,老师会顺着你的操作问一些实现细节,这时候前面讲的号源并发、JWT 权限、状态流转这些点就派上用场了。
6.3 以一条经验收尾:健康医疗类项目要守住数据安全底线
最后分享一点个人体会。开发健康医疗这类项目时,技术难点反而不是最值得焦虑的,真正要绷紧弦的是数据安全和隐私边界。我的代码里所有涉及患者信息查询的接口都做了登录校验,所有列表页面的隐私字段都脱敏处理,这是基本素养,也是一个有工程意识的开发者该有的自觉。你不需要在毕设里去实现多复杂的加密算法,但“该做防护的地方都做上了”这件事本身就能在答辩中体现你的完整性思考。
如果你现在正在做这个项目,或者准备用这个题目开题,希望你至少能从这篇文章里拿走三样东西:第一,号源扣减不要在代码里先查再改,条件 UPDATE 是基本解法;第二,前后端分离项目一定要提前处理跨域预检请求;第三,表结构设计顺着业务流程推,每一张表都要能讲出存在意义。把这三个核心点吃透,剩下的就是按部就班写代码、跑测试、写论文了。