
上个月接了个医疗信息化的需求客户上来就问一周能不能把挂号系统跑起来我当时把之前沉淀的一套 SpringBootVueMyBatisMySQL 前后端分离智慧医疗服务平台骨架翻出来半天搭好环境三天改完定制功能第五天交付验收。这个项目就是这么来的它不是那种光讲概念的demo而是可以真实跑业务的基础工程。整套系统覆盖了患者端、医生端、管理后台三块患者注册登录、查看科室和医生排班、在线预约挂号、查询病历医生管理出诊排班、接诊写电子病历、开处方管理员维护科室、医生、号源规则。后端用 SpringBoot 做接口服务MyBatis 操作 MySQL 数据库前端 Vue 做单页应用前后端通过 RESTful API 通信用 JWT 维护登录态。不管你是刚学完 SpringBoot 和 Vue、想找个完整项目练手还是正在做毕业设计、接外包单子需要一套能快速交付的基础框架这份拆解都值得看完——我会把业务模块划分、数据库表设计、号源并发控制、token 权限链路、部署细节全部讲一遍顺便把我在实际交付中踩过的坑也交代清楚。1. 从挂号到问诊一个医疗服务平台的业务边界到底怎么划1.1 三个门户一套API我在规划这个系统的时候没有一上来就画几十张表而是先把用户角色盘清楚。医疗平台的用户天然分成三类患者、医生、管理员。围绕这三个角色业务边界就很清晰了。患者端负责的是就医前的信息获取和就医后的记录查看核心功能包括注册登录手机号注册 短信验证码密码走 BCrypt 加密存储科室导航按一级科室、二级科室浏览科室下挂医生列表排班查询查看某个医生未来一周的出诊时间、剩余号源在线挂号选择日期和时段锁定号源生成挂号订单就诊记录查看历史挂号记录、病历详情、处方明细医生端则是围绕接诊这个动作展开的排班管理按周设置出诊计划可以停诊、加号患者队列查看当天挂了号的候诊患者列表电子病历问诊结束后填写主诉、现病史、诊断结论处方开具开药、开检查检验单关联到对应的病历记录管理后台属于运营维护层面功能相对标准化科室信息维护、医生资质信息管理、号源规则配置每个时段放多少个号、是否开放预约、系统基础参数管理等。这三个门户可以拆成三个 Vue 前端工程也可以合在一个工程里用路由做角色区隔。我的建议是实际项目里拆成独立子应用更稳妥因为患者端可能后续要出小程序版医生端可能要做桌面版独立应用可以各自演进互不干扰后端 API 始终保持一套。1.2 前后端分离在医疗服务中的实际收益前几年很多人做医疗系统还在用 Thymeleaf 服务端渲染页面写在 Java 里前端改个样式还要后端重新打包。这个架构在业务简单时没毛病但医疗服务平台有一个特点多端并发。患者可能用手机浏览器访问挂号页医生用电脑浏览器打开接诊工作台管理员在大屏上看数据。三端交互差异非常大服务端渲染很难同时满足三种交互复杂度完全不同的场景。换到 SpringBoot Vue 的前后端分离架构之后最直观的收益有三点。第一前端工程化能力直接拉满。Vue 组件化开发让患者端的科室列表、医生卡片、排班日历可以被拆成粒度很小的组件复用Vuex/Pinia 管理患者登录态和购物车式的挂号信息开发效率比套页面模板高出一大截。第二部署和扩容各自独立。后端接口扛不住压力时可以单独多开几个实例前端静态资源丢 CDN 或者 Nginx 就行两边互不拖累。第三接口可以先于页面完成。我通常先定义好 RESTful API 文档后端和三个前端并行开工最后联调这在客户催得急的交付场景里是保命技能。1.3 做项目先学会砍需求这里想多说一句跟技术无关但比技术重要的事做医疗项目范围控制决定生死。医疗领域的需求可以无限叠加——在线支付、电子发票、云影像、远程问诊、药品配送每个听起来都合理但如果你把这些全部塞进第一版项目大概率烂尾。我拆这个项目的时候刻意做的很克制核心闭环只有这么一条患者注册登录 → 查看排班 → 在线预约 → 医生接诊 → 填写病历 → 开具处方 → 患者查看记录。这七个环节跑通了就是一个能交付、能演示、能应对答辩或验收的完整系统。像支付环节我建议用虚拟支付代替在订单状态里加一个待支付到已支付的流转后续接入微信支付宝只是换掉一个支付实现类的事情但第一版不用耦合第三方 SDK。消息通知可以用站内信不急着接短信平台。先把主干做扎实再做枝叶这是我这几年做项目最深的一条体会。2. SpringBootMyBatis后端这套技术组合在医疗场景下的选型逻辑2.1 选型对比与取舍这套技术栈网上争议不小经常有人问都 2025 年了为什么不用 SpringCloud为什么不用 JPA为什么不用 PostgreSQL我统一回答一下因为这类问题在医疗项目评审时被问到的频率极高。选型维度本方案常见替代方案选择理由服务架构SpringBoot 单体应用SpringCloud 微服务中小型医疗平台并发量有限单体部署运维成本低业务边界都在模块内后续确实需要拆再按模块拆分持久层MyBatisSpring Data JPA / MyBatis-Plus医疗报表和统计查询 SQL 复杂MyBatis 对 SQL 有完全控制力性能问题可以直接优化 SQL数据库MySQL 8.xPostgreSQL / SQL Server生态成熟云厂商支持好外包交付时客户环境大多有 MySQL8.x 的窗口函数、JSON 类型都够用前端构建Vue3 ViteVue2 WebpackVue3 的组合式 API 更适合中后台复杂交互Vite 构建速度比 Webpack 快一个量级微服务乍一听很高级但医疗平台里一个挂号接口和一个病历接口之间并没有独立的服务边界强行拆成多个服务只会引入分布式事务、服务发现、配置中心这些额外复杂度。SpringBoot 单体应用配合模块化包结构已经能覆盖九成以上的业务场景。MyBatis 对比 JPA 的核心差异在于JPA 让你面向对象编程但当你想优化一条多表关联的统计 SQL 时JPA 生成的 SQL 会把你绕晕。在医疗场景里数据查询的复杂度和准确性是第一位的MyBatis 这种把 SQL 握在自己手里的方式反而踏实。另外提一嘴现在 Spring 的版本节奏很快连 AI 组件都在出里程碑版本但生产项目我依然建议用稳定版本SpringBoot 2.7.x 对应 JDK8/JDK11SpringBoot 3.x 强制要求 JDK17。如果你在创建项目时发现版本太高导致一堆依赖不兼容记得先检查 JDK 版本是否匹配。2.2 目录结构与分层规范后端工程我按下面这种结构组织清晰且好扩展com.hospital.platform ├── config // 配置类如 WebMvcConfig、MyBatisPlusConfig ├── controller // 接口层只做参数接收和结果封装 ├── service // 业务层事务边界在这里 │ └── impl ├── mapper // MyBatis 数据访问接口 ├── entity // 数据库实体 ├── dto // 请求参数对象 ├── vo // 响应视图对象 └── common // 统一返回结果、异常处理、工具类这个结构最大的价值是约束了数据流向。Controller 不直接操作数据库Service 里写事务逻辑Mapper 只做 SQL 映射。Entity 对应数据库表结构DTO 承接前端请求参数VO 是返回给前端的数据视图。为什么要分开因为数据库字段不能直接暴露给前端比如用户表的密码字段、医生表的手机号字段在 VO 层要做脱敏和隐藏。统一返回结构我也给出一个约定所有接口统一返回 R 对象包含 code、msg、data 三个字段。配合全局异常处理器业务代码里只需要抛出指定异常前端就能拿到稳定的错误提示结构。实际开发时把 200 和 500 的定义统一写在常量类里避免前端同学每个接口问一遍这个 code 是啥意思。2.3 JWT拦截器登录态与角色权限的控制链路医疗系统的权限控制比普通管理系统敏感得多患者只能看自己的病历医生只能处理自己排班下的患者管理员才能维护基础数据。我用 JWT Spring 拦截器实现了一套轻量级权限链路完整流程如下。用户登录成功后后端从数据库查出用户信息生成 JWT token 返回给前端。token 的 payload 里包含用户ID、角色、过期时间签发密钥放在配置文件中用 HS256 算法签名。public String generateToken(LoginUser user) { return Jwts.builder() .setSubject(user.getId().toString()) .claim(role, user.getRole()) .claim(name, user.getName()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); }前端把 token 存在 localStorage每次请求在请求头带上Authorization: Bearer token。后端写一个拦截器继承HandlerInterceptorAdapter新版本实现HandlerInterceptor接口在preHandle里校验 token 合法性并把解析出来的用户信息放入 ThreadLocal 供后续业务代码取用。public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行白名单 if (isWhitelist(request.getRequestURI())) { return true; } String token request.getHeader(Authorization); // 解析失败直接返回 401由全局异常处理转换为统一结构 LoginUser user JwtUtil.parseToken(token); UserContext.set(user); return true; }白名单就三类登录接口、注册接口、获取验证码接口。剩下所有接口都要过 token 校验。角色权限的校验有两种做法轻量做法是在需要权限的 Controller 方法上加自定义注解RequireRole(ADMIN)拦截器里读取注解判断角色另一种是写专门的权限校验工具类在业务代码里显式调用判断当前登录用户角色和资源归属。我更推荐第二种因为医疗场景里水平越权很常见——比如一个患者尝试查看另一个患者的病历这种校验藏在方法内部比注解更直观。2.4 MyBatis实操中三个容易翻车的点2.4.1 缓存一级缓存和二级缓存该怎么用MyBatis 默认自带一级缓存和二级缓存但很多人根本不清楚它们的边界。一级缓存是 SqlSession 级别的同一个 SqlSession 内执行两条相同的 SQL第二次直接走缓存但 Spring 管理的 Mapper 每次调用都是独立的 SqlSession所以一级缓存实际帮助有限。二级缓存是 namespace 级别的可以在多个 SqlSession 间共享默认关闭。我的观点很简单医疗项目不要开 MyBatis 二级缓存。一旦系统部署多个实例每个实例的二级缓存各自独立数据库数据更新后其他实例读到的还是旧数据。挂号余号这类数据对实时性要求极高宁可每次查库也不要出现缓存不一致。真的有频繁读取且更新不频繁的数据比如科室列表放到 Redis 里统一管理控制权在你自己手里。2.4.2 单个数字字符比较的坑网上这个问题的讨论热度一直很高我也被坑过。Mapper XML 里写动态 SQL 判断状态常见的错误用法是这样的if teststatus 1 AND order_status #{status} /if在 MyBatis 的 OGNL 表达式里1会被解析成字符类型而status是字符串类型两者用比较永远为 false。正确写法是if teststatus 1 AND order_status #{status} /if外层用单引号、内层用双引号或者直接用.equals()方法判断。这个坑特别隐蔽因为代码能正常启动、SQL 能正常执行就是条件不生效查出来的数据跟你预期不一样。排查这类问题最有效的办法就是打印 MyBatis 执行的完整 SQL 和参数配置一行即可mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl2.4.3 批量写操作的正确姿势批量插入数据是医疗系统的刚需比如医生一次开十种药前端提交一个处方明细数组后端不可能循环单条 insert。MyBatis 的 foreach 标签可以解决insert idbatchInsert INSERT INTO prescription_item (prescription_id, drug_name, dosage, frequency) VALUES foreach collectionlist itemitem separator, (#{item.prescriptionId}, #{item.drugName}, #{item.dosage}, #{item.frequency}) /foreach /insert不过这里有两个细节。第一MySQL 连接串必须配置 rewriteBatchedStatementstrue否则 JDBC 驱动的批量写入不会走真正的 batch 模式性能提升有限。第二foreach 拼接的 SQL 长度有限制默认 max_allowed_packet 是 64MB一次批量插入控制在 1000 条以内比较稳妥超过就分批。我见过有人一条 SQL 拼了两万条数据直接把数据库干宕机的这种事故在医疗平台上线前发生一次就够了。3. 数据库设计与号源扣减预约挂号并发问题的处理方案3.1 核心表结构设计数据库设计是整个系统里最忌讳返工的部分表结构没设计好后面写业务代码处处难受。我按业务模块把核心表划分为用户权限域、排班挂号域、诊疗记录域三类。用户权限域不展开细说就是 user 表加 role 字段区分患者和医生医生和科室的绑定关系放在 doctor_info 扩展表里。重点看排班挂号域这是并发问题的集中地。医生出诊先要有排班表排班表决定了哪些日期、哪些时段可以挂号CREATE TABLE schedule ( id bigint(20) NOT NULL AUTO_INCREMENT, doctor_id bigint(20) NOT NULL COMMENT 医生ID, dept_id bigint(20) NOT NULL COMMENT 科室ID, work_date date NOT NULL COMMENT 出诊日期, start_time time NOT NULL COMMENT 开始时间, end_time time NOT NULL COMMENT 结束时间, total_count int(11) NOT NULL DEFAULT 0 COMMENT 总号源数, remain_count int(11) NOT NULL DEFAULT 0 COMMENT 剩余号源数, version int(11) NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0停诊 1正常, PRIMARY KEY (id), KEY idx_doctor_workdate (doctor_id, work_date), KEY idx_dept_workdate (dept_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表;挂号订单表记录每一次挂号的完整信息包括患者、排班、医生、就诊日期、状态。这里加了一个唯一索引uk_patient_schedule同一患者对同一排班不能重复挂号这是防重复下单的最后一道兜底。诊疗记录域的核心是 medical_record 病历表和 prescription_item 处方明细表病历表关联患者和医生记录主诉、诊断、处理意见处方明细表挂在病历下面存药品名称、剂量、频次。3.2 防超卖的三种方案对比与实现一个医生上午放 30 个号千万不能卖出 31 个。号源扣减本质上就是秒杀系统的库存超卖问题常见的解法有三种我对比一下在医疗场景的适用性。方案一乐观锁。SQL 里带上版本号或剩余数条件影响行数为 0 则失败重试或者提示号源不足。Update(UPDATE schedule SET remain_count remain_count - 1, version version 1 WHERE id #{scheduleId} AND remain_count 0 AND version #{version}) int deductQuota(Param(scheduleId) Long scheduleId, Param(version) Integer version);这个方案适合并发量不算极端的场景比如一个专家号同时也就几十个人抢乐观锁冲突率低性能损耗最小。方案二悲观锁。先SELECT ... FOR UPDATE锁住排班记录然后检查余号、扣减、生成订单最后提交事务。这个方案能保证绝对准确但并发高时会产生锁等待而且锁的粒度是整个排班记录如果某个专家号特别火所有患者都在这一行上排队。使用悲观锁必须让事务尽量短不要在锁内做远程调用或者耗时操作。方案三先下单占号延时释放。用户点击挂号时并不直接扣减号源而是创建一个待支付订单并冻结号源如果用户 30 分钟内未支付定时任务解冻号源。这实际上是对真实医疗流程的模拟——线下去医院挂号窗口会保留号源等你去交费。第一版建议用这个方案业务上更合理。不管选哪种方案前面说的唯一索引约束都要保留它就是数据库层面的最后一道防线。3.3 事务边界、索引设计与关键字段规范Transactional不是加得越多越好。我见过有人把整个挂号流程写成一个大事务扣号源、生成订单、发通知、扣积分全包在一起。这样做的代价是事务占用数据库连接的时间变长并发能力急剧下降。我的习惯是事务只包裹必须原子生效的写操作。扣号源和生成订单必须在一个事务里发站内信、调外部接口这些全部挪到事务外面用 Spring 的TransactionalEventListener监听事务提交后再执行或者直接放到消息队列里。索引设计上挂号平台的高频查询就两类患者查自己近期的挂号记录patient_id create_time患者查某个医生的排班doctor_id work_date。这两个联合索引必须建。病历查询通常按患者维度走patient_id单列索引即可。特别注意不要在索引列上做函数运算比如DATE(create_time) CURDATE()会让索引失效正确写法是create_time 2025-01-01 00:00:00 AND create_time 2025-01-02 00:00:00。金额、数量、号源等数字字段请一律用 DECIMAL 或 BIGINT不要用 FLOAT/DOUBLE避免浮点精度问题。时间字段统一用 DATETIME不要用 TIMESTAMP因为 TIMESTAMP 有 2038 年问题而且 DATETIME 在 MySQL 8.0 中支持的范围更广。4. Vue3前端与token会话管理患者端和医生端如何做到权限隔离4.1 环境准备与工程初始化很多同学卡在环境这关报了各种奇怪的错其实就是 Node 版本和工具链版本对不上。推荐统一使用 Node 16 或 Node 18 LTS 版本Vite 4/5 在这两个版本下都稳定。创建工程直接用官方脚手架npm create vitelatest hospital-frontend -- --template vue cd hospital-frontend npm install npm install vue-router4 pinia axios element-plus npm run dev前端工程结构上我习惯按业务模块拆分 views 目录router 目录里做路由配置和守卫api 目录按后端接口模块建独立 JS 文件utils/request.js 封装 axios 实例。这样一个中等规模的前端工程不会随着业务增长变成一坨浆糊。4.2 前端路由守卫与token处理token 处理是前后端分离项目里前后端协同的关键点。前端逻辑核心就三条请求前带上 token响应里遇到 401 清除登录态跳回登录页页面路由跳转前判断是否已登录。axios 封装这一段几乎是每个项目都要用到的模板// utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const service axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器自动携带token 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.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )路由守卫配合角色配置实现医生端和患者端的入口隔离router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.role to.meta.role ! role) { next(/403) } else { next() } })如果你是非登录状态能访问的公开页面比如科室导航、医生排班查询放行即可但是收费的预约挂号动作必须在守卫里挡住。后端在这一点上也必须有同样的校验前端只能提升体验不能作为安全边界。4.3 m3u8视频播放健康宣教和就诊引导现在的智慧医疗平台几乎都有健康宣教功能术前注意事项、康复指导、用药说明以视频形式展示给患者。我这次在系统里集成了 m3u8 格式视频的播放后端提供 m3u8 索引文件和 ts 分片文件前端用 video.js 直接播放。video refplayer classvideo-js vjs-default-skin controls/videoimport videojs from video.js import video.js/dist/video-js.css const player videojs(this.$refs.player, { autoplay: false, controls: true, fluid: true, sources: [{ src: this.videoUrl, // 例如 https://cdn.example.com/health/advice.m3u8 type: application/x-mpegURL }] })注意新版本的 video.js 已经内置了 HLS 解析能力不需要再额外安装 videojs-contrib-hls 插件。真正容易出问题的是服务端配置Nginx 里必须把 m3u8 和 ts 的 MIME 类型指对location ~ \.(m3u8)$ { add_header Content-Type application/vnd.apple.mpegurl; } location ~ \.(ts)$ { add_header Content-Type video/mp2t; }否则浏览器会把文件当成 text/html 解析播放器直接报错。4.4 打包后布局异常与404问题排查Vue 项目本地开发没问题一打包部署就白屏、样式错乱、跳转 404这个问题在热搜词里挂了很久我把它拆透。白屏和布局异常百分之八十是资源路径问题。Vite 默认 base 是/部署到服务器根目录没问题但如果放在子目录比如http://ip:8080/hospital/资源全变成http://ip:8080/hospital/hospital/...自然加载不到。处理方式是在vite.config.js里把 base 配置成./或用绝对路径/hospital/。路由 404 是 history 路由模式导致的。前端用createWebHistory()时浏览器直接访问/doctor/schedule后端没有对应文件就返回 404。解决方案是让 Web 服务器把所有路由 fallback 到 index.htmlNginx 配置就是一行try_files $uri $uri/ /index.html;。如果你不想管这些要么换 hash 模式URL 带#丑一点但是省心要么严格按照文档配置好 Nginx。我实际项目中全都用 history 模式加 Nginx 回退URL 干净也更专业。5. 从本地到云服务器整套系统的部署流程与Nginx配置5.1 本地拉起项目的完整步骤想让这套系统在本地跑起来按照下面顺序十步走完稳。安装 JDK 17如果用的 SpringBoot 3.x和 MySQL 8.0在 MySQL 中创建数据库hospital_platform导入项目提供的sql/hospital.sql修改后端application.yml里的数据库连接信息重点检查serverTimezoneAsia/ShanghaiMySQL 8.0 时区不对会报错后端工程根目录执行mvn spring-boot:run看到启动成功日志说明后端起来了前端工程执行npm install时间较长耐心等待npm run dev启动前端开发服务器默认端口 5173浏览器访问http://localhost:5173使用初始管理员账号admin/123456登录后台本地联调时最大的坑是跨域。前端在 5173 端口后端在 8080 端口浏览器会拦截跨域请求。开发环境我建议用 Vite 的 proxy 配置解决不需要后端开启 CORS// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/user/login会被 Vite 代理转发到后端浏览器看到的还是同源请求没有跨域问题。5.2 Linux服务器部署步骤生产环境部署我用的是最经典的三件套Nginx 托管前端静态页后端 jar 包用 nohup 后台运行MySQL 独立实例。以 CentOS 7.9 为例完整链路如下。先安装环境。JDK 用 tar 包解压安装配置/etc/profile环境变量MySQL 用 yum 源安装后初始化数据库、创建账号Nginx 用 yum 安装后修改配置。后端打包前确认生产环境配置。在application-prod.yml里覆盖数据库地址、日志级别、JWT 密钥然后打包mvn clean package -DskipTests # 产物在 target/hospital-server.jar启动后端nohup java -jar hospital-server.jar \ --spring.profiles.activeprod \ --server.port8080 /app/logs/hospital.log 21 前端打包上传npm run build # 把 dist 目录上传到服务器 /usr/share/nginx/htmlNginx 配置是部署的核心我给出一个可以直接抄的配置它同时解决了静态资源托管、API 反向代理、history 路由回退三个问题server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; # 前端路由回退 location / { try_files $uri $uri/ /index.html; } # API反向代理 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; } # m3u8视频流 location ~ \.(m3u8)$ { add_header Content-Type application/vnd.apple.mpegurl; } location ~ \.(ts)$ { add_header Content-Type video/mp2t; } }这里proxy_pass http://127.0.0.1:8080/;结尾的斜杠很重要它表示去除/api前缀后再转发。前端请求/api/user/login后端收到的是/user/login。如果你的后端context-path配置了/api那proxy_pass就需要去掉斜杠这两种方式二选一最忌讳的是两边都加前缀最后 404 找不到接口。5.3 部署期高频故障排查表部署过程中碰到的问题五花八门我梳理了出场率最高的几类用表格列出来方便你对照处理。故障现象根本原因解决方案浏览器白屏控制台提示 chunk 加载失败前端打包 publicPath/base 配置错误或部署目录与配置不一致检查 vite.config.js 的 base 配置上传 dist 到 Nginx 对应 root 目录前端能打开但接口全部 404Nginx 的 proxy_pass 转发路径和后端 context-path 不匹配对照 5.2 的两种代理方式确保前缀不冲突接口报 401token 过期、缺失或请求头名称不一致检查 axios 请求拦截器是否携带 Authorization后端拦截器白名单是否正确数据库连接失败 access deniedMySQL 用户授权或密码错误检查 MySQL 用户是否允许远程登录host 是否为%后端启动时报端口被占用8080 端口被其他进程占用netstat -tlnp接口报时区错误MySQL 连接串缺少 serverTimezone在 jdbc-url 加serverTimezoneAsia/Shanghai上传的图片/视频打不开静态资源路径或 MIME 类型错误检查 Nginx location 配置里的 root 路径补 add_header Content-Type排查部署问题的基本原则是自底向上先确认页面能不能打开再看接口通不通最后看数据库有没有数据。每层用 curl 验证不要一上来就瞎猜配置。6. 上线前必须处理的几个环节安全加固、性能优化与后续扩展6.1 医疗数据的隐私与安全要点医疗服务平台的用户数据属于高敏个人数据这个性质决定了安全意识要从开发第一天就建立而不是上线出问题才补救。密码存储必须用 BCrypt 这类自适应哈希算法每次加密的盐是随机的即使数据库泄露也无法批量逆向。千万不要用 MD5 或者 SHA1 直接存密码彩虹表分分钟把你打穿。接口返回值里的敏感字段要做脱敏。医生手机号在管理后台列表中应该显示成138****1234患者真实姓名在非必要场景用张*代替。这个可以在 VO 对象的 getter 方法里做或者用 Jackson 的自定义序列化注解统一处理。MyBatis 的 SQL 注入防护记住一条铁律能用#{}的地方绝对不用${}。#{}是预编译参数占位符MyBatis 会把它替换成?由 JDBC 驱动做参数绑定天然免疫注入而${}是字符串拼接仅用于动态表名、排序字段这类无法参数化的场景使用前必须做白名单校验。越权防护是医疗项目里最容易被忽略的安全漏洞。前端隐藏了删除病历按钮不代表接口安全攻击者完全可以直接调用后端接口。所以在每一个涉及资源操作的接口里除了校验登录态和角色还要校验资源归属权——医生只能操作doctor_id等于当前登录用户ID的病历患者只能查询patient_id等于自己ID的档案。这个校验写在 Service 层统一封装成工具方法避免每个 Controller 写一遍。6.2 高频接口的性能优化思路医疗平台性能优化的重点不是花式加缓存而是先把慢查询和无效请求解决掉。我的实践顺序是开 MySQL 慢查询日志找出执行时间超过 1 秒的 SQL用EXPLAIN分析执行计划检查是否走索引优化 SQL 或补建索引最后才考虑缓存。科室列表、排班日历这类读多写少的数据可以用 Redis 缓存设置合理的过期时间比如科室列表 30 分钟。但挂号余号、病历详情这种强一致数据不能缓存或者缓存时要有非常明确的失效策略。还有一个小细节列表接口必须分页前端不要一次性拉取所有数据。患者端历史挂号记录、医生端患者队列数据量会随着时间增长不分页的接口上线三个月后必崩。我建议在后端加一个简单 APIH 层记录每个接口的耗时超过 2 秒的自动告警。不需要引入复杂监控系统一个拦截器加一个日志表就够了但收益巨大——你会在用户投诉之前知道系统哪里变慢了。6.3 从单体走向演进哪些扩展值得做这套系统交付后如果要继续演进我的建议优先级是从业务痛点到技术升级先做预约提醒引入 ActiveMQ 这类消息队列做异步通知挂号成功、停诊通知都是典型的异步场景流量削峰的同时把耗时操作从请求链路里摘掉再做业务数据可视化大屏看板这类需求客户几乎一定会提复用现有接口做聚合统计就行然后可以考虑接入 onlyoffice 实现在线预览检查报告、处方单减少打印和线下流转视频问诊的话不是普通 Web 开发能搞定的建议直接用成熟的音视频服务。微服务拆分和容器化编排放到最后等真正出现某个模块需要独立扩展时再动不要为技术而技术。我在实际交付中反复验证过一件事把注册登录、挂号、接诊这条核心链路用最扎实的工程手段做透远比铺一堆炫技代码更有价值。这种项目客户不关心你用了什么中间件只关心系统能不能稳定跑、数据会不会出错、患者用起来顺不顺手。你如果准备拿这套骨架做二次开发也建议先别急着加功能开两个浏览器窗口分别以患者和管理员身份把完整流程走一遍你会在这一步发现很多写代码时根本看不出来的逻辑漏洞。把这些基础问题堵住项目的战斗力会提升一个档次。