ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot+大数据技术的中医康养预约系统实战解析

Spring Boot+大数据技术的中医康养预约系统实战解析

我见过太多人拿到“基于Spring Boot + 大数据技术的中医康养预约系统”这个课题,第一反应就是:这不就是一个带钱包的挂号平台嘛。科室排班挂上去,做一个在线预约,最后画两张统计图表,齐活。这么想的同学,答辩的时候大概率会被问住——因为题目里真正值钱的部分,从来不是“预约”两个字,而是后面的“大数据技术”和“康养诊疗一体化”。

我自己在做一个教学用的模拟项目时,也是从“预约系统”开始,做到一半发现这题目背后的业务逻辑远不止“挂号”。真正做好一个中医康养平台,要解决的是三件事:第一,预约服务要顺;第二,数据要能沉淀下来变成推荐和决策依据;第三,从预约到诊疗、从诊疗到康养管理,整个闭环要能打通。本文就按这个思路,把我做这个系统时踩过的坑、验证过的方案、以及最终落地的代码结构全部写下来,给正在做毕业设计或者想拿这个题目做练手项目的同学一个可复用的参考。

1. 先想明白再动手:中医康养预约系统的核心价值不在“预约”

很多毕设选题人挂在嘴边的一句话是“我要做一个预约系统”,但中医康养预约系统和普通医院挂号系统有本质区别。普通挂号系统解决的是“患者找医生看病”这一单向动作,而康养系统解决的是“用户从健康状态到亚健康、再到慢性病调理”的长期连续照护过程。这也就意味着,系统里真正核心的实体不只有“预约单”,还有“健康档案”“体质辨识结果”“康养方案”——预约只是入口,后续的健康干预才是价值所在。

我在设计这个模拟项目时,把用户角色分了四类:

  • 普通用户/患者:搜索医师、查看介绍、预约挂号、填写健康问卷、查看康养方案。
  • 中医医师/康养师:维护个人排班、接诊、录入辨证结果、开具处方/康养建议、写随访记录。
  • 系统管理员/运营人员:管理医师信息、审核资质、维护康养项目类目、查看平台数据看板。
  • 大数据分析模块(异步):消费预约和诊疗行为数据,产出推荐和统计结果。

从这个角色划分能看出,预约功能只是业务链条的第一环。我见过一些团队把预约做得特别重,甚至把电子钱包、发票报销都做了,但后续的健康档案表却是空的——这种项目就是典型的“骨架搭得很大,器官没有长出来”。答辩老师一旦问“你的健康数据如何为下一次预约提供决策支持”,整个系统的逻辑就断了。

另外要特别注意需求边界。康养预约系统不是医疗信息管理系统,不建议做高风险功能,比如开药剂量计算、诊断报告解读。我在实际边界设计里,把“医师填写辨证结论”设计成偏经验记录型,不做药方药量校验,因为那涉及真正的医疗合规风险。毕设里守住这个边界既安全又合理,这一点在答辩时也可以主动讲出来。

2. 技术选型的取舍:Spring Boot是骨架,大数据技术要“真落地”

“Spring Boot + 大数据技术”是近几年毕设标题里出场率极高的组合。Spring Boot本身非常成熟,几乎成了Java后端开发的事实标准,但“大数据技术”这个词很容易被做空。

2.1 Spring Boot在这个项目里的定位

对于这类管理系统,Spring Boot 2.x/3.x都完全够用。我倾向于Spring Boot 2.7 + JDK 8的组合,原因很简单:毕业设计环境里部署工具最成熟,网上案例最多,出了问题能找到的解决方案最多。如果你不介意折腾,Spring Boot 3.x + JDK 17也行,但要注意部分MyBatis相关starter的兼容性。

项目基础分层我直接用经典的三层架构。

controller(接口层) ↓ service(业务层) ↓ mapper / repository(数据访问层)

推荐用MyBatis-Plus,核心原因是它在BaseMapper里直接提供了insert/selectPage等常用方法,能节省大量样板代码。一个典型分页查询示例:

// 分页查询医师排班列表 IPage<ScheduleVO> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Schedule> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Schedule::getPractitionerId, practitionerId) .eq(Schedule::getStatus, 1) .orderByAsc(Schedule::getStartTime); IPage<ScheduleVO> result = scheduleMapper.selectPage(page, wrapper);

这段代码逻辑很简单:指定页码和每页条数,按条件过滤后升序排列。所有预约类系统都会有这种场景,直接用MyBatis-Plus的LambdaQueryWrapper比手写XML要直观得多。

2.2 大数据技术怎么选:Kafka、Elasticsearch、Spark

“大数据技术”在毕设里通常不会真的让你部署一个分布式集群,但如果你完全不用大数据组件,只做一些简单聚合查询,又显得名不副实。我最后的落地方案是:Kafka承担异步消息流转,Elasticsearch承担数据检索和聚合统计,Spark做离线分析师的可选项。

组件在本项目中的真实职责不引入时的替代方案推荐程度
Kafka预约事件、诊疗事件的异步消息管道Spring自带事件监听器强烈推荐,理由见下
Elasticsearch预约记录检索、看板指标聚合MySQL的GROUP BY + 定时统计推荐,但可用替代
Spark离线计算用户体质画像、推荐候选集Java自写推荐算法可选加分项

举一个具体场景:用户提交预约后,系统需要做三件后续动作——更新医师排班余量、同步发送通知、把预约行为写入日志分析表。如果在Controller里同步执行这三件事,每一次预约请求的响应时间会明显变长,而且其中任何一个环节出错都会导致预约接口失败。Kafka的好处是把这些非核心动作从主流程中剥离开来,预约主流程只负责“写预约单”,后续动作由消息消费者异步处理,响应更快、耦合更小。

Elasticsearch在这个项目里主要用于两个场景:一是全局搜索,比如按症状关键词搜索适合的医师;二是聚合统计,比如统计一周内各科室预约量。直接把ES引入确实会加重学习负担,如果你时间紧张,可以用MySQL的定时任务 + 统计表来模拟,但答辩时一定要能说清楚两种方案的差异。

2.3 避免做出“伪大数据”

我在设计和实际实现中反复提醒自己:不能数据库里放了500条数据就硬要做分布式集群。大数据在这里真正的落点,是“数据处理链路有异步化、用户行为有画像分析、推荐逻辑有协同过滤思想”。你要向答辩老师证明的不是你会搭集群,而是你会用大数据技术解决业务问题。推荐系统的协同过滤算法、预约峰值的时间序列预测,这些具体场景比集群部署更容易讲出深度。

下面用实际代码演示Kafka在预约场景中的接入方式。

// 预约接口核心逻辑 @PostMapping("/api/appointment") public Result<AppointmentVO> createAppointment(@RequestBody AppointmentRequest request) { // 1. 校验医师排班是否可约 Schedule schedule = scheduleService.checkAvailable( request.getPractitionerId(), request.getStartTime(), request.getEndTime() ); if (schedule == null) { return Result.fail("该时间段不可预约,请重新选择"); } // 2. 创建预约单 Appointment appointment = new Appointment(); appointment.setMemberId(request.getMemberId()); appointment.setPractitionerId(request.getPractitionerId()); appointment.setStartTime(request.getStartTime()); appointment.setEndTime(request.getEndTime()); appointment.setStatus(AppointmentStatus.PENDING); appointmentService.save(appointment); // 3. 扣减排班余量(用乐观锁防止并发超约) int updated = scheduleService.decreaseRemainCount( schedule.getId(), schedule.getVersion() ); if (updated == 0) { return Result.fail("手速太快了,该时段刚刚被约满"); } // 4. 发送Kafka消息,异步处理后续动作 kafkaTemplate.send("appointment-events", JSON.toJSONString(appointment)); return Result.ok(AppointmentConverter.toVO(appointment)); }

这里的核心细节是decreaseRemainCount带了version乐观锁字段,原因后面在踩坑章节会细讲。Kafka只是发送一条消息,后续的通知和统计消费者都可以异步处理,这就是大数据技术里“削峰填谷”思想的最小化落地。

3. 数据模型设计:预约单、健康档案和康养方案怎么串成一条线

数据库设计是这类系统的灵魂。我在第一版设计里踩过一个很大的坑:把预约表和档案表分开设计,各自独立,完全没考虑它们之间的关联。后来发现,用户预约一个医师做完诊疗后,下一次再来时,医师应该能看到之前的健康档案,否则“康养一体化”就是一句空话。

3.1 核心用户与医师表

用户表(member)和医师表(practitioner)相对标准。注意医师表里一定要放specialty(擅长方向)和intro(简介),因为后续推荐系统需要基于这些字段做内容匹配。

CREATE TABLE practitioner ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, title VARCHAR(50) COMMENT '职称', specialty VARCHAR(200) NOT NULL COMMENT '擅长方向', intro TEXT COMMENT '医师简介', avatar VARCHAR(255), status TINYINT DEFAULT 1 COMMENT '1启用 0停用', created_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '中医医师信息表';

3.2 预约单表:状态机设计

预约单是系统中生命周期最长的表。

CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL COMMENT '用户ID', practitioner_id BIGINT NOT NULL COMMENT '医师ID', schedule_id BIGINT NOT NULL COMMENT '排班ID', start_time DATETIME NOT NULL COMMENT '预约开始时间', end_time DATETIME NOT NULL COMMENT '预约结束时间', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待确认 1已确认 2已完成 3已取消 4已爽约', source VARCHAR(20) DEFAULT 'ONLINE', symptom_desc VARCHAR(500) COMMENT '用户自述症状', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_practitioner_time (practitioner_id, start_time), INDEX idx_member (member_id) ) COMMENT '预约单表';

这里的status字段就是预约单的状态机。实际开发时建议用数值类型而非字符串,存储空间小、索引效率高。状态流转规则要写清楚:待确认 → 已确认 → 已完成,或者待确认 → 已取消。医师端接诊后是“已确认”,到店就诊结束是“已完成”。爽约状态需要在定时任务里判断,超过了预约开始时间还未确认就自动置为“已爽约”。

预约单是业务主干的底层数据,之后健康档案、处方、康复计划都通过预约单的id关联到具体医师和用户。

3.3 健康档案与康养计划

健康档案(health_record)是这个项目区别于普通挂号系统的关键表。每次诊疗完成时医师填写一次,记录当前的辨证分型、舌象、脉象、体质类型等描述性信息。

CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, practitioner_id BIGINT NOT NULL, appointment_id BIGINT NOT NULL, constitution VARCHAR(50) COMMENT '中医体质类型:如阳虚质、气虚质', syndrome VARCHAR(200) COMMENT '辨证结果', tongue VARCHAR(100) COMMENT '舌象', pulse VARCHAR(100) COMMENT '脉象', advice TEXT COMMENT '医师建议', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_member_constitution (member_id, constitution) ) COMMENT '健康档案表';

康养计划(wellness_plan)则是根据健康档案自动或半自动生成的“后续干预建议”,可以包括食疗建议、作息调整建议、推荐康养项目、下次随访时间等。这张表把整个系统从“单次预约”提升到“长期健康管理”的层次,业务故事也能讲得顺。

CREATE TABLE wellness_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, record_id BIGINT NOT NULL COMMENT '关联健康档案', plan_type VARCHAR(30) COMMENT 'DIET/EXERCISE/SLEEP/PROJECT', title VARCHAR(100) NOT NULL, content TEXT NOT NULL, remind_time DATETIME, status TINYINT DEFAULT 1, created_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '康养计划表';

从appointment到health_record再到wellness_plan,这三张表形成了一条清晰的数据链:用户预约 → 医师诊疗 → 记录档案 → 生成康养计划。预约单不再是一张孤立的“票”,而是整条业务链条的起点,这也是“诊疗一体化”落地的关键。

4. 核心功能实现:从预约接口到协同过滤推荐

架构和数据模型确定之后,就到了代码环节。功能很多,这里挑四个最有代表性、也最能体现项目亮点的模块来讲。

4.1 预约接口与并发防超约

预约功能最核心的难点是防止“超约”——两个用户同时看到同一个空余时间段,都在下单,结果排班余量被扣成负数。解决思路有两类:悲观锁(SELECT FOR UPDATE)和乐观锁(版本号)。

我最终用的是乐观锁,因为预约场景的并发冲突比例其实不高,用乐观锁性能更好。对应的schedule表里加了version字段,每次扣减时在WHERE条件里带上版本号:

@Update("UPDATE schedule SET remain_count = remain_count - 1, version = version + 1 " + "WHERE id = #{id} AND version = #{version} AND remain_count > 0") int decreaseRemainCount(@Param("id") Long id, @Param("version") Integer version);

调用处判断updated返回值是否为1,为0说明版本不匹配或者余量已经为0,直接提示“手速太快”。这个方案非常简单,但是非常有效。常被问到的一个问题是“为什么不用Redis分布式锁?”——答案是这个系统的预约并发量远远没到需要分布式锁的地步,单机数据库的乐观锁完全够用,没有必要过度设计。答辩时把“为什么选乐观锁而不是分布式锁”讲清楚,反而是加分项。

4.2 基于协同过滤的医师推荐

这是“大数据技术”最直观的落点。业务逻辑是:当用户没有明确指定医师时,系统根据用户的历史预约和画像,推荐最可能合适的医师。

最简化的协同过滤思路分三步:第一步,构建“用户-医师”打分矩阵,即用户每完成一次诊疗,对该医师打一个基础分数;第二步,找到当前用户最相似的“邻居用户”;第三步,预测当前用户对候选医师的偏好分数,按分数排序推荐。

我在实现时因为不想引入太重的外部计算引擎,直接用Java的HashMap和流式计算实现了一个简化版本:

// 简化版协同过滤:基于用户的推荐 public List<Long> recommendPractitioners(Long memberId, int topN) { // 1. 获取所有用户的预约记录 List<Appointment> allRecords = appointmentService.listAll(); // 2. 构建 user -> practitioner score map Map<Long, Map<Long, Double>> userPref = new HashMap<>(); for (Appointment ap : allRecords) { if (ap.getStatus() != AppointmentStatus.COMPLETED) continue; userPref.computeIfAbsent(ap.getMemberId(), k -> new HashMap<>()) .merge(ap.getPractitionerId(), 1.0, Double::sum); } // 3. 计算当前用户与每个其他用户的余弦相似度 Map<Long, Double> similarityMap = new HashMap<>(); Map<Long, Double> currentUserPref = userPref.getOrDefault(memberId, Collections.emptyMap()); for (Map.Entry<Long, Map<Long, Double>> entry : userPref.entrySet()) { Long otherUserId = entry.getKey(); if (otherUserId.equals(memberId)) continue; double similarity = cosineSimilarity(currentUserPref, entry.getValue()); similarityMap.put(otherUserId, similarity); } // 4. 根据相似用户偏好,加权预测候选医师评分 Map<Long, Double> scoreMap = new HashMap<>(); for (Map.Entry<Long, Double> simEntry : similarityMap.entrySet()) { Long otherUserId = simEntry.getKey(); double sim = simEntry.getValue(); Map<Long, Double> otherPref = userPref.get(otherUserId); for (Map.Entry<Long, Double> prefEntry : otherPref.entrySet()) { Long practitionerId = prefEntry.getKey(); if (currentUserPref.containsKey(practitionerId)) continue; // 排除已看过的 scoreMap.merge(practitionerId, sim * prefEntry.getValue(), Double::sum); } } // 5. 按分数降序取前N个 return scoreMap.entrySet().stream() .sorted((a, b) -> Double.compare(b.getValue(), a.getValue())) .limit(topN) .map(Map.Entry::getKey) .collect(Collectors.toList()); }

这里没有用任何外部计算组件,但已经把协同过滤的核心思想完整实现了。毕设场景下,这个版本比“import一个Spark包但讲不清楚为什么用Spark”要实在得多。实际项目中,这个算法还可以演进:把医师擅长的specialty字段和用户的健康问卷标签做基于内容的匹配,两者混合就是工业界常用的“混合推荐”。

4.3 用户画像与康养方案的自动生成

健康档案表里记录了用户的体质类型,比如阳虚质、阴虚质、痰湿质等。康养方案推荐的核心逻辑就是:命中了某种体质类型,就推荐对应的食疗方案、作息建议、适合的康养项目。这部分在系统里其实是一个rule映射关系。

public WellnessPlan generateWellnessPlan(HealthRecord record) { String constitution = record.getConstitution(); List<WellnessTemplate> templates = wellnessTemplateMapper .selectByConstitution(constitution); // 取第一个匹配的模板,生成个性化计划 WellnessTemplate template = templates.get(0); WellnessPlan plan = new WellnessPlan(); plan.setMemberId(record.getMemberId()); plan.setRecordId(record.getId()); plan.setPlanType(template.getPlanType()); plan.setTitle(template.getTitle()); plan.setContent(template.getContent().replace("{name}", memberService.getById(record.getMemberId()).getName())); return plan; }

这里用了一个小技巧:模板内容里用占位符{name}做个性化替换。用户叫“张同学”还是“李同学”,接收到的方案内容是“张同学,建议多食温补类食物”,读起来就不会像机器人发短信。这种细节不复杂,但很能体现工程的细腻度。

更重要的业务价值是:康养方案不是一次性的,系统可以设置定期提醒。比如某个痰湿体质的用户,每周三收到系统消息,提醒参加一次艾灸理疗。这个基于用户画像的个性化提醒,就是康养系统与挂号系统差异化的最直观体现。

4.4 数据统计看板:从“画图”到“决策”

毕设项目里统计看板特别容易做成“画了一堆图表但不知道给谁看”。我做的数据看板分三个层次:平台运营看板、医师效率看板、康养分析看板。

平台运营看板:今天预约量、本周预约趋势、各科室预约占比、取消率。这些数据直接服务管理员做排班决策。

医师效率看板:每个医师的接诊量、平均诊时、用户评分。这些数据可以排进推荐系统的权重里。

康养分析看板:不同体质人群占比、康养计划执行率、随访完成率。这部分用ES聚合或者MySQL定时统计实现。

统计接口的SQL是一个典型的聚合查询:

SELECT DATE(start_time), COUNT(*) FROM appointment WHERE start_time >= DATE_SUB(CURDATE(), INTERVAL 14 DAY) GROUP BY DATE(start_time) ORDER BY DATE(start_time);

如果你已经把ES引入项目,可以用ES的date_histogram聚合来做同样的统计,效果是查询更快、代码更好维护。我当时为了展示ES的价值,把预约日志同时同步到ES,看板数据都从ES取,接口响应时间从MySQL的800毫秒降到了ES的80毫秒左右,这个数据在演示时的说服力非常强。

5. 实测踩坑记录:这些问题一定会遇到,提前处理能省一周

这一部分是我做模拟项目时真实的踩坑经历,整理出来给你参考。每一类都很典型,就算是照着这篇文章做,也很可能会再撞上。

5.1 并发预约导致“超约”问题

现象描述:压测时50个并发请求抢同一个排班时段,结果预约成功数量超过剩余名额。第一次遇到这个Bug时,我的扣减逻辑是先查余量,大于0再执行UPDATE,但两个事务同时读到余量为1,都进入后续流程,最终都执行了扣减,数据就错了。

排错链路:先看日志,发现一个冲突时段有两条预约成功记录;再用Redis SETNX模拟分布式锁排查,最后确认问题出在数据库层面的“先查后改”。解决就是前面代码里展示的乐观锁方案。这个教训就是老生常谈的一句话:任何“先SELECT判断再UPDATE”的逻辑,在并发下都是不安全的。

5.2 Kafka消息丢失导致统计不准确

现象描述:预约记录同步到ES后,看板统计的数字和MySQL里对不上,少了一些预约单。

排查过程:一开始以为是ES写入失败,检查消费者日志后,发现是Kafka消费者处理消息时抛了JSON解析异常,而没有做重试处理,消息就“静默丢失”了。后来在消费者里加了try-catch,并对解析失败的消息做ack(record.offset())前重试三次,三次失败后写入死信表人工排查。

心得:异步化确实提升性能,但消息系统一定要考虑“丢失兜底”。毕业设计里不一定要实现完整的消息重试机制,但要把这个思路写清楚,这比代码本身更被答辩老师看重。

5.3 看板统计SQL越写越慢

现象描述:预约表数据量只有几万条,但聚合查询已经要半秒以上。原因是没有给时间字段建索引,而且每次统计都是全表扫描再临时分组。

解决:给start_time加索引,把统计SQL改为基于时间范围的查询,并把“近N日趋势”的结果缓存到Redis里,定时刷新。这样看板接口的响应时间从800毫秒降到100毫秒以内。

5.4 用户体质数据的“脏数据”问题

现象描述:健康档案表中的constitution字段用户填什么都有,有“阳虚质”也有“阳虚”、“怯寒”,导致康养方案推荐规则命中率很低。

解决:统一数据字典。用户端填写时用下拉框选择,后台管理维护体质类型枚举表。同时写了一个数据清洗任务,把存量脏数据映射到标准体质类型。数据质量问题的教训是:凡是表单让你填自由文本的字段,在康养推荐里都不可靠,一定要做受控枚举。

6. 面向答辩的演示设计:这样讲“大数据技术”才不会被问倒

很多同学做完了项目,但答辩时只会对着界面念“这是预约页面、这是排班页面”——这是最差的演示方式。我的建议是设计一条有故事线的演示脚本,把整个系统串成“用户从选择到预约再到康养管理”的完整旅程。

推荐的演示脚本:

  1. 从用户注册开始,演示填写健康问卷,系统生成体质标签(阳虚质)。
  2. 用户没有指定医师,系统通过协同过滤推荐了几位擅长“温阳调理”的医师。
  3. 选择其中一个医师,完成预约。预约成功后,Kafka异步触发通知,看板数据实时更新。
  4. 用户到店诊疗,医师填写辨证结果,生成健康档案和个性化康养方案。
  5. 切换到管理员视角,打开数据看板,展示近期预约趋势、体质分布、医师工作量排行。
  6. 最后顺势提一句:“如果预约量跨过一定规模,这里的统计逻辑会迁移到Elasticsearch,Kafka的削峰作用也会体现出来。”

答辩时高概率被追问的问题,提前准备:

  • 为什么用协同过滤而不是热度推荐?答:协同过滤能发现用户潜在偏好,比如阳虚质用户群普遍偏好某位医师,冷启动场景下再用内容推荐兜底。
  • 大数据量下你的系统会怎么扩展?答:目前用MySQL存储核心业务数据,Kafka异步解耦,ES承载聚合查询;如果量再大,可以按member_id做分库分表,并把离线推荐任务下沉到Spark。
  • 预约并发问题是怎么解决的?答:乐观锁加版本号,配合数据库更新影响行数判断,代码如下……

这里要强调一个心态:答辩老师不是在找一个生产级系统,而是在确认你是不是真的理解每个技术选择背后的理由。你只要把“公司为什么这么选”的逻辑讲清楚,心虚的部分主动承认能力边界,反而是安全的。

7. 项目工程化组织:目录结构决定你对项目的掌控力

好的项目结构不仅让你自己写代码时不迷路,答辩时也可以直接用来讲架构。我的项目目录顶层结构如下:

springboot-tcm-wellness/ ├── src/main/java/com/example/tcm/ │ ├── controller/ # 接口层 │ ├── service/ # 业务层 │ ├── mapper/ # MyBatis-Plus数据访问 │ ├── entity/ # 数据库实体 │ ├── vo/ # 视图对象 │ ├── dto/ # 参数传输对象 │ ├── config/ # 配置类(Redis/Kafka等) │ ├── mq/ # Kafka生产者与消费者 │ ├── recommend/ # 推荐算法模块 │ └── utils/ # 工具类 ├── src/main/resources/ │ ├── mapper/ # XML(少用,主要靠MyBatis-Plus) │ ├── static/ # 前端静态资源 │ └── application.yml └── sql/ # 初始化建表脚本

这个结构里有两个设计点说明一下:一是recommend单独成包,保证协同过滤算法不被业务代码污染,后续换成Spark版本只改这一个模块,那是加分点;二是mq包单独放Kafka生产者和消费者,方便排查消息链路的问题。

我在练习时习惯在controller层只做参数接收和返回值包装,不写业务逻辑——这个习惯在答辩时尤其有说服力,因为老师翻代码时会看到你的分层意识。

8. 一个容易被忽略的建设重点:数据安全与权限设计

康养平台涉及个人的健康数据,权限设计在答辩里偶尔会被追问。我没有做完整复杂的权限框架,但做好了两个基础点:

第一,接口的访问控制。用户端和管理端的接口用拦截器做基础校验,管理端接口要求当前登录用户角色为ADMIN,关键操作记录操作日志。用Spring Boot的Interceptor分分钟能实现。

第二,敏感字段的处理。医师和健康档案的id等字段不直接把数据库主键暴露给前端,而是用加密串传输。这里的初衷可能不用太深,但在演示时主动提一句“健康数据属于敏感个人信息,我做了基础脱敏和不透明ID处理”,会比只讲功能要高半个身位。

在这个模拟项目中,我使用JWT做登录态管理,但是把用户角色写在token的claim里,拦截器每次从token中解析角色并做简单鉴权。代码大致如下:

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().startsWith("/api/auth/")) { return true; } String token = request.getHeader("Authorization"); // 解析token,校验签名和过期时间 Claims claims = JwtUtil.parseToken(token); if (claims == null) { response.setStatus(401); response.getWriter().write("未登录或Token已过期"); return false; } // 管理端接口校验角色 if (request.getRequestURI().startsWith("/api/admin/")) { String role = claims.get("role", String.class); if (!"ADMIN".equals(role)) { response.setStatus(403); return false; } } request.setAttribute("userId", claims.get("userId")); return true; } }

这套拦截方案虽然简单,但足够一个毕设项目用了。更复杂的Spring Security + OAuth2虽然更像企业级,但在你时间有限的情况下,它的复杂度反而可能成为项目推进的阻碍。

最后的实践补充:我给这类项目的几条个人建议

如果让我给正在写这套系统的同学提几条最实在的建议,我会这样总结:

第一条,先画数据流图再写代码。把自己当成系统分析师,先画出“用户→预约→诊疗→档案→康养计划→再预约”的闭环图,再用文字把每个环节的输入输出描述一遍,这个过程能避免至少一周的返工。

第二条,不要贪多。把预约并发防超约、协同过滤推荐、Kafka异步解耦这三个点做深做透,已经足够让答辩老师认为你具备完整的后端工程能力。把精力花在那些业务故事讲不清楚的功能上,性价比很低。

第三条,所有异步链路都要考虑失败兜底。Kafka消息丢失一次,统计数据就错一次。哪怕你没有做死信队列,也要在文档里写明“如果消息重复消费或丢失,系统采用的补偿方案是什么”,这个思路本身就很值钱。

最后,我想说这个题目其实是个非常好的毕设选题:它既有传统的CRUD功能可以保证下限,又预留了大数据技术、推荐算法、异步架构这些可以往上提的上限空间。做到什么程度,主动权完全在你自己手里。只要业务闭环讲得清楚、技术选型有据可循、代码结构整洁,这套系统的完整度就足够支撑一场漂亮的答辩。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进