
1. 项目概述与选题背景先说说这个项目是什么。SSM232药品电子商城在线诊断系统一句话概括就是用 SSMSpring SpringMVC MyBatis这套 Java 经典技术栈做一个既能在线上买药、又能在线问诊的综合平台。药品不再是普通的日用商品它牵扯到资质审核、处方管理、库存有效期管控还得接上在线诊断这条线让用户在买药之前先得到医生的专业判断整个系统的复杂度和业务逻辑比一般电商要高出一截。这个项目当时立项的出发点很现实线下药店买药要排队线上购药又存在乱买、滥用、拿不准该吃什么药的问题。把电子商城和在线诊断绑在一起等于在交易链路里插了一个专业审核环节——用户先描述症状医生或系统辅助规则给出建议再决定是否下单买药。这样一来系统不只是卖货它承担了初级的药学服务和健康管理职能这类项目在课程设计、毕业设计乃至中小型药企信息化改造中都非常常见。适合谁来看这篇博文如果你是刚学完 Java 基础、想找一个综合练手项目的在校生或者工作中需要快速搭一套 SSM 后台管理系统再或者你想理解电商加问诊这种复合业务怎么建模那我下面写的内容应该能帮到你。我会把框架整合、核心模块、数据库设计、踩坑排查一条线全部捋清楚很多细节是我自己实际开发中一步步试出来的不是那种网上抄来抄去的模板代码。2. 技术选型为什么依然是 SSM 三件套2.1 Spring整个系统的大管家SSM 里的 S 第一个是 Spring。在这套系统里Spring 承担的角色是容器管理和事务控制。药品商城的业务链路长从用户下单到库存扣减再到订单状态变更中间涉及多个 Dao 操作任何一个环节断了订单数据就会错乱。Spring 的声明式事务用Transactional就能把一组数据库操作包成一个原子动作这在订单创建、诊断记录保存这种场景里是刚需。我见过不少同学用 Servlet 写商城业务逻辑全堆在 Controller 里的对象全靠手动 new事务基本靠 JDBC 硬编码代码改一个地方能牵连三五个类。Spring 的 IoC控制反转解决的就是这个问题把 Service、Dao、DataSource 全部交给容器管理类与类之间的依赖通过配置文件或注解声明切换实现类、扩展新功能都不需要动业务代码。2.2 SpringMVC前后端交互的桥梁第二个 S 是 SpringMVC。它负责接收前端请求、调用 Service、返回视图或 JSON 数据。在线诊疗系统里前端页面要频繁向后端发起异步请求比如用户填完症状表单后要实时提交问诊单医生端要刷新待处理队列此时 SpringMVC 的ResponseBody Jackson 序列化能力就非常顺手一个 POJO 直接变成 JSON 返回给页面省去了手工拼接字符串的麻烦。SpringMVC 还有个核心价值是统一异常处理和参数绑定。比如药品下单接口前端传过来的购买数量可能是字符串、可能是空值通过RequestParam默认值和类型转换机制能把这些脏数据处理在入口处不至于让异常一路炸到数据库层。2.3 MyBatisSQL 与业务逻辑解耦第三个 S 是 MyBatis准确说是 MyBatis 的 Mapper 机制。药品商城的查询需求五花八门按药品分类筛选、按关键词模糊搜索、按照症状关联推荐、按状态查询订单列表。这些 SQL 写法和业务耦合度很高每次调整查询条件如果都在 Java 代码里拼字符串维护成本会直线上升。MyBatis 的动态 SQLif、where、choose让查询条件的组装变得非常灵活。举个例子药品列表页的分类筛选、价格区间筛选、关键字搜索这些条件用户可能只填其中几个用动态 SQL 拼条件比用 Java 代码逐层 if 判断要清爽得多而且 SQL 语句本身在哪里执行一目了然调优也方便。这里我多说一句选型心得。现在市面上 Spring Boot 加 JPA 或者 MyBatis-Plus 非常流行为什么我仍然推荐 SSM一个重要原因是SSM 是理解 Spring 生态底层运作逻辑的最佳教材。Spring Boot 帮我们自动配置了太多东西出了问题往往一脸懵而 SSM 的手工配置过程能让你亲眼看到每个 Bean 是怎么注册的、拦截器是怎么生效的、事务代理是怎么织入的。这套知识掌握了以后不管用什么框架排查问题的思路都会清晰很多。另外在不少高校和传统企业的存量系统里SSM 依然是生产环境的常见选择会这套东西就业面并不窄。3. 系统整体架构与功能模块规划3.1 分层架构设计我采用的是经典的四层结构表现层Controller、业务层Service、数据访问层Mapper/Dao、数据库层MySQL。层与层之间通过接口通信表现层不直接操作数据库业务层不关心前端传参格式。这种分层带来的直接好处是在线诊断和药品商城两个业务模块之间可以互相调用 Service 接口但不会出现类之间的循环依赖。比如诊断模块在生成建议后要调用药品模块的查询接口把推荐药品信息返回给前端这种跨模块协作在分层架构里就是普通的 Service 调用非常干净。系统还设置了一个全局的异常处理类用ControllerAdvice捕获所有业务异常、运行时异常和参数校验异常统一封装成约定格式的 JSON 返回给前端。前端拿到{code: 500, message: 库存不足}这种结构就能在页面上弹对应的提示而不是看到一个空白页或者一串堆栈信息。3.2 药品商城核心功能拆解商城主干我拆成了六个功能域用户中心、药品管理、购物车、订单管理、支付模拟、后台管理。用户中心注册、登录、个人信息维护、收货地址管理密码用 MD5 加盐后存储登录状态用 Session 或 Token 保持。药品管理药品的增删改查、上下架、价格调整。这里的药品分类不只是简单的树形结构还需要维护症状-药品的关联关系供在线诊断模块使用。购物车用户未登录也能加购存浏览器 LocalStorage登录后合并到服务端。这个设计虽然多写点代码但用户体验好很多。订单管理订单状态机从待付款到待发货到已完成到已取消每个状态流转都有明确触发条件和权限校验。支付模拟因为不接真实第三方支付我在订单表里加了一个支付状态字段再写一个模拟支付接口把状态置为已支付即可。重点是把支付回调的幂等逻辑做出来——同一个订单如果重复支付系统只能算一次。3.3 在线诊断模块功能设计在线诊断是整个项目的亮点也是最容易设计成花架子的部分。我把它设计成三条链路第一条是用户提交问诊单。表单包含基础信息性别、年龄、症状描述文本、既往病史可选项。提交后系统根据症状关键词自动做一次初筛打上建议线上问诊或建议线下就医的标签。第二条是医生处理。医生登录后台看到一个待处理问诊队列点进去查看用户描述给出诊断结论和用药建议。医生可以手写建议也可以从药品库中选择推荐药品系统自动生成关联记录。第三条是结果反馈。用户端可查看自己的问诊历史和处理状态问诊完成后能看到诊断结论页面同时展示推荐药品列表点击可直接跳转药品详情并加购。这就把问诊和开药在业务上打通了。我当时做这条链路时特别注意了状态管理。问诊单的状态字段有待分诊用户提交、待处理系统自动分配或医生接管、已完成医生提交结论、已取消用户主动撤销或超时自动关闭。每个状态变更都记录操作日志方便事后追溯。这个思路在后来的项目复盘中被证明非常关键因为在线问诊涉及专业医疗信息操作留痕是合规底线。4. 数据库设计与核心表结构实现4.1 用户与权限相关表数据库设计是整个系统的地基我在建表时秉持一个原则宁可多拆几张表也不要在一张表里堆字段。用户这块拆了三张用户信息表t_user、角色表t_role、用户角色关联表t_user_role。t_user的字段有用户ID、用户名、密码加盐哈希、真实姓名、性别、年龄、手机号、状态正常/禁用、创建时间。药品属于特殊商品平台必须知道购买者基础信息而且线上诊断需要年龄这个参数所以我在注册页面就要求填写而不是像普通电商那样等下单时再补。t_user_role表用user_id和role_id两个字段表示多对多关系。角色分三类管理员管理药品和订单、医生处理问诊、普通用户购药和咨询。登录拦截时我先查用户角色再根据角色决定能访问哪些接口。这里有一个开发中常犯的错误把角色直接硬编码在用户表里比如加一个role字段值为admin。这样看起来省事但后来要扩展角色权限时表结构得动代码也得动非常被动。4.2 药品与订单相关表药品信息表t_drug是最核心的表字段包括字段名类型说明drug_idint药品ID主键自增drug_namevarchar药品名称category_idint分类ID外键指向分类表pricedecimal(10,2)售价stockint库存数量prescription_flagtinyint是否为处方药0非处方 1处方expiry_datedate有效期manufacturervarchar生产厂家descriptiontext药品说明书/详情statustinyint上架/下架这里特别要说的是prescription_flag和expiry_date。处方药字段决定了商城是否允许用户直接下单如果不加这个字段系统在合规上就有硬伤。我在下单校验中写死了逻辑如果是处方药用户必须有一个已完成状态的问诊单且该问诊单的推荐药品包含该药品否则禁止下单。这个规则从数据库层面限制了非法购买。订单相关我拆成订单主表t_order和订单明细表t_order_item两表通过order_id关联。主表存订单号、用户ID、总金额、订单状态、支付状态、收货信息、下单时间明细表存药品ID、药品名称快照、购买单价、数量。药品名称和单价一定要冗余到明细表不能下单后靠drug_id去现查否则药品改了价格历史订单的对账就对不上了。4.3 在线诊断相关表在线诊断模块设计了四张表问诊单主表t_consultation、问诊详情表t_consultation_detail、诊断结论表t_diagnosis、症状-药品关联表t_symptom_drug。t_consultation存问诊单编号、用户ID、医生ID初始为空、症状描述、症状关键词通过分词提取后存储、紧急程度标记、状态、创建时间、完成时间。t_diagnosis存诊断结论文字、用药建议、推荐药品ID列表用逗号分隔的字符串或者单独再建一张关联表我偷懒用了前者但数据量大了还是建议拆表。t_symptom_drug是很有意思的一张表它把常见症状关键词如感冒、发烧、咳嗽和对应的非处方药关联起来。这张表的作用有两个一是在用户提交问诊单时做自动初筛把匹配度高的药品作为候选推荐写到页面上二是给医生处理问诊时做参考提高效率。当然这只是辅助规则最终判断权一定在医生手里系统不能替代人做医疗决策。数据库设计完下一步就是把这些表用 MyBatis 映射成 Java 对象然后一层层把代码写上去。我的经验是建表一定要先想清楚业务边界表间关联一旦定下来后面改成本极高。如果是在校同学做类似项目建议先画 ER 图把字段粒度敲定再动手能省一半返工时间。5. 核心功能实操从零搭建 SSM 框架5.1 项目环境与依赖配置我开发时用的环境是JDK 1.8、Tomcat 8.5、MySQL 5.7、IDEA 2019。这里提个醒不要一上来就装最新版 JDKSSM 是老技术栈JDK 8 和 Tomcat 8 的兼容性最稳我试过用 JDK 11 跑旧版 Spring 4 项目直接报 CGLIB 相关的错误排查半天才发现是版本兼容问题。创建的是 Maven 工程pom.xml里的核心依赖有dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version4.3.30.RELEASE/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version4.3.30.RELEASE/version /dependency !-- MyBatis及Spring整合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.4.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.2/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.47/version /dependency !-- 其他常用工具 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.1.10/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.9.8/version /dependency /dependencies版本号我故意选的是比较老但稳定的版本。SSM 框架的坑很多都出在版本匹配上尤其是mybatis-spring和 Spring 版本之间的兼容性。如果你用 Spring 5 配 MyBatis 3.4 的整合包运行期极容易出现 Mapper 注册不上的问题。保守选型、上线再说是这类老项目的第一原则。5.2 Spring 与 SpringMVC 整合配置SSM 整合的本质是三个配置文件Spring 根容器配置、SpringMVC 子容器配置、MyBatis 配置。初学者最容易懵的就是为啥要分两个容器简单理解就是Spring 容器管理 Service、Dao 等业务组件SpringMVC 容器管理 Controller 和视图解析器。SpringMVC 容器是 Spring 容器的子容器可以访问父容器的 Bean反之不行。所以我把 Controller 相关配置写在spring-mvc.xml把 Service、Dao 相关配置写在applicationContext.xml。applicationContext.xml里的关键配置!-- 开启注解驱动扫描Service和Dao -- context:component-scan base-packagecom.hospital context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan !-- 配置数据源 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/drug_mall?useSSLfalseamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ !-- 连接池初始化大小 -- property nameinitialSize value5/ property namemaxActive value20/ /bean !-- 配置SqlSessionFactory -- bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.hospital.entity/ /bean !-- Mapper扫描 -- bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.hospital.dao/ /bean !-- 开启事务 -- tx:annotation-driven transaction-managertransactionManager/spring-mvc.xml里的关键配置!-- 开启SpringMVC注解支持 -- mvc:annotation-driven/ !-- 开启静态资源放行否则CSS/JS会被拦截 -- mvc:default-servlet-handler/ !-- 配置视图解析器 -- bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /bean !-- 扫描Controller -- context:component-scan base-packagecom.hospital.controller/一个细节JDBC 连接串里的characterEncodingutf8必须显式写明否则中文写进数据库全是问号这个问题我印象太深了。还有 druid 连接池的密码我直接写在配置文件里这只是开发环境图省事生产环境一定要用配置中心或环境变量注入。5.3 MyBatis 映射文件与通用分页MyBatis 的 Mapper 接口和 XML 映射文件通过命名空间绑定。我建了一个BaseMapper思路把通用的增删改查抽出来每个业务模块的 Mapper 继承扩展。比如药品 Mapper 的查询代码select idselectDrugList parameterTypemap resultTypecom.hospital.entity.Drug SELECT * FROM t_drug where if testdrugName ! null and drugName ! AND drug_name LIKE CONCAT(%, #{drugName}, %) /if if testcategoryId ! null AND category_id #{categoryId} /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if if testprescriptionFlag ! null AND prescription_flag #{prescriptionFlag} /if AND status 1 /where ORDER BY drug_id DESC /select这里用map作为参数类型是因为筛选条件组合多变用 Map 传递天然支持可选字段。动态 SQL 的test表达式里要特别注意 null 和空字符串两个情况的判断我在实际开发中发现很多查询失效就是因为只判断了一个。另外price字段排序如果要用的话记得加索引数据量上去后药品列表的排序查询会明显变慢。分页我一开始是手写LIMIT加计算总条数后来发现代码重复严重就引了 PageHelper。引入方式很简单只需要在 MyBatis 配置里加一个插件bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean ... property nameplugins array bean classcom.github.pagehelper.PageHelper property nameproperties value dialectmysql reasonabletrue /value /property /bean /array /property /beanPageHelper 能自动拦截 Mapper 方法的执行生成 count 查询和分页 SQL页面代码里只需要调用PageHelper.startPage(pageNum, pageSize)后面紧跟的查询就会自动分页返回值用PageInfo包装。6. 商城核心业务逻辑实现6.1 注册登录与权限拦截用户注册时密码不能明文入库。我用的是 MD5 加盐盐值是用户名加一串固定后缀算出来的哈希存数据库。虽然 MD5 现在不算高强度算法但配合加盐在中小型项目中够用。真正要注意的是登录状态管理我采用了 Session 拦截器的方式public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { // AJAX请求返回JSON提示页面跳转则重定向到登录页 if (XMLHttpRequest.equals(request.getHeader(X-Requested-With))) { response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\message\:\请先登录\}); } else { response.sendRedirect(request.getContextPath() /login); } return false; } return true; } }这里有一个必须处理的细节拦截器里要区分普通页面跳转和 AJAX 异步请求。如果是 AJAX直接重定向到登录页是无效的前端拿到 302 也没法处理。我在前端统一封装了 axios 实例遇到 HTTP 状态码 401 就自动跳转登录页并提示用户。这个设计在前后端分离项目和传统 JSP 项目中都通用。角色拦截我是写了一个AuthInterceptor在注解里声明需要的角色方法执行前校验。比如医生提交诊断结论的接口标注RequireRole(doctor)普通用户访问就返回无权限提示。用注解比硬编码判断的代码可维护性好很多。6.2 药品浏览与购物车合并药品列表页支持分类导航、关键词搜索、价格排序。前端用 JSP 加少量 jQuery 实现点击分类标签时通过 AJAX 请求后端接口返回 JSON 后局部刷新列表区域。购物车的设计我前面提过未登录状态存 LocalStorage登录后把本地购物车数据 POST 到后端做合并。合并逻辑有个先后顺序问题先判断用户服务端购物车里已有的药品数量要叠加而不是覆盖本地购物车和服务器购物车都有同一种药品时以两者中数量较大者为基准避免用户困惑。药品库存校验在下单时做购物车阶段只做展示提示比如库存仅剩3件。这里我给一个建议购物车合并接口一定要做幂等设计。前端网络状况不好时合并请求可能被重复发送后端接口幂等就能保证重复提交不会导致数量翻倍。我的实现是在合并接口里加一个请求流水号数据库存唯一索引重复请求直接忽略。6.3 订单提交与库存扣减订单提交流程是商城里最容易出并发问题的地方。用户点击提交订单后后端要做的事按顺序是校验用户登录态、校验购物车不为空、校验药品状态和库存、计算总金额、冻结库存、生成订单主表和明细表、清空购物车。这串操作必须在一个事务里我用Transactional包住整个方法。库存扣减是重中之重。我先用乐观锁的思路实现了一个安全扣减UPDATE t_drug SET stock stock - #{quantity} WHERE drug_id #{drugId} AND stock #{quantity}这段 SQL 的关键是stock #{quantity}条件如果库存不足受影响行数为 0说明库存已被抢完事务回滚并抛异常。这里我用的是数据库层面的条件更新比先查后改的检查-再更新模式安全得多。先查后改在并发下会出现经典的超卖问题两个请求同时查到库存为 1都判断可买都去扣减结果库存变成 -1。我踩过并发扣减的坑当时的惨痛教训是把扣库存写在 Redis 里但数据库库存没有同步结果页面显示有货、下单却失败。后来统一为以数据库为准Redis 只做热点药品的展示缓存定时同步问题才彻底解决。订单创建完成后我还会做一次金额二次校验前端计算总金额只是展示后端必须从数据库读取单价再计算一遍防止恶意客户端篡改价格参数。7. 在线诊断模块的实现思路7.1 问诊流程设计在线诊断模块的流程设计我前后改过两版。第一版做得过于简单用户提交症状后直接生成诊断结果没有医生参与环节后来被否了原因很简单——这既不合规也不合理系统算法不具备医疗判断资质。第二版改成了用户提交 系统初筛 医生处理 结果反馈的完整流程。用户提交问诊单时前端表单要校验必填项症状描述限制在 20 到 500 字内。后端拿到问诊单后先对症状文本做关键词提取。我没有用复杂的 NLP 库而是维护了一份症状词典t_symptom_drug里的关键词字段用字符串匹配的方式找出命中词条再根据命中数量和紧急程度词如剧烈疼痛、呼吸困难判断紧急级别。紧急级别为高的单子直接打上建议立即线下就医的红色标记不再进入医生问诊队列这是系统最基础的安全兜底。医生端看到的是待处理队列按提交时间正序排列。医生点开详情页能看到用户填写的完整信息包括年龄、性别、症状描述、既往病史同时系统会把t_symptom_drug关联的候选药品列在侧边栏供参考。医生输入诊断结论选择推荐药品点击提交后状态流转为已完成用户端随即可以查看。7.2 诊断建议与用药推荐的数据流转诊断完成后系统生成一条t_diagnosis记录核心字段有诊断结论、用药建议文案、推荐药品ID列表。推荐药品和t_drug表通过drug_id关联前端展示时查出药品名称、价格、图片。用户点击加入购物车会触发前面提到的处方药校验逻辑非处方药直接加购处方药则校验当前问诊单是否包含该药品且状态为已完成满足条件才放行。这个处方药绑定问诊单的机制是整个系统合规性的体现。数据库结构上t_consultation表加了一个字段linked_order_item_id记录这条问诊单产生的购买明细这样后续追溯时能非常清楚地看到哪个用户、在哪个问诊结论下、买了什么药。如果没有这条关联链路监管审查时是要出问题的。用户端的历史问诊列表页面每次加载都查t_diagnosis表把用户ID等于当前用户的记录全部取出来按时间倒序展示。每条记录带状态标签已完成的可点击展开查看详细结论未处理的显示等待医生回复。这套数据结构非常简单但把所有核心链路都覆盖了。7.3 在线诊断模块的扩展空间我在做这个模块时想过很多扩展方向比如接入 WebSocket 实现医生和用户的即时沟通窗口把提问式问诊改成多轮对话引入规则引擎让系统根据更多维度的数据年龄、过敏史、已购药品给出更精准的药品冲突预警给诊断结果记录加上 PDF 导出功能方便用户保存。这些扩展方向在代码结构上都不需要大改因为问诊相关的表设计和 Service 接口已经按业务边界拆解好了这是我在设计阶段比较欣慰的地方。如果你要在自己项目里做类似功能我的建议是优先把状态机做扎实把医生端和用户端的角色权限边界划清楚其他功能都可以后期迭代。问诊模块最怕的就是状态混乱——用户已经看到结果了医生端还显示待处理这种问题一旦发生会让使用者对整个系统丧失信任。8. 常见问题与排查技巧实录8.1 SSM 整合期的拦路虎SSM 项目在搭建阶段最容易碰到的问题我按出现频率排一下Mapper 接口找不到实现启动报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)。八成是 Mapper 接口和 XML 映射文件的 namespace 不匹配或者 XML 没有被扫描到。检查mapperLocations配置的路径是否正确XML 文件是不是放在了 resources 目录下。SpringMVC 静态资源 404页面 CSS、JS 全部加载不出来。在spring-mvc.xml里加mvc:default-servlet-handler/就能解决。事务不生效方法上加Transactional但数据还是只写了一半。检查两点配置里是否加了tx:annotation-driven方法是否被同类内部调用——Spring 的声明式事务是通过 AOP 代理实现的同类调用不会走代理事务自然失效。页面中文乱码过滤器配置一个CharacterEncodingFilter强制 UTF-8 编码。同时数据库连接串加characterEncodingutf8JSP 页面头部设置pageEncodingUTF-8。三层任何一层缺了中文都会出问题。8.2 商城并发与数据一致性排查订单库存扣减是我项目中发生问题最多的环节。除了前面说的乐观锁方案还有几个配套手段下单成功后立即把购物车标记为已处理cart_status字段防止用户反复提交同一份购物车数据订单表加一个唯一索引user_id order_snapshot数据库层面杜绝重复订单。排查并发问题有个通用思路用压测工具模拟多用户同时下单观察库存数据和订单数据。我用的 JMeter 开了 100 个线程同时下单同一药品压完后查库存、查订单数、查明细表中药品总量是否对得上。对不上就说明数据一致性出了问题然后看日志里有没有乐观锁更新失败记录逐条定位是哪个环节丢了。压测期间我发现一个隐蔽问题订单创建时如果用了SELECT ... FOR UPDATE锁行药品库存更新阻塞时间会明显变长导致用户体验卡顿。后来分析发现药品详情查询不需要锁只有扣减库存才需要所以我把锁的范围从整个事务缩小到扣减那一条 SQL吞吐量直接从每秒 30 单提到了每秒 80 单左右。8.3 在线诊断模块的典型问题问诊模块出现过一次状态不同步的问题医生端提交诊断结论后数据库状态已改为已完成但用户端页面一直显示等待回复。排查后发现是 Redis 缓存了问诊单对象用户端接口优先读缓存但医生端写完数据库后没有主动更新缓存导致双方数据不一致。解决方法是保存诊断结论的方法里事务提交后同步删除相关缓存 key用户端读取时缓存不存在就会回源数据库问题消失。这个案例也提醒我缓存不是摆设缓存失效策略和更新链路要一起设计。另一个问题是超时自动关闭。我原本计划写一个定时任务每分钟扫描超过 48 小时未处理的问诊单自动置为关闭状态并通知用户。最开始用 Spring 自带的Scheduled实现但部署到 Tomcat 后发现执行时间漂移严重排查原因是配置了多个 Spring 容器导致任务重复注册。后来把定时任务单独放到一个配置类里并且在 web.xml 里指定只加载一次才彻底解决。8.4 开发效率与团队协作经验最后一个部分是协作相关的心得。项目代码目录我是按模块划分的controller、service、dao、entity、dto、util每个包下面再按业务域分子包user、drug、order、consultation。接口命名统一规范查询方法一律selectXxx更新方法一律updateXxx插入方法一律insertXxx删除方法一律deleteXxx大家看代码不用猜。另外我在每个 Service 实现类里都加了一行注释业务目标和注意事项比如提交订单的入口必须带上完整的收货地址前端不传则默认使用最近一次地址。数据库脚本我也养成了用版本管理的习惯每次表结构变更都生成一个带日期前缀的 SQL 文件建表和初始化数据分开。这样不管是在本地、测试环境还是生产环境数据库结构都能保持一致不会出现本地能跑、部署到服务器就报字段不存在的尴尬。写在最后的实际操作体会这个项目前后我完整做了大概一个半月中间经历过大大小小几十个问题大多数是细小但致命的点拦截器漏掉了 AJAX 请求、MyBatis 参数没加Param导致空指针、事务方法内部自己调用自己导致回滚失效、数据库表字段用了order这种保留字导致 SQL 报错。如果你也在做 SSM 相关的系统我建议你特别留意三点第一框架整合是一切功能的前提整合阶段宁可慢一点、每一步都启动验证也不要一口气写完所有配置再调试问题定位的成本会翻倍第二涉及钱和库存的业务必须把数据库一致性放在第一位前端的展示问题都可以容忍数据错了不能忍第三在线诊断这类涉及专业领域的模块功能设计一定要考虑合规和边界系统只能做辅助不能替代专业人员的判断把这个理念贯穿到代码里你的项目才算真正立得住。最后再分享一个小技巧SSM 项目跑起来之后强烈建议在本地配置一个 MyBatis SQL 日志打印。只需要在日志配置里把com.hospital.dao的级别调到 DEBUG控制台就能看到每次执行的 SQL 和参数。这个习惯帮我在排查问题上省了至少一半时间对比前端传参和后端实际执行的 SQL很多数据异常一眼就能看出来原因。希望对你有用。