SSM框架+Java:互联网+护理服务平台的设计与实现
在互联网浪潮里“网约护士”算是一个很典型也很有价值的落地场景。患者不用往医院跑在手机上选服务、约时间专业护士上门完成护理。这个项目我刚完整做完一遍技术栈就是题目里的那套SSM框架加Java从需求分析、数据库设计到核心订单流程、并发接单再到部署上线整个过程踩了不少坑也沉淀了一些值得讲的经验。这篇文章我就把这套“互联网护理服务平台”的设计与实现完整拆开讲重点放在业务闭环怎么走通、并发和事务怎么处理、以及SSM项目里那些文档里不会写的细节。如果你正准备做同类平台或者是用SSM做毕业设计、个人项目这篇文章可以直接当参考。1. 系统整体需求拆解护理服务平台到底要做什么很多人一上来就着急建表、写代码结果做着做着发现业务逻辑对不上最后只能反复删表改代码。做这种平台型系统第一步一定不是技术而是把“谁在用、用起来是什么流程”想清楚。1.1 角色模块划分患者、护士、运营三条线护理服务平台本质上是连接“有护理需求的患者”和“有上门服务能力的护士”的桥梁。系统里的角色不能拍脑袋定业务主线不同模块划分就完全不同。我把这个系统拆成了三条业务线。第一条是患者线。患者注册登录后浏览服务项目比如打针、换药、导尿护理、压疮护理、新生儿护理、术后康复选好服务内容和服务时间在线下单支付后等待护士接单。服务完成后患者对本次护理进行评价。第二条是护士线。护士注册时需要提交执业证书、职称、工作经历等资质材料由平台管理员审核。审核通过后护士可以看到待接单的订单列表根据自己的时间安排和位置抢单。上门完成服务后确认订单完成。第三条是运营线也就是管理员后台。管理员负责审核护士资质、管理服务项目分类和价格、处理投诉和订单异常、查看订单统计和收入报表。整个平台的运转后台这一层是核心因为护理服务不同于普通商品交易护士资质审核必须人工介入而且平台对服务过程要有监管能力。这三条线对应到模块上就是患者端App/H5、护士端App/H5、管理后台Web。如果用一套SSM项目来承载前端可以分三个入口后端共用一套Service和DAO只是Controller层的路径和权限拦截不同。这么设计最大的好处是业务规则只维护一份比如订单状态流转规则、护士审核逻辑都只写在Service层不会出现多端逻辑不一致的问题。1.2 为什么选SSM而不是Spring Boot不是不会是学得透很多人一看到2025年了还在用SSM就下意识觉得技术老旧。但说实话从学习和项目落地的角度SSM一点都不过时。Spring Boot说白了是Spring的自动化配置用起来方便但大量细节被封装了。SSM则把Spring的IoC容器、SpringMVC的请求映射和参数绑定、MyBatis的SQL管理都明明白白暴露在你面前。比如在SSM中你要自己配置Spring和SpringMVC的父子容器、配置事务管理器、配置MyBatis的SqlSessionFactoryBean还要处理DispatcherServlet拦截路径和静态资源放行之间的微妙关系。这些在Spring Boot里基本上一个starter就搞定了但如果你从来没有亲手配置过就永远不会理解为什么Spring Boot里会有那些“约定优于配置”的设计。我的建议很直接如果是为了快速交作业用Spring Boot当然省事但如果你想把Java Web的底层吃透就老老实实做一遍SSM。而且SSM项目并不是不能用于生产配合Tomcat、Druid连接池、Redis缓存完全能满足中小型平台的需求。这个护理服务平台的核心是业务闭环不是拼框架版本SSM足够支撑。2. 数据库设计核心表结构和订单状态流转数据库设计是整个系统的地基。地基打不好后面每写一个功能都在难受。我设计这个系统时始终把握一个原则订单是中心护士和患者是两端支付和评价是闭环的最后两块拼图。2.1 用户与护士基础表一张表还是两张表患者和护士虽然都是平台的“用户”但他们除了登录认证之外属性差异非常大。患者需要的是健康档案、常用地址护士需要的是资质证书、执业编号、审核状态、评分、接单量。如果强行放在一张用户表里要么护士的专属字段大量为空要么患者的扩展字段无处安放。我的方案是拆成t_user和t_nurse两张表t_user存公共的账号信息手机号、密码、昵称、头像、角色类型t_nurse通过user_id外键关联存护士专属信息。这么设计还有一个好处权限控制和护士资料审核是分开的。比如护士的账号被禁用只需要改t_user的status字段而护士的资质到期只需要修改t_nurse的audit_status两者互不干扰。护士资质审核在表里要有对应的字段我的t_nurse表核心字段大致是这样id主键自增user_id关联用户表certificate_no执业证书编号level护士职称初级/中级/高级hospital所属医院名称work_years工作年限audit_status审核状态0待审核1通过2驳回score综合评分默认5.0total_orders累计接单次数service_status护士在线/离线状态控制是否展示给患者其中service_status这个字段容易被忽视。刚开始我设计的护士端没有上下线概念所有通过审核的护士都能看到订单结果有的护士明明在休息平台还把订单推给她。后来加了这个字段护士端登录后默认离线自己点击“开始接单”才进入可接单状态这样既尊重护士个人安排也避免无效推送。2.2 订单主链路表从下单到支付到服务完成订单表是整个系统的核心。先看我最常用的订单表结构再解释每个字段为什么存在CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单编号, user_id BIGINT NOT NULL COMMENT 下单患者, nurse_id BIGINT DEFAULT NULL COMMENT 接单护士, service_item_id BIGINT NOT NULL COMMENT 服务项目ID, service_time DATETIME NOT NULL COMMENT 预约服务时间, address VARCHAR(200) NOT NULL COMMENT 上门地址, contact_name VARCHAR(50) NOT NULL COMMENT 联系人, contact_phone VARCHAR(20) NOT NULL COMMENT 联系电话, amount DECIMAL(10,2) NOT NULL COMMENT 订单金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0待接单 1已接单 2服务中 3待确认完成 4已完成 5已取消 6已退款, remark VARCHAR(500) DEFAULT NULL COMMENT 备注比如病情描述, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 支付状态, create_time DATETIME NOT NULL, update_time DATETIME NOT NULL );订单状态我用了一个数字状态位这比用字符串更省空间也方便索引。但数字状态位有个问题新人看代码时根本不知道3代表什么。所以我把状态定义成一个常量类OrderStatus每个状态注释写清楚后端判断时用常量比大小写字符串更安全。这里有一个非常重要的业务设计订单金额amount必须由后端根据服务项目表计算绝不能信任前端传来的价格。因为前端数据可以被篡改如果接口直接按前端金额生效支付金额就和实际订单金额对不上。我会在后端根据service_item_id查出单价和时长再结合优惠计算最终的amount。订单状态流转是这个系统最容易写乱的地方。我现在把完整流转列在这里供你参考状态值状态含义允许的下一步操作触发动作0待接单护士抢单患者取消状态改为1或改为51已接单护士开始服务患者取消状态改为2或改为52服务中护士确认完成服务状态改为33待确认完成患者确认收货完成状态改为44已完成患者评价生成评价记录5已取消无无6已退款无触发退款流水为什么会在“已接单”和“已完成”之间塞一个“待确认完成”这是借鉴电商订单的“确认收货”逻辑。护士上门做完护理后不能直接自己确认完成必须要等患者确认或者超时自动确认。护理服务的服务对象是患者的身体如果不设置患者确认环节护士提前点了完成一旦后续出现纠纷平台就没有中间状态可以介入。后来我在实际操作中发现患者确认时间是评估平台服务质量的一个关键数据点这个状态加得非常有必要。3. 核心功能实现下单、抢单、支付和评价闭环这个系统最核心的业务流是患者浏览服务项目选择时间地址下单支付护士接单上门服务双方确认评价。这中间有两个技术难点最考验功底一个是护士抢单的并发控制另一个是支付回调的幂等处理。3.1 患者下单与护士抢单一次只能有一个人成功患者下单的逻辑相对简单生成订单号插入订单记录状态设置为待接单然后把这个待接单信息推送到护士端。这里订单号我采用的是“时间戳随机数”的生成方式比如yyyyMMddHHmmss加6位随机数保证不重复即可。护士抢单才是真正的硬骨头。试想一个场景平台把订单推给20个符合条件的护士20个人同时点“接单”数据库只有一个订单记录谁能接如果用select查询订单状态判断是待接单再update这个两步操作之间存在时间差多个护士可能同时看到待接单订单然后一起更新成功最后订单就被多个护士“认领”了。解决办法是用数据库行级锁或者乐观锁把“查询并更新”变成一个原子操作。我采用的做法是直接执行一条带条件的UPDATE语句Update(UPDATE t_order SET nurse_id#{nurseId}, status1, update_timeNOW() WHERE id#{orderId} AND status0 AND nurse_id IS NULL) int acceptOrder(Param(orderId) Long orderId, Param(nurseId) Long nurseId);这个更新语句的条件里加上了status0 AND nurse_id IS NULL数据库的行锁会保证同一时刻只有一个护士的UPDATE能匹配到这条记录影响行数为1。其余的护士影响行数为0就可以告诉他们“手慢了订单已被抢走”。这样还不够。因为护士端A和护士端B可能同时抢同一个订单A成功B失败。但B失败后返回的信息不该只是“抢单失败”而应该让护士看看是不是自己“今日接单次数已满”或者“当前有未完成订单”。所以我在护士端还加了一层业务约束一个护士同时最多只能有一个待服务订单。这个判断我放在Service层先用select查护士当前订单数如果大于等于1就直接返回“请先完成手头订单”避免护士累死。最后说一个实战体会抢单业务不要用Redis分布式锁除非你明确知道锁的粒度怎么设计。一个简单的数据库条件UPDATE就能解决99%的抢单并发问题而且代码量最少最容易理解。过度设计在这个场景没有意义。3.2 支付回调与订单确认幂等处理是底线支付模块是这个系统里另一个容易踩坑的地方。如果是真实商用一般对接支付宝或微信支付如果只是学习演示可以做一个本地模拟支付通道用一个充值余额表来实现支付逻辑。我的做法是架了一个模拟支付宝沙箱的支付通道核心逻辑是患者点击支付时后端生成一个支付单保存支付参数跳转到收银台用户输入密码后支付平台异步回调一个通知地址notifyUrl。后端收到回调后要先验签然后更新订单状态。这个场景里最典型的问题是重复回调。支付平台出于可靠性考虑会多次回调同一个通知地址可能是3次、5次甚至更多。如果每次都执行“更新订单为已支付”第一次把待接单改成已支付第二次再改可能就造成重复更新或者状态错乱。我的解决方案是幂等校验。在更新之前先根据订单号查询当前订单的支付状态如果是已支付就立即返回成功不再做任何更新操作。更进一步我把支付流水单独抽了一张t_payment表每个订单只能生成一条有效支付流水回调处理里先检查流水是否存在存在就幂等返回。还有一个细节必须提醒回调接口是支付平台主动调用你的服务器所以这个接口不能被前端的登录拦截器拦截也不能做session校验。SSM里配置拦截器时一定要记得把/pay/notify这类路径放行。我在第一次做这个项目时忘了放行回调地址结果支付成功后状态死活不更新排查了半天才发现是拦截器把支付平台的回调请求挡在了外面血泪经验。3.3 服务评价与后台统计让数据形成闭环护士完成服务患者确认后系统会提示患者进行评价。评价里包含两个核心字段服务评分1到5分和评价内容。评分直接影响护士列表里展示的综合评分。我计算护士综合评分时并没有简单取平均值而是采用了类似“热度加权”的方式综合评分 (历史评分总数 * 历史平均分 本次评分) / (评分次数 1)说白了还是算加权平均但优势在于每次新增评分时只需要读护士表的score和comment_count两个字段不需要重新算历史所有评价。在订单量大的时候这个更新成本差别很明显。后台统计这一块我做了一个简单的订单看板按月汇总订单量、成交量、成交金额、取消率、平均服务时长。这里对新人最容易翻车的地方是SQL的日期函数。MySQL里统计某月订单量不要用like 2025-05%这种字符串匹配要用范围查询SELECT COUNT(*) FROM t_order WHERE create_time 2025-05-01 00:00:00 AND create_time 2025-06-01 00:00:00 AND status 4;范围查询才能命中索引字符串like匹配在数据量上来后会很慢而且容易把2025-05-01和2025-05-10这类数据搞混。4. 踩坑实录SSM项目里我遇到的典型问题与排查方法这部分我把实际操作中碰到的问题整理成一份速查表并挑几个重点展开说。这些问题在网上搜的时候答案都很零散但几乎每个做SSM项目的人都会遇到。4.1 MyBatis动态SQL里Integer型条件判断为0的坑写动态SQL时我一开始判断状态是否为空时用了这样的写法if teststatus ! null and status ! AND status #{status} /if结果发现当status传入整数0时这个条件竟然不生效查出来的数据不是待接单订单。原因是MyBatis的OGNL表达式里Integer类型的0会被当作false处理status ! 这个判断默认认为0等于空字符串。解决办法是去掉对空字符串的判断只判断nullif teststatus ! null AND status #{status} /if这个问题非常隐蔽因为传1、2、3时都正常只有传0时数据查不出来。排查的时候可以把MyBatis的SQL日志打开看实际执行的SQL里到底有没有拼接上条件。4.2 PageHelper分页失效startPage后面跟的不是查询这个坑我踩过两次。PageHelper的使用规则非常严格PageHelper.startPage(pageNum, pageSize)后面必须紧跟且只能跟一个Mapper查询方法中间不能有任何其他SQL操作。有一次我在startPage和查询之间调用了一个查询护士信息的Mapper方法结果分页就串到了那个无关的查询上订单列表完全不按预期来。还有一次更隐蔽两个Mapper查询之间夹了一个Service方法Service方法内部又调用了别的查询分页就被消耗掉了。排查这个问题时最直接的办法是在日志里看PageHelper输出的count查询属于哪个表基本一眼就能定位。4.3 Spring事务自调用失效在Service层写方法时如果方法内部用this.xxx()调用同类中的另一个方法而这个被调用的方法上标注了Transactional事务是不会生效的。因为Spring的事务是通过AOP代理实现的this直接调用的方法绕过了代理对象。我在实现“护士接单并更新接单数量”时把两个操作写在同一个类里结果出现了一个订单被接了两次、但护士接单数只加了一次的bug。解决思路有两个一是把事务方法拆到另一个Service类中通过注入调用二是注入自身的代理对象用selfProxy调用。如果不想引入额外的依赖用TransactionTemplate手动开启事务也是稳妥的办法。4.4 上传的图片无法访问资源映射问题护士上传资质证书、头像时文件会保存到服务器的某个目录比如/home/upload。但页面访问图片的URL通常是http://localhost:8080/upload/xxx.jpgTomcat默认并不会把/upload映射到磁盘上的/home/upload目录。所以图片上传成功后前端就是显示不出来。SSM中要让这个访问路径生效有两种办法。最简单的办法是在SpringMVC配置里写一个资源映射mvc:resources mapping/upload/** locationfile:/home/upload//这样/upload/开头的请求就会被映射到磁盘目录。另一个办法是部署到Tomcat时修改server.xml在Host节点下配Context把/upload这个访问路径指向物理目录。开发环境用第一种服务器上我用的是第二种因为这样更直观。4.5 日期格式的坑前后端交互报400错误以下是我整理的几类典型问题快速参照表问题现象根本原因解决办法订单状态0查询不出来MyBatis中Integer 0被OGNL当成falseif只判断null不判断空字符串分页数据不对count了别的表startPage和查询之间夹杂了无关操作startPage后面只跟一个Mapper查询接单时事务只执行了一半同Service内部自调用事务代理失效拆到不同Service或使用TransactionTemplate图片上传成功但页面404静态资源没有映射到磁盘目录配置mvc:resources映射外部目录前端传2025-05-01后端报400SpringMVC默认无法解析该格式Controller参数加DateTimeFormat注解金额计算float出现0.30000004浮点数精度问题金额统一用BigDecimal或整数分日期问题多说一句。前端传2025-05-01 10:00:00这种带时间的字符串时SpringMVC默认解析不了必须在参数上加DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss)。如果返回给前端的日期格式也不对可以在SpringMVC配置里统一设置Jackson的日期格式这样所有接口的date字段都会按指定格式输出省得在每个实体类上写注解。4.6 重复提交问题防重设计患者下单时如果重复点击“提交订单”会产生多条订单。我的方案是在下单接口入口加了一个Redis缓存判断以用户ID为key设置一个很短比如3秒的防重锁。如果Redis还没有熟数据库层面也可以在t_user和t_service_item和create_time的组合上做唯一索引或者在下单时先按“待支付订单且创建时间在10分钟内”查询有则直接返回已存在的订单号。5. 部署上线从war包到Tomcat的完整流程这个系统的开发环境是Windows生产环境我部署在一台Linux服务器上。整个部署过程里最需要留意的问题不是代码本身而是环境和配置的一致性。5.1 打包和环境配置SSM项目一般用Maven管理依赖打包成war包。在pom.xml中我配置了打包方式为war。用Maven命令mvn clean package会在target目录下生成xxx.war。将war包放到Tomcat的webapps目录启动Tomcat它会自动解压部署。这里需要高度注意的是JDK版本匹配问题。如果项目用JDK8编译而服务器上的Tomcat运行在JDK11或更高版本有时候会出现兼容性问题。我一般保证开发环境和服务器环境的JDK大版本一致都用JDK8。Tomcat版本我选择8.5系列它和JDK8配合比较成熟。数据库连接池方面开发阶段我用了c3p0或DBCP部署到服务器上我换成了阿里巴巴的Druid因为它有SQL监控页面上线初期排查慢SQL非常方便。jdbc.properties里的数据库连接地址、用户名、密码是部署时改动最多的一定不要硬编码在代码里所有这些放在配置文件中打包时用不同环境的配置文件覆盖。5.2 上线后的几个安全与运维细节上线前我做了几次检查总结成下面这几条每一件都是实际遇到过问题的第一把Tomcat的默认端口从8080改成了80这样用户访问不需要在URL里加端口号。改端口是修改server.xml中Connector的port属性。同时在服务器防火墙的安全组里放行80端口注意如果是云服务器普通CentOS防火墙关闭或者开放对应端口不同环境操作路径不同先确认清楚。第二Tomcat部署时默认会开启管理后台如果不需要一定要在conf/tomcat-users.xml里删除或注释掉manager相关的账号。这类后台管理页面一旦暴露很容易被爆破攻击而SSM项目又是学生和中小团队最容易忽略这一点的。第三护士资质上传的证书图片包含个人隐私生产环境一定要做权限控制。我的做法是管理后台审核页面的图片接口单独加了管理员权限校验患者端和护士端不能直接访问他人的证书原图。开发时为了方便直接暴露上传目录上线前必须改掉。第四数据库字符集统一使用utf8mb4。护理场景里患者备注可能写各种生僻字utf8mb4比utf8多覆盖了表情符号等字符一次到位省得后续数据库迁移。建库时执行CREATE DATABASE xxx DEFAULT CHARACTER SET utf8mb4;即可。6. 我做完这个项目后最想提醒你的三件事第一件事数据库设计阶段多花两天时间后面能少加两个星期班。我在这个项目里中途改过两次表结构一次是给护士加service_status字段一次是给订单加pay_status字段。每次改表都要同步改实体类、Mapper、Service、前端页面牵一发而动全身。我第一次做的时候急着写代码结果后期返工的时间远超省下来的那两天。第二件事不要把业务判断写在Controller里。Controller只负责参数接收和结果封装所有的业务逻辑写Service。这个原则在SSM项目里尤其重要因为SSM没有Spring Boot那种复杂的高级特性一旦业务代码散落在Controller层后期连事务边界都控制不住。第三件事项目做完后重新配一遍环境再走一遍全部流程。我在上线前做的最后一件事是删掉本地数据库重新导入初始化脚本从零开始注册患者、注册护士、审核资质、下单、接单、支付、评价完整跑通了一个闭环。这个流程帮我发现了几个只在“全新数据库”下才会出现的问题比如初始化数据缺失、某些查询没有默认排序、图片路径写死等。新环境所有条件都是最严格的新环境能跑通老环境基本就没问题。这个护理服务平台项目的核心收获并不是SSM框架本身而是把一个真实业务场景拆成数据模型、状态流、并发处理和部署交付的完整能力。如果你也在做同类型的项目先从需求拆解和数据库设计开始别一上来就写代码。把这个基础打牢后面的开发其实就是水到渠成的事情了。