在互联网+浪潮里,“网约护士”算是一个很典型也很有价值的落地场景。患者不用往医院跑,在手机上选服务、约时间,专业护士上门完成护理。这个项目我刚完整做完一遍,技术栈就是题目里的那套: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,或改为5 |
| 1 | 已接单 | 护士开始服务,患者取消 | 状态改为2,或改为5 |
| 2 | 服务中 | 护士确认完成服务 | 状态改为3 |
| 3 | 待确认完成 | 患者确认收货完成 | 状态改为4 |
| 4 | 已完成 | 患者评价 | 生成评价记录 |
| 5 | 已取消 | 无 | 无 |
| 6 | 已退款 | 无 | 触发退款流水 |
为什么会在“已接单”和“已完成”之间塞一个“待确认完成”?这是借鉴电商订单的“确认收货”逻辑。护士上门做完护理后,不能直接自己确认完成,必须要等患者确认或者超时自动确认。护理服务的服务对象是患者的身体,如果不设置患者确认环节,护士提前点了完成,一旦后续出现纠纷,平台就没有中间状态可以介入。后来我在实际操作中发现,患者确认时间是评估平台服务质量的一个关键数据点,这个状态加得非常有必要。
3. 核心功能实现:下单、抢单、支付和评价闭环
这个系统最核心的业务流是:患者浏览服务项目,选择时间地址,下单支付,护士接单,上门服务,双方确认,评价。这中间有两个技术难点最考验功底:一个是护士抢单的并发控制,另一个是支付回调的幂等处理。
3.1 患者下单与护士抢单:一次只能有一个人成功
患者下单的逻辑相对简单,生成订单号,插入订单记录,状态设置为待接单,然后把这个待接单信息推送到护士端。这里订单号我采用的是“时间戳+随机数”的生成方式,比如yyyyMMddHHmmss加6位随机数,保证不重复即可。
护士抢单才是真正的硬骨头。试想一个场景:平台把订单推给20个符合条件的护士,20个人同时点“接单”,数据库只有一个订单记录,谁能接?如果用select查询订单状态,判断是待接单,再update,这个两步操作之间存在时间差,多个护士可能同时看到待接单订单,然后一起更新成功,最后订单就被多个护士“认领”了。
解决办法是用数据库行级锁或者乐观锁,把“查询并更新”变成一个原子操作。我采用的做法是直接执行一条带条件的UPDATE语句:
@Update("UPDATE t_order SET nurse_id=#{nurseId}, status=1, update_time=NOW() " + "WHERE id=#{orderId} AND status=0 AND nurse_id IS NULL") int acceptOrder(@Param("orderId") Long orderId, @Param("nurseId") Long nurseId);这个更新语句的条件里加上了status=0 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 test="status != null and status != ''"> AND status = #{status} </if>结果发现当status传入整数0时,这个条件竟然不生效,查出来的数据不是待接单订单。原因是MyBatis的OGNL表达式里,Integer类型的0会被当作false处理,status != ''这个判断默认认为0等于空字符串。解决办法是去掉对空字符串的判断,只判断null:
<if test="status != 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.jpg,Tomcat默认并不会把/upload映射到磁盘上的/home/upload目录。所以图片上传成功后,前端就是显示不出来。
SSM中要让这个访问路径生效,有两种办法。最简单的办法是在SpringMVC配置里写一个资源映射:
<mvc:resources mapping="/upload/**" location="file:/home/upload/"/>这样/upload/开头的请求就会被映射到磁盘目录。另一个办法是部署到Tomcat时修改server.xml,在Host节点下配Context,把/upload这个访问路径指向物理目录。开发环境用第一种,服务器上我用的是第二种,因为这样更直观。
4.5 日期格式的坑:前后端交互报400错误
以下是我整理的几类典型问题快速参照表:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 订单状态0查询不出来 | MyBatis中Integer 0被OGNL当成false | <if>只判断null,不判断空字符串 |
| 分页数据不对,count了别的表 | startPage和查询之间夹杂了无关操作 | startPage后面只跟一个Mapper查询 |
| 接单时事务只执行了一半 | 同Service内部自调用,事务代理失效 | 拆到不同Service,或使用TransactionTemplate |
| 图片上传成功但页面404 | 静态资源没有映射到磁盘目录 | 配置<mvc:resources>映射外部目录 |
前端传2025-05-01后端报400 | SpringMVC默认无法解析该格式 | 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框架本身,而是把一个真实业务场景拆成数据模型、状态流、并发处理和部署交付的完整能力。如果你也在做同类型的项目,先从需求拆解和数据库设计开始,别一上来就写代码。把这个基础打牢,后面的开发其实就是水到渠成的事情了。