ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于SpringBoot+Vue+MySQL的社区医疗服务可视化管理系统设计

基于SpringBoot+Vue+MySQL的社区医疗服务可视化管理系统设计

拿到这个标题我第一反应是:又是毕设季最经典的那种“前后端分离 + 管理系统 + 可视化”三板斧组合。但仔细一看,这个其实是把两个很多同学容易做砸的东西放在了一起:一是社区医疗服务这个业务场景的贴合度,二是“可视化”不能只做一个静态图。SpringBoot + Vue + MySQL这套技术栈太经典了,反而是经典的组合更容易让人觉得“随便抄抄就行”,结果答辩被问两句就漏了底。这篇文章我从选型、数据库、后端、前端到部署和答辩逻辑全部过一遍,全部基于自己实际做过的项目经验来写,给正在做类似毕业设计或想练全栈的同学一个能直接落地的参考。后面没有废话,直接进正题。

1. 项目到底在做什么:需求拆解与功能边界

1.1 社区医疗服务场景的真实痛点

社区医疗服务系统和不医院挂号系统有个很大的区别:它的核心服务对象是社区常驻居民,服务模式偏向“慢病管理+日常健康服务+健康档案”,而不是“急诊住院+大型手术”。换句话说,居民到社区服务中心来,更多是量血压、测血糖、定期随访、基础体检、开常用药、预约疫苗接种这类轻医疗服务。

我之前帮一位在社区服务中心工作的朋友梳理过实际工作流程,发现他们日常大量工作是真的还在手工登记:居民来了先翻纸质档案,医生看完病再手动补录电脑。数据一多,回头要统计某个小区的老年人高血压占比、某个月的糖尿病患者随访率,得人工筛半天。这个系统表面看是做“可视化管理”,本质上是把社区医疗服务流程中“信息断档”的问题补上:

  • 居民基础档案电子化,不再抽一个档案袋翻十分钟;
  • 预约、随访、用药记录留痕,健康数据能按时间轴聚合;
  • 管理者需要一个公共卫生视角的数据看板,比如重点人群分布、就诊趋势,为社区健康干预提供依据。

所以,你在做这类项目给自己定位的时候,不要把它理解成“像美团一样的医疗预约App”,它是一个“以居民健康档案为核心,以社区服务流程为链条,以管理看板为决策辅助”的综合管理平台。

1.2 角色划分与功能矩阵

社区医疗系统的角色划分一般来说是三到四类:管理员、医护人员(医生/护士)、居民(患者)、如果有药品还需要一个库房角色的权限,但毕设通常没必要拆那么细,四类显得冗余,三类刚好。

我建议的角色设定:

  • 系统管理员:负责用户管理、科室信息维护、基础数据字典维护、公告发布,拥有系统全部菜单权限。
  • 医护人员:负责居民档案录入与更新、健康随访记录、门诊/接诊记录填写、预约审核处理、药品查询。医生进入系统看到的主要是待办和病历操作台。
  • 居民用户:负责查看健康档案、预约挂号、查看个人就诊记录和随访建议。居民通过前台界面自注册或者由管理员直接建档开通账号。

围绕这三类角色,功能边界划分起来比较清晰:

功能模块管理员医护人员居民
用户管理支持只读不支持
居民健康档案全部权限新增/编辑查看本人
预约挂号管理查看全部审核/处理预约发起/取消预约
门诊接诊记录查看全部新增/修改查看本人
健康随访管理查看全部新增/编辑查看本人
药品信息管理全部权限查看不支持
数据统计可视化全部报表部分报表个人健康图表
公告信息发布/编辑查看查看

有了这张矩阵,你做数据库设计的时候就不会漏表,也不会一个功能点搞出四五张表来。

1.3 功能边界的取舍:什么该做,什么不该做

做毕设最忌讳的就是什么都想往里塞。我看到太多同学把“社区医疗服务”做成了“综合医疗系统”,又是排班、又是住院管理、又是线上支付,最后结果是每张表都有,每个功能都是半成品。

我自己的建议是砍掉这几块完全别碰:

  • 在线支付:涉及第三方接口对接、退款、对账,工作量爆炸,答辩还不好讲。
  • 复杂的权限框架(如菜单级RBAC动态路由):社区系统用户量不大,用角色字段做简单路由拦截完全够用。
  • 消息推送/短信:不要做,技术难度高,评审老师也知道毕设做不出生产级推送。
  • 在线问诊聊天:很多人想做,但是WebSocket、消息记录、会话管理,牵扯面太大。

反而建议把省下来的时间投到“可视化”和“数据统计”上,这两块做深了,无论是页面效果还是答辩讲技术方案,都是很大的加分项。

2. 技术栈选型:每个选择的背后逻辑

2.1 后端为什么是SpringBoot,而不是SSH或SSM

这个其实不用多解释,SpringBoot在当下已经属于Java后端开发的绝对主流。但很多同学在写论文的时候,只知道“SpringBoot简化了配置”,写不到点子上。你答辩前一定要把这个理由讲得能让评审老师点头:

  • 嵌入式的Tomcat,不用单独部署WAR包,前后端分离项目通过一个可执行Jar包就能跑起来,部署成本低;
  • Starter机制把常见三方库的依赖冲突问题基本消灭了,比如你要集成MyBatis就加一个mybatis-spring-boot-starter,要做参数校验就加spring-boot-starter-validation;
  • 自动配置加外部化配置(application.yml),环境切换非常方便,开发环境、生产环境用不同Profile;
  • 生态成熟,遇到任何问题搜索解决方案基本都有现成答案。

我见过一个同学因为守旧选了SSM框架,结果光Spring和MyBatis的XML配置就堆了七百多行,答辩时连他自己都解释不清换环境之后为什么数据库连接串总报错。以学习为目的还能接受,以“在最短时间内做出稳定可演示系统”为目标,还是SpringBoot更合理。

2.2 前端为什么是Vue,可视化为什么必须配ECharts

前端选Vue的核心原因是它对新手最友好:模板语法接近原生HTML,渐进式引入,不需要一开始就啃复杂的工程化概念。当然了,现在官方推荐Vue 3加组合式API,如果你是从零开始学,那直接用Vue 3加Element Plus也没问题。但如果你项目是参考旧开源项目,大概率是Vue 2加Element UI,这个也不要慌,两者核心思想一致,对毕设来说完成功能优先。

重点说说可视化选型。ECharts在这个场景下是唯一合理的选择:

  • 纯前端渲染,无后端依赖,Apache基金会维护,文档和示例非常全;
  • 折线图、柱状图、饼图映射社区健康数据非常合适,而且渲染效果在同类框架里算非常精致的;
  • 不需要额外写后端图表API,数据从后端接口拿JSON,经过绘制配置转成图表即可。

特别是ECharts有“数据集”这个概念,可以非常自然地把后端返回的数据绑定给图表,比如这样:

myChart.setOption({ dataset: { source: res.data }, xAxis: { type: 'category' }, yAxis: {}, series: [{ type: 'line', smooth: true, encode: { x: 'month', y: 'count' } }] });

后端只需要返回一个包含month和count的数组,前端就可以动态渲染。

2.3 数据库为什么是MySQL,不是PostgreSQL和Oracle

很多同学写论文喜欢写“MySQL的优点”,背出来的全是官方宣传语:开源、免费、跨平台……说实话这些都不是选型理由。你选MySQL最实际的原因是:

  • 社区资料最多,遇到sql_mode问题、字符集问题、索引失效问题,搜出来的解决方案命中率高;
  • 学校机房和导师默认的就是MySQL,用其他库反而增加环境兼容风险;
  • Navicat和DataGrip对MySQL的支持非常成熟,导出SQL脚本给导师检查也非常方便;
  • 社区医疗这个数据量级(几百张表、几万到几十万条记录)对MySQL来说毫无压力。

你真正要在论文里写的是“MySQL 8.x的窗口函数和JSON能力足以支撑统计查询”,这句话比空泛的“免费开源”有信息量得多。

2.4 权限认证方案:JWT还是Session

技术选型里还有一个容易被追问的点:登录态怎么保持。我明确推荐JWT(JSON Web Token)方案,理由有三条:

  • 前后端分离环境下,Session共享需要额外解决跨域Cookie携带问题,JWT无状态,前端存储后每次请求放在Header里带过来,避免了很多麻烦;
  • JWT能承载用户基本信息(用户ID、角色),后端不查库就能识别身份;
  • 它是目前主流的后端认证方式,写在简历上或答辩里都不会被挑刺。

但这不代表JWT没问题。要注意JWT的有效期和续签策略,不设过期时间,或者设一天,都会影响使用体验。建议AccessToken设两小时过期,前端配合Axios拦截器,检测到401时重新走一次登录流程。

3. 数据库设计:核心表结构和ER关系拆解

3.1 表清单:最少需要哪些数据表

数据库设计是这个项目最先需要稳住的环节。我见过太多的项目做到一半疯狂加字段改表结构,就是因为一开始没把实体关系想清楚。按照上面的功能矩阵,我的表设计建议如下:

  • sys_user:用户表(登录账号、密码、姓名、角色、手机号、状态)
  • health_record:居民健康档案表(身高、体重、血型、既往病史、过敏史、家族病史)
  • doctor_info:医护人员信息表(科室、职称、简介)
  • department:科室表(内科、外科、全科……)
  • appointment:预约挂号表(预约人、预约医生、预约时间、时段、状态)
  • clinic_record:门诊/接诊记录表(就诊人、就诊医生、诊断结果、症状描述、处方)
  • follow_up_record:健康随访记录表(随访居民、随访日期、随访类型、结果描述)
  • medicine:药品信息表(药品名称、规格、库存、价格、生产厂家)
  • notice:公告表(标题、内容、发布时间、发布人)
  • dict_data/dict_type(可选):数据字典表,用于维护科室分类、预约时段等公共数据

差不多十张表上下,既有业务主线的深度,又没有膨胀到失控。

3.2 核心表结构字段设计参考

以健康档案表为例,字段设计要兼顾完整度和冗余度:

字段名类型说明
idbigint(20)主键自增
user_idbigint(20)关联用户表ID
namevarchar(50)居民姓名
gendertinyint性别 0男 1女
id_cardvarchar(18)身份证号
birth_datedate出生日期
heightdecimal(5,2)身高cm
weightdecimal(5,2)体重kg
blood_typevarchar(10)血型
allergy_historyvarchar(255)过敏史
past_historyvarchar(500)既往病史
family_historyvarchar(500)家族病史
create_timedatetime创建时间
update_timedatetime更新时间

这几个字段覆盖了体检类数据的最核心部分,如果你还想做“老年人健康评估”或“慢病管理”的可视化,这里一定要留出“是否慢病”“慢病类型”等标志字段——统计高血压患病率这类图表就是靠它出数据。

3.3 外键到底建不建,索引怎么加

很多教学大纲还在强调外键约束,但在实际开发里外键就是个隐患。原因很简单:如果订单被预约挂号的逻辑依赖了外键级联删除,删除用户的时候会把预约记录一并删掉,属于静默数据丢失。社区医疗系统中档案关联的数据非常多,用户删除如果级联删档案,会导致统计维度缺失。

我的做法是:保留字段关联(比如health_record.user_id),但不建物理外键约束,在逻辑层控制数据完整性。这样做的好处:

  • 插入和更新速度快一点,不用每次做外键校验;
  • 数据清洗或批量导入不受外键掣肘;
  • 删除操作可以自定义逻辑,比如将用户状态置为禁用而不是物理删除。

索引方面,预约表的doctor_id、visit_date,档案表的user_id,都是高频查询字段,一定要建普通索引。另外,如果系统里做了按身份证号查重,那id_card字段必须建唯一索引,否则用户重复建档的脏数据能让你崩溃。

4. 后端核心实现:接口设计、JWT、统计SQL

4.1 统一返回结果:R对象设计

一个管理系统,不管功能多少,接口风格一定要统一。我自己固定用的是R对象作为所有接口的返回结构:

@Data public class R<T> { private Integer code; private String message; private T data; public static <T> R<T> success(T data) { R<T> r = new R<>(); r.setCode(200); r.setMessage("操作成功"); r.setData(data); return r; } public static R<Void> error(int code, String message) { R<Void> r = new R<>(); r.setCode(code); r.setMessage(message); return r; } }

这样做的好处是前端Axios拦截器可以统一判断code来弹出错误提示,不用每个接口单独写try catch。全局异常处理器配合后效果更好:

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public R<Void> handleBusinessException(BusinessException e) { return R.error(e.getCode(), e.getMessage()); } @ExceptionHandler(MethodArgumentNotValidException.class) public R<Void> handleValidationException(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldError().getDefaultMessage(); return R.error(400, msg); } }

这种约定一旦定下来,后面写所有Controller都只会返回R.success(...),代码会非常清爽。

4.2 JWT登录鉴权的真实写法

JWT的坑主要在配置和拦截器上。Spring Boot 3里集成JWT,加依赖的时候注意jjwt版本要选0.11以上的,否则部分类名都不一样。核心思路走一遍:

第一步,生成Token的工具类:

public class JwtUtils { private static final String SECRET = "your-256-bit-secret-key"; private static final long EXPIRE = 1000 * 60 * 60 * 2; public static String createToken(Integer userId, String role) { return Jwts.builder() .claim("userId", userId) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(Keys.hmacShaKeyFor(SECRET.getBytes()), SignatureAlgorithm.HS256) .compact(); } public static Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(SECRET.getBytes())) .build() .parseClaimsJws(token) .getBody(); } }

第二步,登录接口签发Token:

@PostMapping("/login") public R<Map<String, Object>> login(@RequestBody LoginDTO dto) { User user = userService.login(dto.getUsername(), dto.getPassword()); String token = JwtUtils.createToken(user.getId(), user.getRole()); Map<String, Object> map = new HashMap<>(); map.put("token", token); map.put("userInfo", user); return R.success(map); }

第三步,拦截器校验Token:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); // 判空、解析、失败返回401 Claims claims = JwtUtils.parseToken(token); request.setAttribute("userId", claims.get("userId")); return true; } }

第四步,注册拦截器并配置放行路径:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/api/login", "/api/register"); } }

这里有个细节:跨域时预检请求OPTIONS也会经过拦截器,如果不对它放行,前端会一直报跨域错误。必须拦截器放行OPTIONS:

if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; }

这行代码能救不少人的命。

4.3 预约挂号核心流程怎么串起来

预约挂号是整个项目业务逻辑最复杂的点之一,状态流转必须设计清楚否则逻辑会乱。我的建议是预约表设计以下状态字段:

  • 0:待确认
  • 1:已确认
  • 2:已完成
  • 3:已取消

居民在前台发起的预约,默认状态待确认;医生可以确认接诊或取消预约;居民也可以取消未开始的预约。这里面的关键逻辑是“号源防重复”。我建议用唯一索引来兜底,在数据库层就干掉重复预约的可能性:

ALTER TABLE appointment ADD UNIQUE KEY uk_doctor_time (doctor_id, visit_date, time_slot, patient_id);

这个MySQL字段加对之后,即便并发请求同时打到后端,数据库也会拒绝重复条目,是比应用层synchronized更可靠的防重方案。

4.4 统计可视化页面的SQL怎么写

可视化不是前端画画就完了,后端要能给出高性能的聚合数据。三个核心场景:

场景一:近12个月就诊量趋势。

SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS visit_count FROM clinic_record WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH) GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month;

这里用DATE_FORMAT在MySQL里做按月分组合适,数据量小,比在Java里做循环统计效率高得多。如果你的MySQL版本到8.0,还可以用DATE_TRUNC的替代语法,效果一样,但考试和论文里写DATE_FORMAT更通用。

场景二:各科室就诊占比。

SELECT d.name AS department_name, COUNT(c.id) AS total_count FROM clinic_record c LEFT JOIN doctor_info di ON c.doctor_id = di.id LEFT JOIN department d ON di.department_id = d.id GROUP BY d.name;

这种统计会引出ECharts的饼图,前端不用做任何二次聚合。

场景三:慢病人群年龄分布。就像前面说的,如果档案表里存的是出生日期,那统计就要先算年龄再分组:

SELECT CASE WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 18 THEN '0-17' WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 45 THEN '18-44' WHEN TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) < 65 THEN '45-64' ELSE '65以上' END AS age_group, COUNT(*) AS user_count FROM health_record WHERE is_chronic = 1 GROUP BY age_group;

这样的SQL在答辩的时候也特别好讲,评审老师一看就知道你真的懂业务。

5. 前端核心实现:管理后台与可视化看板

5.1 前端路由设计:一页一模块

很多同学前端页面放了无数个vue文件,但实际上路由和菜单嵌套关系都没梳理好。做管理系统,路由用懒加载按模块划分:

const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/layout', component: () => import('@/layout/index.vue'), children: [ { path: 'dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index.vue'), meta: { title: '数据看板' } }, { path: 'appointment', name: 'Appointment', component: () => import('@/views/appointment/index.vue'), meta: { title: '预约管理' } }, { path: 'records', name: 'Records', component: () => import('@/views/healthRecord/index.vue'), meta: { title: '健康档案' } } // 其他模块 ] } ];

前端路由的meta.title和侧边栏菜单联动,最终生成的效果就是面包屑和菜单标题统一,不会出现菜单叫“健康档案”进来页面标题是“Dashboard”这种低级问题。

5.2 Axios封装:拦截器做三件事

Vue项目里Axios一定要封装,不封装的直接后果是每个页面里面都有一大堆重复的loading逻辑和状态判断。我的封装固定做三件事:

第一,请求拦截器带Token:

service.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = token; } return config; });

第二,响应拦截器统一处理401和业务错误:

service.interceptors.response.use( response => { const res = response.data; if (res.code !== 200) { if (res.code === 401) { router.push('/login'); } ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; }, error => { ElMessage.error('网络请求异常'); return Promise.reject(error); } );

第三,API接口统一在独立的js文件里管理,比如api/appointment.js:

export function getAppointmentList(params) { return request({ url: '/api/appointment/list', method: 'get', params }); } export function confirmAppointment(id) { return request({ url: `/api/appointment/confirm/${id}`, method: 'put' }); }

这么做的价值,前端组件里永远只调接口函数,不自己拼URL,全局改接口版本号的时候只动封装文件,维护成本低到极致。

5.3 ECharts可视化看板的实现思路

就我的经验来说,可视化看板是决定整个毕设“看起来高级不高级”的核心页面。视觉上做得好,装修分数蹭蹭往上涨。但实现复杂度并不高。

第一步,安装ECharts:

npm install echarts

第二步,封装一个图表组件,基础折线图的逻辑长这样:

<script setup> import * as echarts from 'echarts'; import { ref, onMounted } from 'vue'; const chartRef = ref(null); let myChart = null; onMounted(() => { myChart = echarts.init(chartRef.value); const option = { title: { text: '近12个月就诊趋势' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category' }, yAxis: { type: 'value' }, series: [{ name: '就诊量', type: 'line', smooth: true, areaStyle: {} }] }; // 从后端API拉数据 getVisitTrend().then(data => { option.xAxis.data = data.map(item => item.month); option.series[0].data = data.map(item => item.visit_count); myChart.setOption(option); }); }); </script> <template> <div ref="chartRef" style="width: 100%; height: 360px;"></div> </template>

第三步,看板页面用栅格布局,做成一个两行两列的“仪表盘”效果:

  • 左上:就诊量趋势折线图(月份X轴,数量Y轴)
  • 右上:科室就诊占比饼图
  • 左下:居民年龄分布柱状图
  • 右下:药品库存预警表格或横向条形图

这样一块大屏,配合合适的颜色和间距,放答辩PPT里就是最好的框架图支撑材料。ECharts图表的自适应别忘了加:

window.addEventListener('resize', () => myChart.resize());

不然演示时你拖拽浏览器窗口大小,图表就变成了半个身子,非常尴尬。

5.4 表单校验:用细节提升系统完整度

很多同学做的后台,表单点完能提交,但一到异常输入就露馅。评审老师最爱的操作就是随便填几个非法字符再点提交看反馈。为了应对这一点,前端表单校验不能只依赖HTML的required。我推荐Vue用Element的rules配置:

const rules = { phone: [ { required: true, message: '手机号不能为空', trigger: 'blur' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' } ], idCard: [ { required: true, message: '身份证号不能为空', trigger: 'blur' }, { pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '身份证格式不正确', trigger: 'blur' } ] };

这种做法,一小段代码就能配合后端参数校验堵住绝大多数非法输入。好的毕设项目不需要面面俱到,但关键输入位置必须有反馈,这决定了你在答辩演示环节的“可控感”。

6. 部署与联调:本地跑通一次完整流程

6.1 开发环境版本选择

版本选对有安定心神的作用。根据项目和现在主流教程的配置,我建议按下面这个清单装:

  • JDK:8 或 17 都可以。如果用的是Spring Boot 3.x,必须JDK 17;如果用的Spring Boot 2.7,JDK 8完全够。考虑到代码生成器兼容性,Spring Boot 2.7 + JDK 1.8的组合是最稳的。
  • MySQL:5.7 或 8.0,如果是自己新装的,建议直接8.0。
  • Node.js:14.18 或 16.x,适配Vue 2项目;Vue 3的话建议Node 16+。
  • 包管理器:npm 或 yarn均可,别混用。
  • 开发工具:IntelliJ IDEA 后端 + VSCode 前端,或者全部用IDEA也行。

6.2 项目初始化的几个关键步骤

第一步,后端用Spring Initializr生成基础工程,勾选Spring Web、MySQL Driver、MyBatis、Lombok、Validation,这几个依赖是核心。不需要一开始选一堆没用的中间件,比如Redis和消息队列,加依赖容易排除依赖难。

第二步,配置文件里注意时区问题:

spring: datasource: url: jdbc:mysql://localhost:3306/community_health?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai

serverTimezone不配,启动直接报时区错误;Jackson日期格式不配,前端拿到的时间就是一串时间戳,还得每个接口单独处理,所以这两行一定在一开始就配上。

第三步,前端初始化Vue项目,推荐用Vite加Vue 3,比Webpack构建快好几倍。如果项目模板是Vue 2,那保留Webpack也行,但新建直接Vite。

6.3 前后端联调与跨域跨域的根因不在后端,而在浏览器的同源策略:前端跑在8080端口,后端跑在9090端口,两者端口不一样,浏览器就认为是跨域,直接拦掉请求。解决方式三个:

第一,后端配置全局CORS:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

第二,前端Vite开发环境配置代理,这是开发期最顺手的方案,生产环境也不用改代码:

// vite.config.js server: { port: 8080, proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } }

这样前端代码里请求/api/login,开发服务器自动转发到后端9090端口。生产部署时把打包后的静态文件放在Nginx里,再配一个反向代理到后端服务,效果和开发期一致。两个方案双保险,建议都用上:开发阶段用Vite代理,部署阶段用Nginx反向代理,性能和灵活性都兼顾。

第三,如果只是临时测试,后端把CORS配置配好,前端直接写完整URL请求也可以,但这样不是好习惯,上线必踩坑,趁早用代理方案。

6.4 打包部署要点回顾

后端打包:

mvn clean package -DskipTests

然后java -jar target/community-health.jar就能跑。如果数据库不在本机,注意打包前把application.yml里的数据库地址改成部署机器的可访问地址。

前端打包:

npm run build

生成dist目录,把里面的静态文件扔到Nginx的html目录下,Nginx配置里加一条代理规则把/api转发到后端:

server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:9090/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里有一行很关键:try_files $uri $uri/ /index.html;。如果不写这一行,刷新页面到二级路由时Nginx会报404。好多同学部署完发现首页能打开,一刷新就白屏,就是这个配置缺失。

7. 常见问题和排查技巧实录

7.1 前端页面白屏和控制台报错

大概率是路由模式问题。Vue Router的history模式,在Nginx下需要依赖后端配置;如果你不想过多配Nginx,直接改用hash模式最省心:

createWebHashHistory()

URL虽然带个#,但部署在任何静态服务器都不需要额外配置。毕设演示完全能接受。如果需要美观,再上history模式配Nginx。

7.2 后端启动报数据库连接失败

先检查MySQL有没有启动,再核对application.yml里的密码和端口。另外注意MySQL 8.0的驱动名是com.mysql.cj.jdbc.Driver,低版本驱动连8.0数据库会报加密协议错误。这一步如果没注意,能卡一整天。

7.3 MyBatis查询结果中文乱码

一句话:在MySQL连接URL上加characterEncoding=utf8,同时在MySQL侧把表结构默认字符集设置成utf8mb4。如果是新建库,建库语句直接用:

CREATE DATABASE community_health DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

这样基本杜绝乱码的来源。

7.4 预约功能始终保存失败

这个排查三个点:一是前端提交的数据里“时间”字段是字符串,后端实体有没有加@DateTimeFormat;二是预约状态字段给的值是否在枚举范围内;三是数据库表有没有约束和它冲突。一般九成的保存失败都是类型不匹配,看后端控制台报错信息比瞎试效率高得多。

7.5 登录成功但请求接口报401

这种问题多半是前端把Token放在了请求参数而不是Header里,或者Token被解析后过期了。后端拦截器可以打印一下对应请求日志,确认Header到底有没有传成功。

8. 避坑指南与实操心得

8.1 别用复杂的代码生成器

我见过非常多同学在项目里集成MyBatis-Plus Generator、甚至自定义了一套代码生成前端页面的工具,目的就是“快”。结果代码生成后一堆冗余文件放在工程里,几百行全是自动生成的,逻辑在哪里根本分不清。毕设答辩前你要是从头看一遍自己的代码,花的时间比重新写一遍还久。

我自己的偏好是:手动建表、手动写Mapper XML、手写Service。项目规模不大,也就十张表左右,手写一遍的好处是你能准确说出每个接口的逻辑。包括MyBatis-Plus这个依赖,我建议用它的BaseMapper和ServiceImpl来省掉基础CRUD代码,但复杂的统计SQL还是手写XML,这样接口实现有轻有重,讲起来也有层次。

8.2 代码里别堆“试错痕迹”

很多人做项目过程中会改需求,于是代码里充斥着被注释掉的旧逻辑、调试用的System.out.println、临时写死的假数据。我强烈建议提交前做一次全面清理:

  • 搜索console.log和System.out,全删;
  • 搜索注释掉的代码大段,全删;
  • 搜索写死的测试值,改成从词典表读取。

不要觉得这些无关紧要。评审老师一般不会逐行讲代码,但一旦评审演示过程中页面报错或者控制台打印出一堆痕迹,印象分立刻打折扣。

8.3 演示数据要做到“恰到好处的假”

做可视化和预约功能,没有数据光秃秃肯定不行。建议写一段测试数据SQL脚本,一次性往数据库里塞:

  • 居民档案:30~50条,覆盖不同年龄段,包含几条明显的高血压/糖尿病患者;
  • 预约记录:近3个月的,状态各异;
  • 门诊记录:跟着预约走,至少30条;
  • 药品库存:设置几条低于预警值的记录,让预警图表有内容。

有了这套脚本,演示任何模块都有数据支撑,而不是现场临时录数据。脚本建议放在项目根目录的sql/demo_data.sql里,这也是加分细节。

8.4 答辩准备时怎么讲项目

明确讲“解决什么问题”、“用了什么方案”、“最后做出了什么效果”这三个层面,不要花大量时间背技术栈概念。评审老师最常用的问题就这几个:

  • 数据可视化用到了哪些图表?数据从哪来?
  • JWT的Token过期了怎么办?用户重新登录怎么处理?
  • 多角色权限是怎么控制的?
  • 预约并发时怎么防止重复?
  • 慢病统计的SQL是怎么写的?

这些问题,把这个项目真正做一遍之后再对照本文过一遍,就基本都能接住。回答的时候多讲场景、讲细节,会非常有说服力。

结尾

最后分享一个自己的实际经验。做这类全栈管理系统,最容易踩的坑是“沉迷于搭建,忘记了业务”,也就是把大量时间花在初始化项目、配置依赖、折腾前端脚手架上,等到真正要写业务逻辑的时候发现时间已经不够了。正确的节奏是:第一周把环境和数据库表定死,第二周做后端CRUD和登录,第三周做前端页面和可视化,第四周填充数据和打磨细节。加上“演示数据脚本”这个技巧,你会发现整个项目的完成度会瞬间上一个台阶。

社区医疗服务和可视化管理的组合,无论从业务温度还是技术展示度来说,都是一个非常适合作为综合训练的项目。写好每一张表、跑通每一个流程,这个项目做下来,你对前后端分离、权限认证、数据聚合的理解会非常扎实。跨过跑通之前的那道坎,后面就是坦途了。

RELATED READING

延伸阅读

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