这套前后端分离的医院后台管理系统,用SpringBoot、Vue、MyBatis、MySQL四个技术栈搭了一套完整的业务闭环,覆盖了挂号、门诊、收费、药房、住院这些医院日常运营的核心环节。市面上的医院管理系统教程很多,但多数只给一段登录功能加两张表,真正能跑起来、能部署、能当项目经验讲清楚前后端如何协作的反而少。这篇文章我就从项目设计、数据库建模、后端接口实现、前端页面搭建到最后的打包部署,把整条链路完整拆开讲一遍,尤其是我实际开发中遇到的那些坑和取舍,希望对正在做毕设或者想拿完整项目练手的朋友有帮助。
1. 项目整体设计与技术选型思路
1.1 为什么选"前后端分离"这套组合
医院后台管理系统有一个很典型的特点:业务角色多、操作界面差异大、数据流转链路长。管理员、医生、护士、药房药师、收费员,每个角色看到的页面和操作权限完全不同。如果还用传统的服务端渲染模式,所有页面逻辑都堆在后端模板里,前端每改一个按钮样式,后端就要重新编译打包,开发和维护效率会非常低。
前后端分离的核心逻辑,就是把"数据接口"和"页面展示"彻底拆开。后端团队只负责提供稳定的RESTful API,前端团队只关注Vue组件和页面交互,两边通过JSON数据通信。医院项目还有一个好处:后端服务可以独立部署在服务器上,前端打包成静态文件用Nginx托管,平时医院内网的接口升级完全不影响正在使用的页面,出了问题也能快速回滚,这对业务连续性要求高的场景非常友好。
技术栈选择上,SpringBoot负责接口层和业务层,它内置Tomcat、自动配置能力强,一个jar包就能启动整个后端服务,省去了大量XML配置。Vue负责前端页面,它的响应式数据绑定和组件化开发非常适合后台管理这种"表格+表单+弹窗"密集型的交互场景。MyBatis作为持久层框架,保留了SQL的灵活性,医院业务里经常会出现多表关联、复杂统计查询,用原生SQL写反而比ORM框架拼条件更直观。MySQL则是最成熟稳定的开源关系型数据库,医院系统对数据一致性和事务要求高,MySQL的InnoDB引擎在事务和行级锁上完全够用。
1.2 从业务场景反推模块划分
我不喜欢一上来就列技术清单,做这类系统最好先走一遍业务。想象一下一个患者从进医院到看完病的完整流程:先挂号,然后去诊室看医生,医生开处方,患者去收费处缴费,再去药房取药,如果情况严重还要办理住院。每个环节都对应一个后台管理功能。
所以这套系统的模块划分是跟着业务走的:
- 系统管理:用户管理、角色管理、菜单权限管理
- 门诊管理:挂号管理、医生排班、门诊候诊
- 医生工作站:患者接诊、电子处方单录入、检查检验申请
- 收费管理:费用结算、退费管理、收费日结报表
- 药房管理:药品信息维护、库存管理、出入库记录
- 住院管理:入院登记、病房床位管理、医嘱执行
这些模块之间不是孤立的。挂号会产生一条挂号记录,医生接诊需要关联这条记录,开处方又关联挂号ID,收费时根据处方明细计算费用,药房发药时扣减库存。如果一上来就埋头写代码,很容易漏掉这些关联关系。先画清楚业务流程图,再设计表结构,后端的Controller和前端页面都是水到渠成的事。
1.3 技术栈落地时的几个取舍
我没有引入Spring Cloud微服务那套,原因很简单:医院后台管理系统属于典型的单体应用,用户量撑死几百人并发,拆成微服务只会增加服务注册、配置中心、网关这些复杂度,对业务本身没有任何收益。单应用加前后端分离,已经是这个规模下性价比最高的架构。
持久层我用的是原生MyBatis而不是MyBatis-Plus,这算是一个有意的选择。MyBatis-Plus确实写CRUD很方便,内置方法连SQL都不用写,但问题在于项目经验面试的时候,面试官问你MyBatis的Mapper XML怎么传参、动态SQL标签有哪些,你如果只会用Plus的BaseMapper,很容易露馅。原生MyBatis能让你真正理解SQL映射、参数绑定和缓存机制,学会之后切到Plus也就是加个依赖的事。另外,医院系统的很多查询需要自定义动态SQL,MyBatis的<if>、<foreach>标签在这里比Plus的Wrapper更加直观可控。
MySQL版本我建议用5.7或者8.0,两个版本我都跑过。5.7稳定、资料多,网上搜报错基本都有答案;8.0性能更好,窗口函数、JSON类型都支持,但连接驱动要换com.mysql.cj.jdbc.Driver。考虑到教程的普适性,下面所有示例我都基于MySQL 5.7写,如果用的8.0,注意驱动类名和时区参数就好。
SpringBoot版本这里要特别注意。现在很多新项目直接上SpringBoot 3.x,但3.x要求JDK 17,而且包名从javax改成了jakarta,网上大量老教程的代码在3.x下面根本跑不起来。我做这套系统用的SpringBoot 2.7.x + JDK 8 + MyBatis 2.x,这是目前兼容性最好、资料最多的组合。你如果非要尝鲜用3.x,记得所有import javax.servlet.*改成import jakarta.servlet.*,MyBatis也需要用对应的starter版本。别让版本问题成为拦路虎,稳定跑起来比追求新版更重要。
2. 数据库设计与核心表结构
2.1 表结构怎么规划才不乱
数据库是这套系统的地基。我见过很多新手项目,表建得随意,字段类型混乱,比如金额用double存,时间用varchar存,后面做统计回调数据全乱了。我建表前花了半天时间统一规范:主键全部用bigint自增,金额用decimal(10,2),时间用datetime,状态字段用tinyint配合字典表解释,逻辑删除用deleted字段,所有表都带create_time和update_time。
整个系统一共设计了20张表,核心表如下:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| sys_user | 系统用户(医生/药师/收费员/管理员) | username、password、real_name、role_id |
| sys_role | 角色表 | role_name、role_key |
| sys_menu | 菜单权限表 | menu_name、parent_id、path、perms |
| patient | 患者档案表 | patient_name、id_card、phone、gender |
| registration | 挂号表 | patient_id、doctor_id、dept_id、visit_date、status |
| dept | 科室表 | dept_name、leader、phone |
| doctor_info | 医生信息表 | user_id、dept_id、title、introduction |
| prescription | 处方表 | registration_id、doctor_id、total_amount、status |
| prescription_item | 处方明细表 | prescription_id、drug_id、quantity、price |
| drug_info | 药品信息表 | drug_name、specification、unit、price、stock |
| drug_stock_record | 药品出入库记录 | drug_id、type、quantity、operator_id |
| settlement | 收费结算表 | registration_id、amount、pay_type、status |
| bed | 病床表 | bed_no、ward_id、status |
| hospitalization | 住院记录表 | patient_id、bed_id、admission_date、discharge_date |
这些表之间的外键关联我没有直接在数据库层面强制加,而是通过业务代码维护。原因很简单:医院系统后续很可能要做分库分表或者数据清洗,物理外键会变成约束。逻辑关联靠ID字段保证,查询时用JOIN拼,性能更好也更灵活。
2.2 关键表设计背后的思考
就拿registration挂号表和prescription处方表来说,它们的字段设计直接影响后面功能好不好写。挂号表我记录了visit_date和visit_time两个字段,上午下午分开,方便医生排班查询当天号源。同时加了一个status字段,0表示待就诊,1表示已就诊,2表示已取消,这样收费和药房模块通过状态就能判断是否允许发药,不用去关联一堆业务表。
处方表的total_amount字段是冗余存储的。正常做法是通过处方明细表里每条药品的quantity * price累加算出总额,但每次结算都去累加一遍,代码啰嗦而且效率低。我在生成处方的时候就直接算出总额存进去,收费模块直接读这个字段,明细表和汇总表的数据一致性通过事务来保证。这就是典型的"冗余换性能"思路,但要记住:冗余字段必须由程序保证同步更新,否则会出现对不上的情况。
密码存储方面,sys_user表的password字段我用的BCrypt加密,不是MD5。MD5加盐虽然也能用,但BCrypt每次加密结果不同,防彩虹表攻击更稳,而且Spring Security自带BCryptPasswordEncoder可以直接用。这个是安全红线,医院数据涉及患者隐私,密码绝不能明文存。
2.3 建表SQL里的三个细节坑
第一,字符集必须用utf8mb4而不是utf8。utf8在MySQL里最多存3个字节,患者姓名如果出现生僻字或者表情符号,直接报Incorrect string value错误,utf8mb4才是完整的4字节UTF-8编码。库、表、字段三级都要指定。
第二,金额字段一律decimal(10,2),禁止用float和double。浮点数在计算机里是二进制存储的,0.1加0.2可能等于0.30000000000000004,做药品价格、结算金额这种敏感数据,精确计算是底线。Java对应用BigDecimal接收,前端也要注意精度。
第三,每个表都加create_time、update_time两个时间字段,并且设置DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP,插入和更新的时候会自动维护,省去在业务代码里手动塞时间的麻烦。逻辑删除字段deleted默认值设为0,所有查询条件里都要带上deleted = 0。
3. 后端实现:SpringBoot + MyBatis 的核心细节
3.1 工程目录分层与职责边界
后端工程我用了标准的四层结构:Controller层负责接收请求和参数校验,Service层处理业务逻辑和事务边界,Mapper层只做数据库读写,Entity层对应表结构。这套分层虽然老,但职责清晰,后续加人维护不会乱。很多新手喜欢在Controller里直接写业务代码,当时觉得快,等要复用逻辑的时候就傻眼了。
一个患者挂号的请求流程大概长这样:前端POST一个JSON到/api/registration,Controller接收后调用RegistrationService.register(),Service里先查医生当天号源是否已满,然后插入挂号记录,再更新号源余量,这两个操作在同一个事务里。事务的注解@Transactional加在Service方法上,默认遇到RuntimeException就回滚,这是保证数据一致性的关键。
我在代码里还统一封装了一个返回体Result<T>,结构是{ code, message, data }。code为200表示成功,401表示未登录,403表示无权限,500表示服务器异常。前端Axios拦截器统一根据code做处理,不需要每个接口单独写错误提示。这个设计强烈建议照抄,比返回裸数据好用太多。
3.2 JWT登录认证与拦截器的实现
这个系统的登录认证用的是JWT方案。用户输入用户名密码,后端校验通过后生成一个带过期时间的token返回给前端,前端每次请求都在Header里带上Authorization: Bearer token。后端写一个拦截器统一解析token,解析失败直接返回401,成功则把用户ID放进ThreadLocal,后续业务代码随时可以拿到当前操作人。
JWT生成的核心代码如下,使用io.jsonwebtoken库:
public String generateToken(Long userId, String username) { Date now = new Date(); Date expireDate = new Date(now.getTime() + 3600L * 1000 * 24); // 24小时过期 return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }这里有两个优化点。第一,token过期时间设置24小时,但医院系统里医生可能连续值班,用着用着突然被踢下线很影响工作。我在拦截器里加了"剩余有效期小于4小时则自动续期"的逻辑,响应头里返回新的token,前端统一替换。第二,token里不要放敏感信息,JWT的payload只是Base64编码,不是加密,任何人解码都能看到内容。
拦截器配置要注意放行白名单:登录接口、验证码接口、静态资源这些不需要拦截,其他接口全部走拦截器。另外CORS跨域配置必须和拦截器一起定义,否则前端请求会先被CORS拦截干掉。我用的WebMvcConfigurer实现addInterceptors和addCorsMappings两个方法,同一个配置类里搞定。
3.3 MyBatis的Mapper写法与缓存配置
MyBatis的Mapper我习惯用XML方式写,因为复杂SQL在XML里格式清晰、方便调试。一个典型的分页条件查询差不多长这样:
<select id="selectPatientPage" resultType="com.example.entity.Patient"> SELECT id, patient_name, id_card, phone, gender, birthday, create_time FROM patient <where> <if test="keyword != null and keyword != ''"> AND (patient_name LIKE CONCAT('%', #{keyword}, '%') OR phone LIKE CONCAT('%', #{keyword}, '%') OR id_card LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="gender != null"> AND gender = #{gender} </if> AND deleted = 0 </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签自动处理前导的AND,<if>标签动态拼接条件,这是MyBatis最核心的用法。Mapper接口方法参数多的时候,我统一用@Param注解命名,参数名和XML里的#{keyword}保持一致。特别注意:XML里的小于号<要转义成<,否则XML解析直接报错。
缓存方面,MyBatis有一级缓存和二级缓存。一级缓存默认开启,作用范围是一次SqlSession,也就是同一个事务内相同查询不会重复查库。二级缓存需要手动开启,作用域是Mapper namespace,多个SqlSession共享。但这里有个大坑:二级缓存对多表JOIN查询要非常小心,因为缓存刷新是按namespace的update来触发的,A表的缓存可能在B表更新后仍然存在,查询结果就不一致了。我直接没开二级缓存,医院系统的数据太多是高并发修改场景,缓存带来的风险远大于性能收益,真要提速就让前端做页面缓存或者用Redis。
3.4 SpringBoot环境配置与版本选择建议
后端项目跑起来之前,最大的拦路虎就是环境。我给这套系统锁定的版本组合是:JDK 1.8、SpringBoot 2.7.18、MyBatis starter 2.3.1、MySQL 5.7。这个组合经过了大量项目验证,网上任何报错都能搜到解决方案。
pom.xml里核心依赖如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.20</version> </dependency>application.yml里的关键配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hospital.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case一定要开,这样数据库字段patient_name自动映射成Java属性patientName,不用手写一堆resultMap。log-impl配置成StdOutImpl后控制台直接打印SQL日志,调试期间别关掉,上线再关。Druid连接池我专门选了,它的监控页面能看活跃连接数和SQL执行耗时,对排查慢查询很有用。
4. Vue前端工程搭建与核心页面实现
4.1 开发环境配置与工程初始化
前端环境配置是新手重灾区。Node.js我用的16.x版本,对应npm 8.x,这套组合跑Vue 3 + Vite非常稳定。Node版本过高过低都可能出问题,太高比如18在某些老依赖下会有兼容警告。工程创建用的Vite,命令是:
npm create vite@latest hospital-web -- --template vue项目装依赖的时候,如果遇到node_modules装一半报错,先删掉整个目录再重新npm install,大部分情况是网络不稳定导致包下载不完整。装完依赖记得确认一下package.json里vue版本是3.x,不要装成2.x,两个版本的语法差别很大。
Element Plus是这个项目UI组件库的基础。后台管理的核心交互无非就是表格、表单、弹窗、菜单、分页,Element Plus全都有现成封装。安装命令npm install element-plus,然后在main.js里全局注册:
import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' import router from './router' const app = createApp(App) app.use(ElementPlus) app.use(router) app.mount('#app')4.2 路由配置与权限控制
前端路由这部分我用了动态路由方案。后端登录成功后,会返回该用户的角色和菜单权限列表,前端根据权限动态添加路由。这样医生登录看不到收费管理菜单,收费员操作不了医生工作站,权限控制在前后端双重生效。
路由表设计成两层:静态路由和动态路由。静态路由包括登录页、404页、首页,所有人可见;动态路由在登录后从后端拉取接口/api/menu/list,前端把返回的菜单数据格式化成Vue Router能识别的route配置,用router.addRoute()动态注入。菜单和路由共用同一份数据,左侧菜单栏根据当前路由表自动生成。
这里用到了Vue的插槽机制。我在封装菜单组件的时候,父组件需要定制菜单项的图标和名称,但又不想写死,就用插槽暴露接口:
<template> <el-sub-menu :index="menu.path"> <template #title> <el-icon><component :is="menu.icon" /></el-icon> <span>{{ menu.name }}</span> </template> <template v-for="child in menu.children" :key="child.path"> <sidebar-item v-if="child.children" :menu="child" /> <el-menu-item v-else :index="child.path">{{ child.name }}</el-menu-item> </template> </el-sub-menu> </template>菜单组件递归渲染,子菜单里还有子菜单就继续调用自己,这个写法在后台管理项目里非常实用。
4.3 Axios封装与跨域配置
axios请求统一封装在utils/request.js里,加上请求拦截器和响应拦截器。请求拦截器从localStorage拿token,有则加到Header;响应拦截器统一处理code,401跳转登录页,500显示错误提示。这段代码每个页面都要用,封装一次后面所有接口都省事:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/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) { return res.data } if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error => { ElMessage.error(error.message) return Promise.reject(error) } ) export default request跨域问题在开发阶段通过Vite的proxy配置解决。前端跑在5173端口,后端跑在8080端口,浏览器会拦截跨域请求。在vite.config.js里配置:
export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })所有以/api开头的请求都会代理到后端的8080端口,前端代码里不需要写完整域名的接口地址。这里有个细节:后端的Controller路由如果是/api/registration/list,那前端baseURL用/api,请求路径直接写/registration/list,不要重复拼接/api。
4.4 核心页面拆解:挂号与处方
挂号页面是前端交互最典型的例子。左边是患者信息表单,右边是当天医生排班列表。患者档案已经存在的,搜到后直接选中;新患者先填档案再挂号。医生排班列表通过科室下拉联动加载,选中医生后展示剩余号源。这个页面用Element Plus的el-form做表单校验,手机号正则、身份证号长度都做在前端,减少无效请求。
医生开处方页面用了一个动态表格来维护药品明细:左上角选药品,选中的药品自动加到下面的处方明细表格,可以修改数量和单价,表格底部自动汇总总额。这里有个有意思的实现:Vue的响应式数据渲染表格时,修改输入框的值不会立刻更新金额,需要在@change事件里手动重新计算。计算公式:
function calTotal() { const total = drugItems.value.reduce((sum, item) => { return sum + item.quantity * item.price }, 0) totalAmount.value = total.toFixed(2) }用reduce遍历所有明细,累加数量乘单价,最后toFixed(2)保留两位小数。注意前端只做展示用的金额计算,最终结算以后端计算的总额为准,防止有人通过修改前端请求绕过校验。
页面样式方面,后台管理系统不需要花哨的前端特效,干净、整齐、信息层次分明才是核心。Element Plus默认主题色是蓝色,我通过CSS变量覆盖的方式换成了一套偏医疗风格的绿色,同时把表格的stripe属性和border属性打开,数据多的表格加上height固定表头,操作体验会比默认样式好很多。
5. 从开发到部署的全流程跑通
5.1 本地启动:MySQL、后端、前端的启动顺序
整套系统的启动跑通,我总结出一个标准顺序,照着做基本不会出问题。
第一步,安装并启动MySQL。Windows环境下我推荐直接下载MySQL Installer图形化安装,选Server only,一路Next即可。安装完成后在服务管理器里启动MySQL服务,然后用命令行或者Navicat创建一个数据库:CREATE DATABASE hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,再导入项目里的hospital.sql脚本。导入完成后执行SELECT COUNT(*) FROM sys_user;能查到数据,说明数据库正常。
第二步,启动后端。用IDEA打开后端工程,等待Maven把依赖下载完,检查application.yml里的数据库用户名密码是否和本机一致。直接运行HospitalApplication.java的main方法,看到控制台输出Tomcat started on port(s): 8080就成功了。这一步报错最密集的就是数据库连不上,检查顺序是:MySQL服务是否启动、密码是否正确、连接URL里的数据库名是否存在、驱动版本是否匹配。
第三步,启动前端。进入hospital-web目录,先npm install,再npm run dev,控制台输出Local: http://localhost:5173/后在浏览器打开。输入后端提前建的测试账号admin/123456,能登录进入系统,说明前后端联调成功。
5.2 打包部署:jar包加静态资源的组合拳
开发环境跑通只是第一步,真正要交付给客户或者做项目展示,还得打包部署到服务器上。
后端打包很简单,在项目根目录执行mvn clean package -DskipTests,target目录下会生成一个hospital-0.0.1-SNAPSHOT.jar。把这个jar上传到服务器,执行java -jar hospital-0.0.1-SNAPSHOT.jar就能启动。生产环境建议用nohup后台运行:
nohup java -jar hospital-0.0.1-SNAPSHOT.jar > server.log 2>&1 &前端打包执行npm run build,构建产物在dist目录。把dist目录里的所有文件复制到Nginx的html目录下,配置一个location把请求转发给后端:
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:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html;这行是前端路由History模式的关键。它保证用户刷新/patient/list这个页面时,Nginx不会返回404,而是把请求重写到index.html,由Vue Router重新识别路由。
5.3 常见问题速查表
我把整个开发部署过程中最容易踩的坑整理成了表格,每个问题都加上排查思路:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 后端启动报Access denied for user | 数据库密码错误或权限不足 | 检查application.yml密码,确认root账号允许localhost访问 |
| 连接数据库报Public Key Retrieval is not allowed | MySQL 8.0驱动认证问题 | JDBC URL加allowPublicKeyRetrieval=true |
| 前端npm install卡住不动 | 网络问题 | 删除node_modules重新装,或配置npm源 |
| 前端请求接口报CORS错误 | 跨域未配置 | 确认后端addCorsMappings配置,或Vite proxy是否生效 |
| 登录接口能通,但业务接口401 | token未传递 | 检查axios请求拦截器Header配置是否正确 |
| 页面刷新后404 | 前端路由History模式+nginx未配置 | 添加try_files $uri $uri/ /index.html |
| MyBatis报Invalid bound statement | XML文件路径不对 | 检查mapper-locations和@MapperScan是否匹配 |
| 页面上中文全部乱码 | 字符集不统一 | 数据库连接URL加characterEncoding=utf8mb4,前端meta标签加charset |
5.4 这套项目后续还能怎么扩展
医院管理系统做完主干功能后,自然会有一些扩展方向。第一个是加Redis做缓存和会话管理,JWT虽然无状态,但用户权限变更、强制下线这种需求还是需要一个Redis来做黑名单控制。第二个是加文件上传功能,患者检查报告、病历图片这些都可以用MinIO或者本地存储对接。第三个是加报表统计,收费日报、药品消耗分析用ECharts做成可视化图表,再配上定时跑批任务生成Excel导出。
这些都是我实际接到过类似需求后总结的方向。框架搭得清晰,扩展就不会伤筋动骨,这也是前后端分离架构加分层设计的最大价值所在。
我个人在实际操作中的体会是,这类完整项目最大的学习价值不在某个单一技术点,而在于让你体会一套系统从需求到上线的完整链路。你会看到一张表结构设计是如何影响后续三个月开发的,会理解一个@Transactional注解背后是数据一致性的底线,也会明白为什么别人都说"部署一小时,踩坑半小时"。如果你拿到手只是把代码跑起来就放仓库落灰,那这套系统的价值你连十分之一都没拿到。我建议你把数据库脚本从头到尾看一遍,把SpringBoot的启动流程断点跟一遍,再自己尝试加一个"退费管理"页面,走完这轮,你才算真正掌握了这套前后端分离项目的精髓。