前阵子有人在我的技术交流群里问社区医院管理系统能不能用现成的开源项目改一改就上,我当时的回答是:能,但千万别高估了“改一改”这三个字的含金量。社区医院的管理系统和三甲医院的HIS系统完全是两个物种,前者要的是轻、快、便宜,后者要的是稳、全、合规。而SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这一套组合,恰恰就是轻量级医疗管理系统里最实用的技术栈。我自己前后参与过两个类似的Java Web医疗项目,今天把这套系统的技术选型思路、核心模块设计、实际开发中遇到的坑以及部署细节整理出来,给准备做Java Web方向毕业设计,或者正在帮中小型诊所、社区卫生服务中心做信息化的朋友参考。
这套源码本身包含完整的前后端代码和文档,技术栈是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,典型的单体应用加前后端分离架构。后端的核心价值在于MyBatis-Plus带来的高效率CRUD开发,前端的核心价值在于Vue3组合式API加上Element Plus组件库做后台管理界面的便捷程度。全文我不会只贴一堆让人看不下去的代码片段,而是把整个系统的设计逻辑、开发顺序、容易踩的坑、部署方式都过一遍,保证你拿到源码或者照着从零开发的时候,心里有底。
1. 社区医院系统的业务边界:先弄清楚要开发哪些功能
1.1 社区医院和三甲医院的信息化差异
很多人一听到“医院管理系统”,脑子里马上跳出门诊挂号、住院管理、手术调度、检验检查、病历归档这一大堆东西,然后觉得这项目太复杂了。但社区医院真的没这么夸张。
社区医院的典型业务场景是:居民来看普通感冒、慢性病开药、做基础体检、打疫苗。整个流程通常是“挂号 -> 医生接诊开处方 -> 收费 -> 药房发药”,核心诉求就三个字:快、准、省。它不需要手术室排班,不需要大型检验设备对接,也不需要医保结算的深度对接(虽然现在部分地区要求对接,但大多数社区医院还是先用内部系统记账,再定期手工汇总上报)。
所以开发社区医院管理系统时,优先做的是这些模块:
- 患者档案管理:登记姓名、身份证号、联系方式、既往病史、过敏史
- 挂号管理:窗口挂号、科室选择、号别区分(普通号、专家号)、退号
- 门诊医生工作站:接诊、查看患者历史就诊记录、开具处方(药品、用量、用法)
- 收费管理:按处方划价收费、支持现金和扫码、退费
- 药房管理:药品入库、库存预警、发药、效期管理
- 统计报表:每日就诊量、科室营收、药品消耗排名、医生工作量
- 系统管理:用户管理、角色权限、操作日志
把这些业务想清楚再动手,整个开发周期可以控制在三到四周。如果一开始就想做大而全的HIS,那这个项目就不是SpringBoot2单体能扛住的事了。
1.2 角色划分决定了权限设计
社区医院系统的用户角色不像企业管理系统那么复杂,但权限设计依然要到位。通常会有这么几类角色:
| 角色 | 核心功能 | 独特权限 |
|---|---|---|
| 系统管理员 | 维护基础数据、管理用户角色 | 系统管理菜单、字典维护 |
| 挂号员 | 挂号、退号 | 挂号模块、患者档案新增 |
| 门诊医生 | 接诊、开处方 | 门诊模块、病历书写 |
| 收费员 | 划价、收费、退费 | 收费模块、费用查询 |
| 药房管理员 | 药品出入库、发药 | 药房模块、库存盘点 |
权限这块用最常见的RBAC模型就够了:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。登录后把用户拥有的菜单权限列表返回给前端,前端根据权限表渲染动态路由和菜单按钮。
后端认证我建议用JWT,做无状态登录。社区医院这种系统内部使用为主,不需要复杂的OAuth2和SSO,JWT加一个拦截器校验Token就是最实用方案。具体实现方式我放到后面后端的章节讲。
1.3 为什么这套技术栈适合这个场景
SpringBoot2加MyBatis-Plus的组合,属于“开箱即用”的典型。社区医院管理系统的数据结构并不复杂,大部分都是单表CRUD加上少量表关联查询,MyBatis-Plus的BaseMapper自带通用CRUD能覆盖大约七成的工作量。Vue3加Element Plus做后台管理界面,表格加表单加弹窗几乎能解决所有页面需求。
MySQL8.0相比5.7性能上有提升,而且对JSON类型、窗口函数的支持更加完善。虽然社区医院系统用不到这么花哨的特性,但既然是新项目,直接用8.0版本免得后续迁移麻烦。这套技术栈最大的优点其实是招人容易、资料多、踩坑有前车之鉴。遇到问题一搜基本都有答案,这对小团队和一个人单干的情况太重要了。
2. SpringBoot2 + MyBatis-Plus 后端架构:为什么这么组合,代码组织方式
2.1 项目分层与包结构设计
一个清晰的包结构决定后续维护的体验。我见过不少毕业设计项目,所有代码堆在几个类里面,一个Controller写了三千行,这种代码跑起来没问题,但答辩或者二次开发时会非常痛苦。
推荐用这样的分层结构:
com.example.communityhospital ├── common // 通用模块 │ ├── result // 统一返回体 │ ├── exception // 全局异常处理 │ └── utils // 通用工具类 ├── config // 配置类(MyBatis-Plus、CORS、Interceptor) ├── controller // 控制层 ├── service // 业务层 │ └── impl // 业务实现类 ├── mapper // 数据访问层(MyBatis-Plus的Mapper接口) ├── entity // 数据库实体 ├── dto // 请求参数封装 └── vo // 返回结果封装Controller只负责参数接收和调用Service,业务逻辑全部写在Service层,Mapper只做数据读写,Entity和数据库表字段一一对应,DTO用于接收前端参数避免和实体直接绑定,VO用于返回给前端的数据(比如医用列表不需要返回密码字段)。这样每一层职责明确,排查问题的时候定位非常快。
2.2 MyBatis-Plus 在项目里的几个核心用法
MyBatis-Plus对社区医院系统这种以CRUD为主的开发场景帮助很大,它有四个特性我几乎在项目里必用。
第一个是BaseMapper自带的CRUD。继承BaseMapper后,insert、deleteById、selectById、selectList这些常用方法直接可用,写Mapper接口只需要定义一些特殊查询。一张表的增删改查在Service层几行就搞定了。
public interface PatientMapper extends BaseMapper<Patient> { // 特殊查询自己写SQL,普通CRUD直接用BaseMapper方法 @Select("SELECT * FROM patient WHERE name LIKE CONCAT('%', #{name}, '%')") List<Patient> searchByName(String name); }第二个是LambdaQueryWrapper,避免在代码里硬拼SQL字符串。多条件查询用链式调用写起来非常舒服,而且类型安全。比如患者列表的分页条件查询:
LambdaQueryWrapper<Patient> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(query.getName()), Patient::getName, query.getName()) .eq(query.getGender() != null, Patient::getGender, query.getGender()) .orderByDesc(Patient::getCreateTime);第三个是分页插件。MyBatis-Plus3.x版本必须手动配置分页拦截器才能使用分页功能,这个坑很多人第一次踩。在配置类里注入PaginationInnerInterceptor:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }配置完后,Service层调用page方法就能拿到分页结果,IPage自带records、total、current、size这些属性,返回给前端时统一封装成分页VO即可。
第四个是逻辑删除。社区医院的数据医疗责任比较重,患者记录、处方记录一般不建议物理删除,用逻辑删除字段做标记更稳妥。在实体字段上加上@TableLogic注解,配置全局逻辑删除值即可。
mybatis-plus: global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 02.3 认证与权限:JWT + 拦截器的落地方式
登录接口的流程是:用户提交用户名密码 -> 后端校验 -> 校验通过后用JWT生成Token返回给前端 -> 前端后续请求都在Header中带上Token -> 后端拦截器校验Token并解析出当前用户信息。
JWT的依赖用io.jsonwebtoken的jjwt库,版本注意使用0.9.1以上,因为低于这个版本对JDK8和JSON解析的兼容有坑。生成Token的核心代码大概是这样:
String token = Jwts.builder() .setSubject(user.getUsername()) // 用户名 .claim("userId", user.getId()) // 自定义载荷 .claim("role", user.getRoleId()) .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 24小时过期 .signWith(SignatureAlgorithm.HS256, secretKey) .compact();拦截器方面,实现HandlerInterceptor接口,在preHandle方法里从请求头拿到Token,校验通过后放行,校验失败则封装一个401返回给前端。放行路径需要注意:登录接口、验证码接口、Swagger接口文档这些不需要Token,其余接口统一校验。
社区医院的权限和科室权限有关系。比如门诊医生登录后只能看到自己的患者接诊记录,不能看其他医生的处方。这个在SQL层做数据隔离:查询处方时根据当前登录用户的工号过滤。我一般会在Token里面带上departmentId和userId,然后在Service层解析出来拼进查询条件。
2.4 统一返回体与全局异常处理的细节
后台管理系统的接口风格要统一,不要让一个接口返回成功是{code: 0},另一个接口返回成功是{success: true},前端会很痛苦。用一个Result类统一包装:
public class Result<T> { private Integer code; // 200成功,500失败,401未授权 private String message; // 提示信息 private T data; // 业务数据 }全局异常处理用SpringBoot的@RestControllerAdvice + @ExceptionHandler,捕获业务异常、参数校验异常、数据库异常三类。特别要注意的是空指针异常也要兜底,不要让异常堆栈直接暴露给前端接口调用方,记录到日志文件方便排查,返回给前端只给“系统繁忙,请联系管理员”这类友好提示。
数据库异常必须单独处理。社区医院系统里,删除一个有处方记录的医生,会因外键约束报错,直接返回数据库原始错误信息既难看又暴露表结构。我在全局异常处理器里对DuplicateKeyException、DataIntegrityViolationException做了转译,统一返回业务提示,比如“该记录已被引用,无法删除”。
3. MySQL8.0 环境搭建与数据库设计:安装、配置、建表
3.1 两步搞定 MySQL8.0:Docker 与本地安装
MySQL8.0的环境搭建,我推荐两种方式,根据你的电脑情况选一个。
方式一:Docker容器化安装(最省事,适合不想污染本机环境的场景)。前提是已经装了Docker Desktop,然后执行下面的命令:
docker pull mysql:8.0 docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123456 \ -e TZ=Asia/Shanghai \ -v /Users/你的用户名/docker/mysql/conf:/etc/mysql/conf.d \ -v /Users/你的用户名/docker/mysql/data:/var/lib/mysql \ mysql:8.0这个命令里几个关键点解释一下:-p 3306:3306是把容器的3306端口映射到宿主机,这样Navicat这些工具就能直接连接;MYSQL_ROOT_PASSWORD设置root密码,自己测试的话简单点没关系,生产环境一定要强密码;TZ=Asia/Shanghai指定时区,这个很重要,如果不设置,后面做日期统计时会有八小时时差问题;两个-v参数是数据卷挂载,把配置和数据目录映射到宿主机,容器删除后数据不会丢。
方式二:本机直接安装。Windows用户去MySQL官网下载mysql-8.0.x-winx64.zip解压版,解压后配置一个my.ini文件,然后以管理员身份运行cmd,执行mysqld --initialize-insecure初始化,再执行mysqld -install把MySQL注册成Windows服务,启动服务后用mysql -u root登录,默认密码为空,然后自己alter user设置密码。
Mac用户更简单,直接brew install mysql,安装完成后brew services start mysql,默认root账号无密码,同样自己改一下。这两种方式我都试过,Docker方式的优势是删除重装干净利落,本地方式的好处是没有容器化增加的那层IO损耗。开发阶段两者差别不大,随你选。
3.2 需要避开的 MySQL8.0 配置坑
MySQL8.0和5.7最明显的区别是默认认证插件变成了caching_sha2_password。这会导致你用老版本的Navicat或者某些早期驱动连接时报错,错误信息通常提示Authentication plugin 'caching_sha2_password' cannot be loaded。
解决办法有两个:一是升级连接工具和JDBC驱动到8.0版本;二是如果不想升级工具,可以在创建用户时把认证插件改回mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root123456';JDBC连接串要记得加时区参数,否则报错或者时间错乱:
jdbc:mysql://localhost:3306/community_hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=trueallowPublicKeyRetrieval=true这个参数必须要加,MySQL8.0的驱动在访问caching_sha2_password用户时会因为拿不到公钥报错。
另外8.0版本的数据库创建语句,字符集推荐直接指定utf8mb4。社区医院系统要存患者姓名,偶尔会有生僻字,utf8mb4是必须的:
CREATE DATABASE community_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;3.3 数据库表设计与核心字段说明
社区医院管理系统的核心表,我基于这套源码的业务梳理下来,大概有下面这些:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 系统用户表 | id, username, password, real_name, role_id, department_id, status |
| sys_role | 角色表 | id, role_name, role_code |
| sys_menu | 菜单表 | id, parent_id, menu_name, path, component, perms |
| sys_user_role | 用户角色关联表 | user_id, role_id |
| sys_role_menu | 角色菜单关联表 | role_id, menu_id |
| patient | 患者档案表 | id, name, id_card, gender, age, phone, address, allergy, medical_history |
| department | 科室表 | id, dept_name, dept_code, introduce |
| registration | 挂号表 | id, patient_id, doctor_id, dept_id, reg_type, status, fee, create_time |
| prescription | 处方表 | id, reg_id, doctor_id, patient_id, diagnosis, create_time, total_amount, status |
| prescription_item | 处方明细表 | id, prescription_id, drug_id, drug_name, quantity, unit_price, dosage, usage |
| drug | 药品表 | id, drug_name, specification, unit, stock, warn_stock, purchase_price, sale_price, manufacturer, valid_date |
| charge_record | 收费记录表 | id, reg_id, prescription_id, amount, pay_type, cashier_id, create_time |
| drug_stock_log | 药品出入库日志 | id, drug_id, type, quantity, operator_id, create_time, remark |
字段命名的规范值得一提:所有表统一用下划线命名,主键统一用bigint,创建时间和更新时间统一叫create_time和update_time。MyBatis-Plus默认开启驼峰映射,下划线字段会自动转成实体类的驼峰属性,非常省事。主键用MyBatis-Plus的雪花算法生成(IdType.ASSIGN_ID),不要用自增主键,好处是分布式环境下不冲突,缺点我在后面联调章节会讲怎么处理精度丢失问题。
逻辑删除字段deleted我基本上每张有“删”语义的业务表都会加,患者表加了,挂号表退号状态用status区分(0待就诊 / 1已完成 / 2已退号),不物理删除。这样做的好处是退号、退费这类操作还能保留操作痕迹,方便后面审计和对账。
4. Vue3 前端工程搭建与后台管理页面实现
4.1 为什么选 Vite + Vue3 + Element Plus + Pinia
前端这块如果你已经习惯了Vue2的Options API,初上手Vue3的组合式API可能会有点不适应。但做后台管理系统,Vue3的组合式API加Script Setup的写法,代码聚合度确实高很多。一个用户管理页面的新增弹窗逻辑、表格加载逻辑、查询逻辑,以前要拆到data、methods、computed三个区域来回跳,现在用组合式API按功能域把相关代码聚在一起,维护起来很顺手。
Element Plus是Vue3官方适配的组件库,后台管理系统的表格、表单、弹窗、消息提示、标签页组件全都有,而且主题样式可以自定义。直接拿来当原型开发工具用,页面的CRUD功能半天就能搭完。
Pinia替代Vuex做状态管理。社区医院系统需要全局存储的状态其实不多,主要是当前登录用户信息和菜单权限列表。Pinia比起Vuex的优势是API更简洁,去掉了mutations这个概念,而且配合Vue3的响应式系统天然适配,不需要额外配置。
4.2 后台管理系统的工程结构搭建
用Vite创建Vue3项目:
npm create vite@latest community-hospital-admin -- --template vue cd community-hospital-admin npm install然后安装依赖:
npm install element-plus @element-plus/icons-vue axios pinia vue-router工程内的目录组织推荐这样:
src ├── api // 所有接口请求定义,按模块拆分(patient.js, reg.js, drug.js...) ├── assets // 静态资源 ├── components // 通用组件 ├── layout // 后台布局组件(侧边栏、顶栏、主内容区) ├── router // 路由配置与动态路由逻辑 ├── store // Pinia状态 ├── utils // 工具函数(axios封装在request.js里) └── views // 页面组件(system, patient, hospital, pharmacy...)api目录下每个文件对应一个后端模块,统一导出函数,页面组件里只调用函数不直接写axios请求。比如patient.js:
import request from '@/utils/request' export function getPatientPage(data) { return request({ url: '/patient/page', method: 'post', data }) } export function savePatient(data) { return request({ url: '/patient/save', method: 'post', data }) }这样做的目的是让接口定义集中管理,修改接口路径或者参数时只需要改一个文件。
4.3 Axios 封装与 Token 注入
axios的封装要点在于请求拦截器加上Token,响应拦截器统一处理业务状态码和HTTP状态码。
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) // 请求拦截器 request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '操作失败') if (res.code === 401) { // Token过期,清空登录态跳转登录页 localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } )VITE_API_BASE_URL这个环境变量在.env.development和.env.production里分别配置。开发环境我会用/开头然后让Vite代理转发到后端,避免跨域,生产环境则用nginx反代,也是/api开头。两种方式的具体配置我放在第5章讲。
4.4 一个通用 CRUD 页面的快速实现
后台管理系统最典型的是列表页,结构基本一致:顶部查询区域、中间表格区域、底部操作按钮。拿药品管理页面举例,核心逻辑长这样:
<script setup> import { ref, onMounted } from 'vue' import { getDrugPage, saveDrug, deleteDrug } from '@/api/drug' import { ElMessage, ElMessageBox } from 'element-plus' const list = ref([]) const total = ref(0) const queryParams = ref({ pageNum: 1, pageSize: 10, drugName: '' }) const loadData = async () => { const res = await getDrugPage(queryParams.value) list.value = res.data.records total.value = res.data.total } const handleSearch = () => { queryParams.value.pageNum = 1 loadData() } const handleEdit = (row) => { // 打开弹窗并回显数据 } const handleDelete = async (row) => { await ElMessageBox.confirm('确定删除该药品吗?', '提示', { type: 'warning' }) await deleteDrug(row.id) ElMessage.success('删除成功') loadData() } onMounted(() => { loadData() }) </script>模板部分使用el-table加el-form加el-pagination,代码量不大但是写起来非常机械。我个人的经验是先写一个带查询、分页、增删改的表单页模板,后续每个功能模块复制一份改字段,效率很高。这套源码里我基本也是这么做的,药房、患者、挂号、收费这些页面的底层结构都是一致的。
Vue3中还有一个容易忽视的点:组件中使用ref和reactive的选择。简单的表单数据我用ref维护一个对象,需要深层响应的数据结构(比如表格多字段操作)用reactive。如果遇到把v-for渲染的列表绑定到reactive数组、用下标直接赋值导致界面不更新的情况,多半是数组操作方式的问题,改用splice更换整个元素或者使用数组的push、splice等方法就能解决。
5. 前后端联调中的权限、跨域与数据格式问题
5.1 接口文档先行:Knife4j 集成
自己一个人做完整项目时,容易忽视接口文档的编写。但社区医院系统这种多个页面都要对接接口的项目,如果没有一份清晰的接口列表,前端写到一半就得反复问后端字段含义。
我在这个项目里集成了Knife4j(Swagger的增强版),只需要在后端pom.xml加依赖,然后在Controller类和方法上加注解,就能在浏览器里访问/doc.html查看每个接口的请求参数和返回示例。Knife4j比原生Swagger UI更符合国人习惯,而且对SpringBoot2的兼容性很成熟。
在Controller上加@Api注解说明模块功能,在方法上加@ApiOperation注解说明接口用途,在实体类字段上加@ApiModelProperty注解说明字段含义。这样做一份文档出来,自己用、答辩用、交给维护的人用都拿得出手。
5.2 跨域问题的两种正确处理方式
前后端分离项目跨域是必然要处理的。社区医院系统我实际操作过两种方案。
方案一:开发环境下用Vite的代理转发。在vite.config.js里配置server.proxy,这样前端请求/api开头的接口都会转发到后端地址,浏览器看到的是同源请求,不会触发跨域:
export default defineConfig({ server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })方案二:后端统一配置CORS。如果在生产环境直接前端直连后端端口,就用这个。通过实现WebMvcConfigurer,重写addCorsMappings方法:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里有个细节要提醒:allowedOriginPatterns是SpringBoot2.4以后的写法,如果你用allowedOrigins(""),配合allowCredentials(true)会报错,因为出于安全考虑不能再使用通配符。我一开始在这个问题上卡了很久,排查到最后发现是版本兼容性问题。
5.3 时间格式与 Long 精度丢失:联调必坑
这一节的内容我觉得比写代码本身更值钱,因为有两次线上排查经验都花了我大半天时间。
第一个坑是时间格式。MySQL8.0的datetime类型,MyBatis-Plus返回给Java是LocalDateTime,默认序列化成JSON时输出的是数组格式,类似于[2026, 5, 12, 14, 30, 0],前端根本没法直接用。解决办法是在application.yml全局配置时间格式化:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai但LocalDateTime不走date-format,需要加一个Jackson的配置类统一处理,或者直接在VO字段上用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解标注。我个人倾向后者,因为个别字段确实只需要日期不需要时间,比如患者的出生日期。
第二个坑是雪花ID精度丢失。MyBatis-Plus默认用的是雪花算法生成19位Long型主键,但JavaScript的Number类型最大安全整数是2的53次方减1(9007199254740991),也就是16位,超过这个值传给前端就会精度丢失。现象就是前端拿到的ID后几位变成了0,你编辑某条患者记录,提交时修改的却是另一条数据。解决办法是在返回实体的ID字段上加上@JsonSerialize(using = ToStringSerializer.class)注解,把Long转成String返回:
@JsonSerialize(using = ToStringSerializer.class) private Long id;加了之后前端拿到的ID是字符串,不会丢精度,传给后端时用Long接收也能自动转。这个细节我建议它在所有可能传到前端的Long主键字段都加上,一劳永逸。
5.4 权限联调:动态路由与按钮级权限
前端权限联调的核心是动态路由和按钮控制。用户登录后,后端根据角色返回该角色能访问的菜单列表,前端在全局路由守卫中动态注册这些路由。
具体做法是:登录成功后调用getUserMenu接口,拿到一个权限标识列表(比如sys:user:add, sys:pharmacy:list),存入Pinia。路由守卫中判断用户是否已经加载过动态路由,没有就调用接口生成后动态添加:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (!token) { if (to.path === '/login') return next() return next('/login') } if (to.path === '/login') return next('/') const userStore = useUserStore() if (userStore.menus.length === 0) { // 获取菜单并动态添加路由 userStore.loadMenus().then(() => next({ ...to, replace: true })) } else { next() } })按钮级别的权限用自定义指令v-permission控制,当用户权限标识列表里没有对应的按钮权限码时,把元素从DOM上移除。比如收费按钮只有收费员角色能看到,管理员全局能看到。
如果用的是TypeScript写Vue3,自定义指令注册要特别注意类型声明,不然会报TS的错误。这套源码里的前端主项目用的是JavaScript版本,如果你要换成TS,检查一下.d.ts的声明文件和Element Plus组件的全局类型,这块是若依这类基于TS框架改代码时最常见的报错来源。
6. 部署上线与项目文档的整理心得
6.1 前后端分离部署:Nginx 反代方案
社区医院系统的部署环境一般是一台2核4G的云服务器,这是最便宜且够用的配置。部署方式用Nginx做静态文件服务加API反向代理。
后端打包:
mvn clean package -DskipTests执行完在target目录下生成community-hospital.jar,上传到服务器,用java -jar命令启动:
nohup java -jar community-hospital.jar --spring.profiles.active=prod > /opt/app/logs/hospital.log 2>&1 &前端打包:
npm run build生成dist目录,把里面的静态文件上传到服务器Nginx的html目录。Nginx核心配置如下:
server { listen 80; server_name your-domain.com; location / { root /opt/www/community-hospital; index index.html; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }location /这里try_files配置是保证前端路由是history模式时,刷新页面不会404。location /api这里保留/api路径转发给后端,后端接口统一在/api前缀下(用server.servlet.context-path=/api配置),这样前端和后端的接口路径一脉相承。
后端生产环境跑起来要注意数据库连接配置,建议在服务器上创建一个单独的数据库账号,只授予community_hospital库的权限,不要直接用root账号连生产库。安全习惯要从这些小地方养成。
6.2 项目文档的内容组织
这套源码里带文档,我拿到手看了一下,文档的质量比大多数课程设计项目要高。整理文档时我一般会在项目根目录放一份README.md,包含几块内容:
- 项目简介与技术栈说明
- 目录结构说明
- 环境要求(JDK版本、Maven、Node版本、MySQL版本)
- 快速启动步骤(创建数据库、执行初始化SQL、启动后端、启动前端)
- 默认账号说明(管理员/admin123、医生/doctor123等)
- 部署说明(服务器环境、Nginx配置、后端启动命令)
- 常见问题FAQ
数据库初始化脚本要单独放在sql目录下,包含建库建表语句和基础数据(科目、管理员账号、菜单权限、药品字典等)。这里有一个细节:脚本要能重复执行,所以在建表语句前都加了DROP TABLE IF EXISTS,方便初始化时重置数据。
接口文档单独整理成一个docs目录,导出Knife4j的md文档或者离线HTML。虽然联调时在线看就行,但后续交接或者部署到内网没有外网环境时,离线文档的价值就体现出来了。社区医院系统经常是部署在政府卫生专网里的,外网访问不到,所以离线文档在项目实施中几乎是刚需。
6.3 上线后的维护事项提醒
系统上线不等于完事。社区卫生服务中心的电脑配置普遍不高,很多人还在用win7的老机器,浏览器版本也比较旧。Vue3项目在老旧浏览器上运行时偶发白屏或样式错乱,需要在前端做浏览器兼容处理。我一般会在index.html引入babel的polyfill或者让运维统一升级浏览器内核,实在不行就只用Chrome内核的浏览器访问。
服务器端MySQL的定时备份也是社区医院系统的刚需。患者的就诊记录、处方记录不能丢,至少每天凌晨做一次全量备份。最简单的方式是写个crontab脚本调用mysqldump:
0 2 * * * mysqldump -uhospital -p'密码' community_hospital | gzip > /opt/backup/hospital_$(date +\%Y\%m\%d).sql.gz备份保留最近30天即可,定期清理旧文件,避免磁盘占满。
遇到节假日门诊量骤增或者电脑突然断电这种场景,系统级的稳定性考验才会出现。我在第二次部署时遇到过断电后MySQL无法启动、导致挂号处电脑全连不上数据库的情况,后来检查发现是容器化部署没有设置restart=always,重启容器时MySQL容器没自动起来。修改docker run命令加上--restart always参数,并在宿主机配置开机自启后,这类基础问题基本不再出现。
我个人在实际操作中的体会是,社区医院管理系统这种中小体量的Java Web项目,真正影响交付质量的核心不在技术炫技,而在两点:一是对业务场景的理解能不能转化为合理的表结构和权限模型,二是在联调和部署阶段能不能快速定位那些看起来不起眼但特别浪费时间的小问题。这套SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0的组合,虽然每一项单拿出来都是成熟技术,但组合在一起恰好能覆盖社区医院信息化的全部需求,而且因为有完整的源码和文档可以直接跑起来看效果,省去了从零搭建骨架的时间。最后再分享一个实在建议:拿到源码后,先不急着改代码,把数据库跑起来,把系统完整操作一遍,跟着就诊流程走一次,你对接下来的所有改动都会有把握得多。