最近刚好在整理一套前后端分离的社区医疗服务系统,技术栈是 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,还带完整文档。这套系统不是那种只做登录注册的Demo,而是把社区卫生服务中心的日常业务流程都串起来了:挂号预约、医生排班、健康档案、电子病历、处方开药、药品库存、科室管理、公告发布,该有的模块基本都有。我把它完整跑起来看了内部实现,又把文档从头翻了一遍,下面从设计思路、表结构、后端实现、前端联调、部署排障到二次开发体验,一条线讲清楚。如果你正准备找一套Java Web开源系统做学习参考或者二次开发,这篇内容应该能帮你省不少时间。
1. 项目整体定位与需求拆解
1.1 社区医疗服务系统到底要管什么
社区医疗服务系统,核心使用场景是社区卫生服务中心、乡镇卫生院这类基层医疗机构,但也完全可以扩展成中小型诊所管理平台。与大型医院信息系统的区别在于:社区医疗更强调“建档-预约-随访”这条主线,业务流程复杂度适中,不需要像大医院那样处理医保接口、住院医嘱、手术排班这些重逻辑。
这套系统里的核心角色有三类:居民(患者)、医生、管理员。部分系统还会拆出药房管理员和护士角色。
主要业务模块可以拆成下面几个:
- 用户与权限:居民注册登录,管理员维护账号,医生账号由后台创建并关联科室和职称。
- 医生信息与排班:维护医生所属科室、职业资质、擅长领域,并按时间段生成排班计划。
- 预约挂号:居民基于科室、医生、排班日期进行预约,预约后进入待就诊状态,医生可以接诊并开始就诊流程。
- 健康档案与病历:居民维护基础健康信息、既往病史、过敏史;医生在就诊时新增病历、诊断结果。
- 处方与药品管理:医生开处方,药房基于处方进行发药操作,同时扣减药品库存。
- 公告与留言:管理员发布门诊停诊、节假日安排等公告,居民可以留言咨询。
- 统计报表:从挂号量、科室收入、药品消耗等维度做基础统计,方便管理人员做经营分析。
这些模块放在一起,就形成一个闭环:居民预约后去社区就诊,医生记录病历与处方,药房发药,后台沉淀数据再生成统计报表。整个流程是典型的医疗信息化起步场景,非常适合用来学习企业级项目如何做业务建模和数据建模。
1.2 技术栈选型为什么偏偏是这一套
先谈 SpringBoot2 而不是 SpringBoot3。SpringBoot2 生态非常成熟,资料多,稳定性强,而且很多企业存量项目仍是2.x版本。项目文档里给的也是2.7左右的版本,大量中间件、第三方框架兼容都不用操心。SpringBoot3 换了JavaEE框架基础,底层改为Jakarta EE 9+,如果只做快速开发,2.x反而更省心。
MyBatis-Plus 选得好。做管理信息系统,大量操作是单表CRUD,MyBatis-Plus 让这类操作连SQL都不用写。内置的分页插件、逻辑删除、自动填充、条件构造器,恰好覆盖了业务系统90%的日常需求。相比 JPA 那种全自动方案,MyBatis-Plus 保留了SQL可控性,在复杂统计查询上更灵活。
MySQL8.0 在系统里不是摆设。8.0 的窗口函数让统计报表SQL写起来非常优雅,比如按月计算挂号量连续变化,可以直接用 LAG 函数;JSON 字段以后做体检报告扩展、患者扩展属性也很方便。更重要的是 8.0 默认字符集是 utf8mb4,中文存储没有坑。
Vue3 那边选择组合式API + Element Plus 是当前主流做法。组合式API让业务逻辑抽取更自然,比如把“预约挂号”的倒计时、表单校验、提交状态全部收拢到一个自定义Hook里。整个系统的前端部分结构清晰,路由守卫、状态管理、请求封装都有,不是拿几个页面拼凑的静态模板。
建议:如果你是刚接触这类系统,别急着替换技术栈。先用原版跑通全流程,再逐步把某一块重构成你喜欢的方式,比如把缓存放进去、把定时任务加上,这样踩坑成本最低。
2. 数据库设计与核心表结构
2.1 概念模型与核心关系
数据模型是理解整个系统最快的方式。核心实体包括:系统用户、医生信息、科室、排班记录、预约挂号单、病历、处方、药品、药品分类、公告、留言反馈。实体之间的关键关系如下。
- 用户与医生是1对1关联:用户表共享账号登录信息,医生表补充职称、坐诊时间、简介等职业属性。
- 科室与医生是1对多:一个科室下有多个医生,医生必须挂在某个科室下。
- 医生与排班是1对多:一个医生可以在多个时间段出诊,排班表负责记录具体日期和上午/下午时段。
- 排班与预约是1对多:一个排班对应多个居民预约,但关键约束是同一个居民在一个排班日期内只能有一条有效预约。
- 预约与病历是1对1:一次有效预约最终会对应一次就诊记录,病历表通过预约单号关联。
- 病历与处方是1对多:一次就诊可能开出多个处方。
- 处方与药品是多对多:处方明细行内记录药品ID和数量金额。
这套关系本质上是围绕“一次就诊”展开的,设计得非常务实。
2.2 关键表字段设计与约定
我把最核心的表整理了几张,字段设计能直接看出项目的规范程度。
| 表名 | 用途 | 关键字段 | 备注 |
|---|---|---|---|
| sys_user | 账号表 | id, username, password, real_name, phone, role, status | role区分居民/管理员/医生/药房;password使用加密存储 |
| doctor_info | 医生档案 | id, user_id, dept_id, title, specialty, introduction, avatar | 关联系统用户与科室 |
| dept_info | 科室 | id, dept_name, intro | 基础数据表 |
| doctor_schedule | 排班 | id, doctor_id, schedule_date, period, total_quota, remain_quota, status | period存枚举值:上午/下午;remain_quota实时扣减 |
| registration | 预约挂号单 | id, schedule_id, patient_id, doctor_id, register_time, visit_status | visit_status为 1待就诊 2已就诊 3已取消 4爽约 |
| medical_record | 病历 | id, registration_id, patient_id, doctor_id, diagnosis, advice, create_time | 一次就诊一条主记录 |
| prescription | 处方 | id, medical_record_id, doctor_id, total_amount, create_time | 处方主表 |
| prescription_item | 处方明细 | id, prescription_id, drug_id, drug_name, price, quantity | 冗余drug_name避免联表过多 |
| drug_info | 药品 | id, drug_name, category_id, spec, unit, price, stock, status | 库存字段标记及时扣减 |
| health_file | 居民健康档案 | id, patient_id, blood_type, allergy_history, past_history, family_history | 扩展个人健康信息 |
从字段设计上能看到几条约定:
- 主键统一使用
id,但不是MySQL自增,而是采用MyBatis-Plus默认的雪花ID。这样做的好处是分布式场景下不会冲突,而且数据迁移时不会因为自增ID变动导致外键关系错乱。 - 所有表都带
create_time和update_time,方便排查线上数据问题。 - 状态字段都用
int或tinyint,后端保存固定的枚举值,前端再映射成显示文本,不要直接存“已预约”这种中文,避免以后改名称时大面积更新数据库。 - 关键业务表都设了
status或visit_status,通过状态机控制业务流转,这样写业务逻辑时可以明确判断当前记录可进行哪些下一步操作。
2.3 表设计里的业务约束经验
开发时容易忽略的是预约挂号那句“同一居民同一排班只能预约一次”。单靠程序先查后插会有并发漏洞,正确做法是在数据库层面加联合唯一索引,例如在registration表上建立uk_schedule_patient(schedule_id, patient_id),并让“已取消”的记录走逻辑删除,而不是物理删除。否则取消后同一用户再次预约时,唯一索引仍然占用,就会报错。
MyBatis-Plus的逻辑删除机制在这里和唯一索引冲突,这是很多新手会踩的坑。解决办法通常有三种:
- 唯一索引里带上状态字段,例如
uk_schedule_patient_status(schedule_id, patient_id, status),通过将取消状态置为不同值规避冲突。 - 不做物理删除也不做逻辑删除,专门设计一张“作废记录表”,取消预约时把原记录置为取消,并生成一条新的作废记录供审计。
- 预约表单独保留一个
cancel_flag,取消时更新该字段,唯一索引不包含它。
这套系统直接采用方案1,因为MyBatis-Plus配置逻辑删除后,查询会自动过滤删除标记,业务代码写起来最顺手。这个设计细节虽然小,但直接影响预约模块的正确性。
3. 后端核心模块实现细节
3.1 工程结构与系统配置
后端工程采用标准的分层结构,按包区分功能域而不是纯按技术层区分,这样在业务复杂后更好维护。
com.example.community ├── common // 通用类:Result、异常处理、常量、枚举 ├── config // 配置类:MyBatis-Plus分页、跨域、拦截器注册 ├── controller // 控制层:按模块划分,AuthController等 ├── service // 业务层:接口与实现分离 ├── mapper // 数据访问层:继承BaseMapper ├── entity // 实体类:对应数据库表 ├── dto // 请求/响应对象,避免实体直接暴露 ├── security // JWT与后端权限控制相关类 └── utils // 常用工具类application.yml里最值得说的是数据源配置。MySQL8.0连接串建议显式指定时区,否则默认时区导致时间和本地时间差8小时,这个是老问题:
spring: datasource: url: jdbc:mysql://localhost:3306/community_medical?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.DriverMyBatis-Plus全局逻辑删除配置也很关键:
mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl在实体类里的deleted字段上加@TableLogic注解,之后所有deleteById都是逻辑删除,所有查询自动追加deleted=0条件。这个配置让删除操作变得安全,特别适合医疗数据需要留痕的场景。
3.2 JWT登录鉴权与角色权限控制
系统不引入Spring Security全家桶,而是用JWT + 拦截器做轻量级鉴权,我觉得特别适合这类中小型系统。Spring Security功能强,但配置和过滤器链的学习曲线高;对社区医疗这类权限模型比较固定的系统,用拦截器反而好维护。
核心流程是:
- 用户提交账号密码,后端校验后生成一个JWT Token,token里封装用户ID、角色、过期时间。
- 前端请求时在Header中携带
Authorization: Bearer <token>。 - 后端定义一个
AuthInterceptor,对受保护接口校验token有效性和角色。
拦截器里可以放行登录接口和公告查询接口,其余接口做鉴权。权限控制使用自定义注解@RequireRole,标注在Controller方法上,例如医生接口就写@RequireRole("DOCTOR"),管理员接口写@RequireRole("ADMIN")。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole == null) { // 未标注权限注解的接口默认需要登录 return checkLogin(request, response); } // 校验登录并解析角色 UserPrincipal principal = parseToken(request); if (principal == null) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } String role = principal.getRole(); if (!Arrays.asList(requireRole.value()).contains(role)) { response.setStatus(HttpStatus.FORBIDDEN.value()); return false; } // 把用户信息放入ThreadLocal,service层可取用 UserContext.set(principal); return true; } }这个方案之所以好用,是因为无状态接口天然适合前后端分离,后端不必维护服务端Session,前端也能把Token持久化到本地实现自动登录。
3.3 MyBatis-Plus 条件构造器与分页实战
MyBatis-Plus 的LambdaQueryWrapper写条件查询简直舒服,不再有字符串硬编码。
以“查询某科室下所有医生并分页”为例:
Page<DoctorInfoVO> page = new Page<>(current, size); LambdaQueryWrapper<DoctorInfo> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(DoctorInfo::getDeptId, deptId) .eq(DoctorInfo::getStatus, 1); IPage<DoctorInfo> doctorPage = doctorInfoMapper.selectPage(page, wrapper);分页插件必须在配置类中手动注册,否则分页查询会变成全表查询,这是非常隐蔽的性能隐患:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination = new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }注意这里的顺序:如果有乐观锁插件,需要放在分页插件后面,但本系统没有使用乐观锁,所以只注册分页即可。setMaxLimit很重要,防止有人直接把size传成一万导致数据库压力过大。
3.4 预约挂号的并发控制实现
预约挂号是系统里并发压力最大的地方。我们模拟过多个居民同时抢最后一个剩余号源的场景,如果只做“先查remain_quota再update”,很容易超卖。
真正的实现思路是:
- 关闭自动提交,开启事务。
- 通过
UPDATE doctor_schedule SET remain_quota = remain_quota - 1 WHERE id = ? AND remain_quota > 0这样的原子扣减来锁行。 - 如果更新影响行数为0,说明号源已经满,直接返回预约失败。
- 更新成功后,接着插入预约记录,利用唯一索引兜底防重复。
数据库的原子UPDATE是这里的关键,不需要分布式锁也能在小规模并发下保持稳定。事务注解加在Service方法上:
@Transactional(rollbackFor = Exception.class) public boolean makeAppointment(AppointmentRequest request) { Schedule schedule = scheduleMapper.selectById(request.getScheduleId()); Date now = new Date(); if (schedule.getScheduleDate().before(DateUtils.truncate(now, Calendar.DAY_OF_MONTH))) { throw new BusinessException("该排班已过期"); } if (!schedule.getStatus().equals(ScheduleStatus.NORMAL.getValue())) { throw new BusinessException("该排班已停诊"); } // 原子扣减号源 int updated = scheduleMapper.cutRemainQuota(schedule.getId()); if (updated == 0) { throw new BusinessException("号源已约满"); } // 插入预约记录,唯一索引兜底 Registration registration = new Registration(); registration.setScheduleId(schedule.getId()); registration.setPatientId(request.getPatientId()); registration.setDoctorId(schedule.getDoctorId()); registration.setVisitStatus(VisitStatus.WAITING.getValue()); registrationMapper.insert(registration); return true; }这里有几个细节:
- 先校验排班日期,再用原子更新,顺序不能反。
- 事务中任何异常自动回滚,预约记录插入失败会连带扣减操作回滚。
- 前端点击“立即预约”必须做按钮防重复提交,因为网络重试也会产生并发请求。
3.5 自动填充与统一响应体
为了不在每个insert/update里手动赋值create_time和update_time,系统实现了MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }实体字段上加@TableField(fill = FieldFill.INSERT)、@TableField(fill = FieldFill.INSERT_UPDATE)即可。这样所有表的新增和修改都不用关心时间字段,统一逻辑处理。
后端返回统一使用Result<T>结构,包括code、message、data三个字段。前端请求拦截器统一判断code,不是200就走统一报错提示。这比后端直接返回裸对象或者零散Map可靠得多,接口文档也能自动生成出来。
4. Vue3 前端实现与联调
4.1 前端工程搭建与目录划分
前端使用 Vite 构建,配合 TypeScript 可选开启。如果不熟悉TS,直接用JavaScript也行,但工程里建议用TS,因为后面维护接口数据模型会很省力。
npm create vite@latest community-web -- --template vue cd community-web npm install vue-router@4 pinia element-plus axios sass标准目录结构:
src ├── api // 按模块拆分的接口请求 ├── assets // 静态资源 ├── components // 通用组件 ├── layout // 整体布局:侧边栏+顶部栏 ├── router // 路由配置与路由守卫 ├── store // Pinia状态管理 ├── views // 页面级组件 └── utils // 请求封装、工具函数把API请求单独拆到src/api非常明智。每个页面组件只调用对应的方法,不直接写axios,后面后端接口地址变动只需改一处。
4.2 Axios封装与路由守卫
axios封装是整个前端质量的关键。系统里做了三件事:请求拦截器加token、响应拦截器统一解包处理业务错误码、全局loading与错误提示。
import axios from 'axios' import { ElMessage } from 'element-plus' const service = axios.create({ baseURL: '/api', timeout: 15000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求错误') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } )路由守卫则在用户未登录时强制跳转登录页。把所有需要鉴权的路由统一挂在一个需要登录的父路由下,比在每个路由里加meta.requiresAuth更省事。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.public) { next() return } if (!token) { next('/login') return } next() })4.3 排班展示与预约操作
医生排班页面是前端业务复杂度最高的地方。系统使用一个按月视图组件展示排班日历,点击某一天的某个时段,弹出号码剩余和确认预约弹窗逻辑。
排班接口返回的数据通常包含scheduleId、period、totalQuota、remainQuota、status。前端按日期分桶后生成日历矩阵。核心代码类似:
const scheduleMap = computed(() => { const map = {} scheduleList.value.forEach(item => { const key = item.scheduleDate if (!map[key]) map[key] = [] map[key].push(item) }) return map }) const checkCanAppoint = (item) => { return item.status === 1 && item.remainQuota > 0 && !appointedSet.value.has(item.id) }这里的“已预约过”需要维护一个已预约排班ID集合,在日历上打上标记,防止用户重复点击。但真正防重复的还是后端唯一索引。
4.4 联调时的跨域与代理配置
开发环境下最稳妥的方式是前端代理,而不是在后端开启CORS。
vite.config.js代理配置如下:
server: { host: '0.0.0.0', port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }所有请求都发到/api前缀下,后端Controller类统一加@RequestMapping("/api/..."),这样部署到生产环境时Nginx也能直接按/api前缀反向代理到后端服务,前后端联调和生产部署无缝衔接。
不要在生产环境打开后端CORS的
allowCredentials(true)随便放行所有域名,除非是开放性接口。加全局限流、加基本鉴权,这些动作在放接口出去之前务必完成。
5. 部署运行与文档说明
5.1 本地环境准备与快速启动
本地跑起来需要以下环境:
- JDK 8 或 JDK 11,不要用JDK17跑SpringBoot2.7之前的老项目,模块化反射限制会导致启动失败。
- Maven 3.6+,用来拉取依赖和打包。
- Node.js 14.18+,推荐用16或18,配合npm或pnpm。
- MySQL 8.0,数据库字符集选
utf8mb4。
启动步骤:
- 创建数据库,导入根目录下的
db/community_medical.sql,脚本里包含建库、建表和基础测试数据。 - 修改后端
application-dev.yml中的数据库连接信息。 - 启动MySQL,执行
mvn spring-boot:run启动后端服务,默认端口8080。 - 进入前端目录,执行
npm install,再执行npm run dev,访问http://localhost:3000。
建议先拿测试账号玩一遍流程:管理员账号登录后创建医生、设置排班;居民账号注册登录后预约、生成病历;医生接诊后写病历开药方;药房发药后库存自动更新。整个闭环跑通后,对系统的理解会非常快。
5.2 文档里到底有什么
很多人说“含文档”,但打开一看就几页说明。这套系统的文档量要充足得多,主要包含:
- 需求文档:详细列出系统功能模块、角色权限、业务流程,可以用作需求分析参考。
- 数据库设计文档:包含每张表的字段注释、长度、默认值、索引,以及E-R图描述。
- 接口文档:主要接口的请求方式、路径、参数、响应示例,配合后端
springdoc-openapi生成的基础,手工补充了业务说明。 - 部署说明:本地开发环境、生产环境部署步骤,Nginx配置示例。
- 测试账号说明:不同角色的内置账号和初始密码。
我个人体会是,这套文档最大的价值在于前后端字段一致性。很多项目文档和代码脱节,这个项目至少接口字段在实际调用时是对得上的,联调效率会高出不少。
5.3 生产环境部署细节
生产部署用前后端分离最干净。
后端通过Maven打包:
mvn clean package -DskipTests java -jar target/community-medical.jar --spring.profiles.active=prod前端构建后生成dist目录:
npm run buildNginx配置要点是:静态文件指向dist目录,/api前缀反向代理到后端,同时处理Vue3 history路由的刷新404问题。
server { listen 80; server_name your-domain; root /opt/community-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files那行是Vue Router history模式必须加的,否则手动刷新页面或者直接访问子路由时会出现404。
6. 常见问题排查与避坑指南
6.1 数据库相关
问题1:MySQL8.0连接时报Public Key Retrieval not allowed
allowPublicKeyRetrieval=true参数没有配。MySQL8.0默认使用caching_sha2_password认证,如果没配这个参数,使用密码认证时客户端可能拒绝接受服务器公钥。
问题2:时间差8小时
连接URL里没有加serverTimezone=Asia/Shanghai。MySQL驱动默认会取系统时区,多数服务器是UTC,所以时间差8小时。统一在JDBC URL里显式设定即可。
问题3:导入SQL脚本报错,提到“Unknown collation”
脚本里使用了utf8mb4_0900_ai_ci排序规则,这个规则是MySQL8.0才支持的。如果你是5.7版本的库,就会报错。解决办法是把脚本里的排序规则替换成utf8mb4_general_ci,或者直接换MySQL8.0。
6.2 后端启动与运行问题
问题1:启动时报端口被占用
SpringBoot默认8080端口被其他进程占用时,启动日志会直接报Port already in use。可以换端口,也可以把占用8080的进程找出来干掉。
问题2:Lombok相关报错
IDEA需要安装Lombok插件,且annotationProcessorPaths需要配置正确。否则实体类里生成的getter/setter编译不过。
问题3:mvn spring-boot:run启动后访问接口404
先看控制台请求日志,确认是访问路径错了还是请求被拦截器挡了。很多情况下是拦截器没放行登录接口,导致前端所有请求都返回401。检查拦截器注册时的excludePathPatterns配置。
6.3 MyBatis-Plus 的隐藏坑
坑1:分页查询没有真正分页
忘了注册MybatisPlusInterceptor。这样调用selectPage时,返回结果虽然是Page对象,但SQL实际上是全表查询,内存分页。
坑2:逻辑删除字段导致某个查询结果为空
实体类加@TableLogic后,如果表中数据是物理删除残留的,很多旧数据会看不到,排查时先检查deleted字段的值。这是逻辑删除一致性带来的阵痛,不是代码逻辑坏了。
坑3:字段自动填充没生效
自动填充依赖MetaObjectHandler实现类能被Spring扫描到。常见问题是配置类包扫描路径没覆盖到,或者填充字段名和实体属性名不一致。严格检查注解和strictInsertFill参数。
6.4 前端联调常见问题
问题1:跨域但代理不生效
先确认前端请求地址确实走的是代理前缀/api,而不是直接写了http://localhost:8080。如果直接写了绝对地址,代理配置就不参与工作。另外修改vite.config.js后要重启dev server,热更新不会自动重载server配置。
问题2:登录后刷新页面路由跳回登录页
Token存在localStorage但没在应用启动时初始化用户信息。建议在路由守卫里每次刷新都校验token,或在Pinia里做初始化动作,从token中解析用户基础信息填充到store。
问题3:接口返回数据但页面没反应
大概率是数据层级变了。axios响应拦截器通常将res.data解包返回,但页面里写的是res直接作为列表数据,此时后端如果返回了分页结构,就要用res.records而不是res。调试时建议先把响应打印到控制台,确认结构后再绑定。
问题4:Vue3响应式丢失
使用reactive定义对象并且直接把后端返回的整个数组赋值给state.list = result,这种操作对于reactive对象内部嵌套的数组通常是响应式的,但如果用ref定义后给ref.value重新赋值则没问题。最容易出问题的是把reactive对象解构,解构出来的变量不再具备响应式。代码中如果看到const { list } = state然后修改list,往往页面不更新。这个项目里很多地方直接用ref或computed,规避了解构陷阱。
6.5 部署后问题
生产环境部署后刷新404,Nginx配置里需要try_files $uri $uri/ /index.html;。如果没有加,浏览器直接访问/schedule这种子路由会返回404,因为服务端找不到对应的物理文件。
生产环境接口后端报500,先看应用日志。最常用的是nohup java -jar xxx.jar --spring.profiles.active=prod >> app.log 2>&1 &方式启动,日志定位问题会方便得多。
关于二次开发的一点体会
这套系统给我最大的启发是它的边界拿捏得刚好:没有复杂到让人劝退,也没有简单到失去参考价值。我实际做二次开发时,第一件事把预约挂号模块的压测补了一轮,确认并发控制在低并发规模下没有问题;第二件事把报表统计SQL改成窗口函数版本,性能提升非常明显;第三件事打算引入定时任务做排班自动生成和过期病历归档。
如果你准备拿这套项目做毕业设计或者个人作品集,我建议优先扩展这三个方向:一是引入缓存缓存热点排班数据,减少数据库压力;二是加入消息通知模块,预约成功、停诊通知通过站内信或短信下发;三是做一套角色更细的权限体系,把护士、药师、科室主任都独立出来。这样既能在答辩时展示独立思考能力,又能真正把项目改造成一个更贴近生产的系统。
代码里还能优化的地方还有不少,比如把发送日志、操作审计、文件上传模块逐步补齐。反正我有时间就改一版,后面有新进展再单独写一篇分享。