1. 这个平台到底在解决什么问题:从养老陪护到院内护工的业务链路
先说一个很直观的场景:家里老人生病住院,子女白天要上班,晚上大概率也没法整夜守着。这时候你打开平台,选一个评分高的陪护人员,看他的工作年限、擅长的护理方向、可预约的时段,下单后对方在手机端接单,次日早上八点准时到医院。整个过程涉及三端:患者/家属端负责浏览、下单、支付、评价,陪护端负责接单、查看排班、确认服务记录,管理端负责人员审核、订单监管、结算对账。
这个平台想解决的核心问题,是“陪护需求”和“陪护供给”之间长期存在的信息不对称。传统模式里,患者家属只能靠护士站推荐或者熟人介绍,价格不透明,服务过程没记录,出了问题连追溯的口径都没有。把交易线上化之后,至少能沉淀三样东西:陪护人员的履历和证照、订单的服务时段和签到记录、双方互评的信用数据。这些数据一旦积累起来,后续做排班优化、价格指导、人员资质审核都有依据。
这种项目面向的群体其实是两类:一类是子女不在身边、需要用通用化流程对接陪护服务的家庭;另一类是医院周边想接稳定单量的个体陪护从业者。平台要做的事不是替他们完成陪护动作,而是把撮合、跟踪、结算这些外围环节标准化。所以这个项目的关键点不在算法有多牛,而在业务流程能不能闭环:从注册认证、下单改约、服务签到,到服务完成后确认并评价,每一步都要有状态流转和记录。
还有一个容易被忽视的点:陪护服务是典型的“人服务业”,高峰时段集中在工作日的上午和傍晚,夜班需求也不少,这和订单型电商有明显区别。设计时不能简单照搬商品交易系统,得考虑陪护人员的“时段占用”概念——一个陪护在同一时间段只能接一单,否则会出线下的实际冲突。这是贯穿整个系统设计的一条暗线,后面讲表结构时会再次提到。
整体技术思路是这样的:后端用SpringBoot做微服务化的基础,但实际落地通常是单应用多模块,避免一开始就拆成十几个服务给自己找麻烦;前端用Vue做单页应用,负责三端的管理界面;数据层面用MySQL存业务数据,用Redis做热点缓存和分布式锁;鉴权这块用JWT完成无状态登录,配合角色区分患者、陪护、管理员三种身份。这套组合在职级评审、毕设答辩、简历项目里都非常常见,因为它兼顾了技术展示面和工作量的可控性。
2. 为什么是SpringBoot+Vue:一种既稳又快的前后端技术组合
2.1 单体拆分前后端,小团队和中等并发场景的现实答案
过去几年的行业风向是“微服务化”,很多团队不管有没有那个体量,先把服务拆了再说。但这类医疗陪护平台,业务复杂度并没有到必须拆服务的程度,正常的团队规模也就是两三个后端加一两个前端。如果强行拆成订单服务、用户服务、陪护服务、支付服务,反而会让联调成本直接起飞,光是一个跨服务的订单状态同步就要写一堆补偿逻辑。
SpringBoot的价值不在于它本身多花哨,而在于它帮你把大量重复配置收掉了:默认内嵌Tomcat,不需要再单独部署Web容器;基于starter机制整合MyBatis、Redis、邮件发送都是几行配置的事;自带统一的异常处理和参数校验入口,这在业务开发里的收益非常直接。再加上Spring生态的认知度很高,出了问题社区资料多、找人接手也容易,团队磨合成本低。
前端选Vue而不是React,也是基于现实考量。国内多数后端转全栈的开发者接触Vue的门槛更低,单文件组件写起来直观,配合Element UI或Ant Design Vue这类现成组件库,后台管理的表格、表单、弹窗基本可以做到“复制改改就能用”。对医疗陪护这种重表单管理的业务场景来说,开发效率比框架的“先进程度”更实际。
2.2 为什么是MySQL+Redis+JWT这一套配套
选型时不能只看框架,配套组件的选择更影响后续开发体验。
MySQL是业务数据的最终存储,订单、用户、陪护人员信息、评价记录都放这里。这类平台的数据量级在很长一段时间内都不会触及单表瓶颈,正垂直到几千单每天就已经算很成功了,所以完全不需要一开始就引入复杂的分库分表,保持单库多表配合索引优化就够用。
Redis在这里承担三个职责:热点数据缓存、接口幂等去重、分布式锁。比如陪护人员的首页推荐列表,查询频率高但数据变化不快,完全可以缓存一份;下单环节为了防止用户重复提交,可以用Redis做一个幂等标记,15秒内同一个用户对同一陪护的重复下单直接拦截;而分布式锁则是用来解决“同一陪护同一时段被两个用户同时下单”的并发问题,后面会细讲。
JWT负责登录态的无状态化。真实场景中,你会希望陪护人员在A手机上登录后,再去B手机打开小程序或网页时,会话还生效。JWT的特点是服务器不存session,把用户信息加密签在token里,前端带token调接口即可。缺点也明确:无法主动让token失效,所以还需要在后端做一个简单的token黑名单或结合Redis做“踢人下线”的操作。
整条链路合起来的请求过程是这样的:前端Vue发起请求,带上存放token的Authorization头;后端SpringBoot由拦截器统一取出token并解析,识别出用户角色后,把用户ID和角色信息塞进当前线程上下文;业务逻辑需要查库时通过MyBatis去MySQL拿数据,涉及热点数据先查Redis;订单这类写操作会加分布式锁防并发,数据落库后再同步更新缓存。这套方案成本低、逻辑清晰,非常适合展示在项目描述里。
3. 后端从0到1:核心模块的落地顺序
3.1 工程结构:先定好模块再写代码
一篇优秀的后端代码的可读性,很大程度上取决于工程结构是怎么切的。这里我采用的是一种非常主流的Maven多模块方案:
parent-project ├── boot-admin // 管理端接口模块 ├── boot-app // 患者端/家属端接口模块 ├── boot-carer // 陪护端接口模块 ├── boot-common // 公共工具、统一返回值、异常、实体 ├── boot-system // 登录鉴权、权限控制、系统配置 └── boot-business // 订单、陪护、评价等核心业务前端这边也是跟着模块走,每个端一个目录:web-admin、web-app、web-carer,或者说三个客户端共用同一套前端工程、用路由前缀区分也可以。但实际体验下来,管理端的页面样式、交互逻辑和用户端的差异很大,硬塞在同一个工程里会让路由和组件权限维护变得很乱,我建议至少管理端单独抽一个工程。
模块划分的另一个好处是给后续扩展留了余地。如果要做护士端或者对接医院的排班系统,直接在boot-business旁边新增模块,不影响现有代码。这里要注意的一点:模块之间的依赖方向要单向,boot-app只能依赖boot-business和boot-common,不能反过来,否则代码就成团了,改动起来到处都是坑。
3.2 订单状态机与并发保护
订单是这类平台最核心的实体,从下单到完成,中间有一系列状态变化。我在数据库里用的状态字段是整数,但是代码里会用枚举去约束,避免魔法数字到处飞。整个状态流可以梳理成一张表:
| 状态 | 含义 | 触发操作 |
|---|---|---|
| 0 | 待接单 | 用户提交订单,占用陪护时段 |
| 1 | 已接单 | 陪护确认接单,服务锁定 |
| 2 | 服务中 | 陪护到达并签到,开始计时服务 |
| 3 | 已完成 | 服务时长结束,用户确认服务完成 |
| 4 | 已取消 | 用户或陪护取消,释放时段 |
| 5 | 已投诉/争议 | 双方有争议,进入后台人工审核 |
| 6 | 退款中 | 用户申请退款,等待管理员确认 |
状态机背后有一条铁律:金额和时段是否释放,是结合状态来判断的。比如状态从0变4(取消)时,占用的陪护时段要释放;从2变3(完成)时,陪护的服务费才能进入结算池;从4变3(恢复)这种情况在业务上是不允许的,所以代码里要用状态判断拒绝非法流转。
并发问题是下单环节的重灾区。假设陪护人员小王在8月20日晚上8点到10点这个时段被甲用户占用,乙用户在这个瞬间也提交同一时段订单,如果不加控制,两张订单都会插入成功、时段占用状态也能查出来,但实际这个陪护根本不可能同时照顾两个人。
解决方式我在项目里用了“数据库唯一索引+Redis分布式锁”的组合。数据库侧在订单表加一个唯一索引(carer_id, service_date, time_slot_start, time_slot_end),保证同一个陪护同一段时间只能有一条有效订单;Redis侧则在下单前先尝试获取锁,key放在陪护ID和时段上,加锁失败直接提示“该时段已被占用”。两者互为兜底,索引防的是数据库层面的唯一性破坏,锁防的是并发请求层提前拿错数据。
3.3 三角色登录的鉴权体系
医疗陪护平台至少有三类用户:患者/家属、陪护人员、平台管理员。三个角色的登录逻辑可以复用同一套登录接口,后端根据账号类型走不同的登录校验逻辑,最终返回的JWT token里带上role字段。前端拿到token后,会解析出角色,并根据角色渲染不同的菜单和页面。
后端鉴权的核心不是“登录”这一步,而是“接口权限”的控制。我用的是一个自定义注解@RequireRole加Spring AOP做的拦截逻辑:用户在某个接口上标注@RequireRole("carer"),AOP切面会先检查当前登录用户的角色是否匹配,不匹配则直接抛403。这套方案比手动在每个Controller里if(user.getRole()!=...)要干净很多,也更方便后续加权限维度。
有一个我踩过的坑:JWT的过期时间不要太长,也不要太短。之前设的是7天,结果用户长期不退,陪护端那边快到期时间全是异常提示;设成24小时之后,用户两天没打开就得重登,体验也很差。比较好的做法是双token方案:短期access token(2小时)负责业务接口调用,长期refresh token(7天)负责在token过期后静默换新。前端在拦截器里发现401就带着refresh token去换新token,换成功了就自动重放原始请求,用户体验几乎没有感知。
3.4 数据库设计:陪护订单的三张核心表
数据表设计能直接反映对这个业务的理解。如果只给一张大宽表把所有字段塞进去,那后面每改一次需求都等于重构。我给这个项目设计的三张核心表分别是用户表、陪护人员信息表、订单表,围绕它们外围再挂评价表、结算表、签到记录表。
用户表的核心字段是登录账号、手机号、密码原文的加密哈希、角色类型、状态。密码用的是BCrypt加密,不用MD5和SHA-1,原因很简单:这些旧算法已经可以靠彩虹表秒破。陪护人员信息表要和用户表分开,因为陪护人员除了账号信息外,还关系到护理级别、服务时长、评分、工作状态和证照文件,这些字段查询频率高、更新频率低,单独建表能降低主表的锁竞争。
以下是订单表的关键字段设计,这个是理解整个系统的基础:
order_id BIGINT // 订单号,作为主键 order_sn VARCHAR(64) // 业务单号,展示给用户 user_id BIGINT // 下单用户ID carer_id BIGINT // 陪护人员ID service_date DATE // 服务日期 time_slot_start TIME // 服务开始时间 time_slot_end TIME // 服务结束时间 service_type TINYINT // 陪护类型:生活陪护/专业护理/夜间陪护 amount DECIMAL(10,2) // 订单金额 pay_type TINYINT // 支付方式 order_status TINYINT // 订单状态,对应上面提到的枚举 cancel_reason VARCHAR(255) // 取消原因 carer_confirm_time DATETIME // 陪护确认接单时间 created_time DATETIME // 创建时间 updated_time DATETIME // 更新时间特别注意amount字段用DECIMAL(10,2)而不是DOUBLE,MySQL里浮点数做金额计算会出现0.1+0.2不等于0.3的经典问题,一旦涉及对账、退款,这个误差会被无限放大。服务日期和时间段分开存,是为了配合唯一索引做并发控制,也方便后续做陪护人员的排班日历展示。同时给carer_id和service_date加组合索引,这是整个系统查询最频繁的路径之一。
4. 前端工程化:Vue侧页面框架与状态管理
4.1 从登录页到服务落地:前端页面链路的组织
用户端的核心页面可以归纳成五条链路:注册登录、浏览陪护列表、陪护详情、下单支付、服务中与评价。陪护端则是对应的个人资料、排班日历、待接单列表、服务签到、收入账单。管理端主要是订单管理、陪护审核、投诉处理、数据看板。
Vue工程里我用Vue Router把这些页面组织起来,路由配置里做了嵌套路由和懒加载。懒加载这块的价值是肉眼可见的:前后端分离之后,首屏JS包很容易超过1MB,如果不做按路由切分,用户打开登录页要等好几秒。用import的动态引入方式,每个页面的JS独立打包,首屏加载体积能降一半以上。
菜单和路由的联动是这里的一个隐蔽复杂度。用户端只有首页和个人中心两个大分区,菜单简单;管理端往往有十几个一级菜单。我做法是后端返回一个菜单树JSON,前端动态生成路由和侧边栏菜单,而不是写死在路由表里。这样后续新增一个功能页面,只要后端权限配置里同步加一个菜单项就行,前端不用改代码。
4.2 axios封装、401刷新与权限拦截
axios是Vue生态里事实标准的请求库,但直接用的话代码里会到处都是一大段配置。我把请求逻辑统一封装在一个request.js模块里:设置统一的baseURL、请求超时时间、请求拦截器和响应拦截器。
请求拦截器做的事很简单:读本地存储里的access token,如果存在就加到请求头的Authorization字段。响应拦截器则要复杂一些:后端返回业务码200表示成功,400表示参数错误,401表示token过期或无效,403表示权限不足,500表示服务器异常。前端的响应拦截器要对这些状态码做统一处理:401尝试用refresh token换新,换成功就重放原请求,换失败就跳转登录页;403则提示“没有权限执行该操作”,并跳转到无权限页面。
这里有一个细节容易忽略:token换新时要处理并发问题。如果页面上同时发起了三个请求,三个都收到401,然后三个都去调刷新接口,就会造成重复刷新。正确做法是设置一个“正在刷新token”的全局标记,其它请求在刷新期间进入等待队列,待第一个刷新请求完成后,队列里的请求统一重放。这个优化的代码量不大,但体验提升极其明显。
4.3 后端渲染优先还是前端控制:真实场景下的取舍
在项目初期,我也纠结过陪护人员的展示方式,后来明确下来:列表页优先使用后端返回的渲染数据,尽量避免前端对数据做二次计算。
具体到陪护列表页,后端返回的每条记录包括:陪护人员ID、头像、工作年限、护理方向标签、服务次数、最新评分、可预约时段列表。前端只是把数据原样渲染成卡片,不参与“哪些时段可约”的计算。这个逻辑放在后端的原因很简单:可预约时段是实时变动的,取决于数据库中的订单占用情况,如果前端每次都要向后端单独拉取时段可用性,那列表页的请求数会翻好几倍。
另外页面上的筛选功能也尽量传给后端排序,而不是拿全量数据在前端filter。数据量小的时候看不出差别,但等陪护人员数量过千、每个人员又有多个服务项时,前端浏览器去处理几千条数据会明显卡顿,后端加索引查询则是毫秒级,这种差距是架构层面该定的,不是单点优化能补的。
5. 踩过的坑:排单冲突、金额精度和时区问题
5.1 同一个陪护被重复下单,索引和锁到底哪个兜住了
这个坑是这个项目里最值得一写的,因为它是真实的业务并发问题,不是书本上那种“为了并发而并发”。
上线的第一天就出现了异常订单:一个陪护的同一时段被下了两单,订单表里出现两条(carer_id, service_date, time_slot_start, time_slot_end)完全一样而且状态都为0的记录。排查时先看日志,两条请求几乎同时到达,都经过了下单接口,分布式锁也加了,但居然双双通过。
原因在于锁的范围不对。我第一次实现的锁key是order:lock:{carer_id},这样同一个陪护的所有时段都共用一个锁,按理说也能保证同一个陪护同一时段不冲突。但问题出在锁的失效时间:Redis锁设置了30秒自动过期,而业务逻辑里除了检查时间段占用,还有用户余额校验、陪护在岗状态判断等一堆操作,加起来超过了30秒。第一条请求的锁到期自动释放,第二条请求趁虚拟机隙获取到锁,走进了和第一条完全相同的判断路径。
正确的做法是双保险:锁key必须细化到“陪护+日期+时段”维度,锁时间要设置在业务操作时间上限的两倍以上,同时一定要在数据库表上加模型级的唯一索引。索引是最终防线,锁是流量入口防线,两个缺一不可。解决这个问题后再做了一次压测,200并发下不再出现重复订单。
5.2 金额计算用BigDecimal,别再让double背锅
这个项目里金额相关的操作很多:下单计算金额、满减优惠、退款、陪护结算。最初没在意,后来QA测出一个诡异问题:某订单金额是129.9元,用户在支付页看到的是129.9,但后台结算对账时变成129.89,差了0.01元。排查下来发现是金额字段在Java里用了double,从数据库的DECIMAL取出后转成double,再乘以服务天数(比如3天),中间第17位小数点开始出现误差。
解决方式统一为:Java后端所有金额字段用BigDecimal,数据库字段统一用DECIMAL,前台传到后端时不要做任何类型转换,直接用字符串传给BigDecimal的构造函数。同时在BigDecimal计算时明确scale和舍入方式,比如new BigDecimal("129.90").multiply(new BigDecimal("3")).setScale(2, RoundingMode.HALF_UP)。
前端支付金额展示也有一个坑:后端返回的金额不要用String拼“¥129.9元”这种形式,直接返回数值和单位字段,前端负责格式化。这样后续如果接入第三方支付平台,对账逻辑好写,也不容易因为格式化问题导致显示金额和实际支付金额不一致。
5.3 时间字段引发的“今天服务还是昨天服务”迷惑
医疗陪护这类业务,时间敏感度非常高。用户晚上9点下单,第二天早上8点的陪护,这里涉及跨天、时区、前端展示三个环节。排查时发现一个典型问题:陪护人员在自己手机上看到的“服务开始时间”是10:00,但用户端看到的是11:00,差了正好一小时。
原因起于后端把LocalDateTime序列化成了带时区偏移量的格式,前端拿到字符串后直接用浏览器的时间解析。用户在上海(UTC+8),陪护人员如果手机时区被设为UTC,双方的展示就不一致。解决方式是统一全局时间格式和时区策略:后端接口统一返回ISO8601标准的时间字符串,如2025-07-20T08:00:00;前端在请求拦截器里统一指定timeZone: 'Asia/Shanghai';数据库联接参数里也要显式设置serverTimezone=Asia/Shanghai。
另外一个容易被忽略的时间坑出现在“服务日期”的计算上。用户下单时选择的是“明天”的陪护,如果前端直接传2025-07-21,但后端现在时间已经是23:50,这个日期就是合法的。但如果前端浏览器日期快了一天,或者时区设置成UTC,传过来的日期可能会错成2025-07-20,导致下单后用户在7月20日23:59收到错误提醒。最后的做法是:服务日期一律由后端根据前端传的服务开始时间推算出日期,前端只负责传递“选的时间点”,不做日期计算,这样彻底杜绝了时区导致的日期漂移。
5.4 本地联调的跨域问题:前端的“优雅处理”其实是隐患
前后端分离开发时,前端跑在localhost:8080,后端跑在localhost:9090。第一次联调时,前端调用后端接口直接报CORS错误,很多人会顺手在后端配置类里加一个允许所有来源的CORS配置,这在开发环境没问题,但如果把这个配置原封不动带上生产环境,就会出现安全隐患:任何人从任意域名都能调你的后端接口,等于把CSRF攻击的大门敞开了。
我的处理方式是:后端CORS配置只允许指定的几个前端域名,生产环境这里通常通过Nginx反代实现同域处理,后端基本不需要再单独处理跨域。开发环境的跨域则用Vue CLI的devServer代理转发,这样前端请求的路径始终是同域的/api,后端也无需开跨域。
还有一个相关的小坑:前端请求的URL带不带/api前缀,会导致后端拦截器匹配规则错乱。我的做法是前后端口径对齐:前端统一请求/api/**,Nginx将/api前缀转发到后端,后端拦截器统一匹配/api/**,前端不再额外拼接后端路径。整个过程里“前缀规范”是小事但特别容易扯皮,建议起步就把这个约定写进项目README里。
6. 上线部署与真实验收:jar包、Nginx和证书
6.1 从开发机跑到服务器:交付的不该只是源码
开发环境跑通和服务器上真正跑起来,永远隔着一道看不见的墙。之前我带的一个小组路径很典型:本地跑着没问题,部署到服务器后一会儿连不上数据库,一会儿静态资源404,日志里全是稀奇古怪的报错。根源基本都是环境配置不一致。
后端打的是jar包,由mvn clean package -DskipTests生成。服务器上的Java环境版本必须和本地一致,我遇到过JDK 8的代码在JDK 17上编译版本不匹配直接起不来,解决方案是在pom里显式指定maven.compiler.source和target为1.8。启动命令建议用nohup java -jar xxx.jar --spring.profiles.active=prod > app.log 2>&1 &,并记录PID,方便后续停机维护。
前端部署则简单一些,npm run build生成静态文件目录dist,然后放到Nginx的站点根目录里。Nginx配置涉及三个要点:gzip压缩开启,能显著减小JS和CSS的传输体积;try_files指令保证前端单页应用刷新时路由不404;proxy_pass把/api请求转发到后端端口。这个配置几乎是前后端分离项目的标配,但每一行都能找到值得注意的小细节。
6.2 HTTPS证书与生产环境的安全标配
如果平台要引入真实用户,上线时没配HTTPS等于裸奔。用户手机号和订单支付信息在传输层被明文抓包,事后追究起来是平台方的责任。我的部署方案里,Nginx强制HTTPS:把证书文件和私钥配置到443端口,同时把所有80端口请求重定向到HTTPS,这样一来,前端页面本身是加密的,请求后端时也是同域加密。
证书的来源渠道不限,自签名证书可以用来内网测试,但公网服务必须有受信任的CA签发的证书。申请证书后还需要一个续期机制——不少证书有效期只有90天,忘了续期网站直接打不开。我的经验是把续期命令加到crontab里,每月自动检查并更新证书,再配合Nginx reload操作让新证书生效。这个动作看起来不起眼,却是上线后最容易出问题的运维点之一。
6.3 上线后要盯的监控指标
在我的经验里,这个系统上线后真正需要盯的是这几个指标:后端接口响应时间、订单接口的QPS、数据库慢查询数量、Redis命中率、异常日志数。单独一个低端服务器跑这个项目,QPS承载量有限,但高峰期并发打过来时,数据库的慢查询会迅速拖累整个系统。
我会在项目里引入一个简单的日志切面,在核心业务接口记录请求路径、参数摘要、耗时、用户ID,每天跑一次日志分析看看哪些接口特别慢。慢查询排查时优先看是否缺索引,比如订单表按carer_id查待接单列表,如果没有索引,数据量过千以后查询速度会断崖式下降,加完索引效果立刻可见。
服务器上看系统状态,我用的是两行命令走天下:free -h看内存余量,top看CPU占用。项目本身的内存占用不高,最怕的是Java默认堆内存设置太大导致服务器内存吃紧,或者Nginx日志文件没人清理占满磁盘。建议启动时显式配置JVM堆内存大小如-Xms256m -Xmx512m,并搭配一个简单的日志切割任务,这是保障稳定性最朴实的手段。
最后说个运维层面的判断标准:一个技术项目做完的验收,不只是“页面能点、接口能通”,而是把它想象成真实用户每天在用的产品。一旦用户开始下单,稳定性、数据正确性和资金安全就成了不能妥协的底线。陪护订单涉及真金白银、涉及人的服务承诺,数据错一条都可能引发线下纠纷。所以我在部署完之后的建议是,至少跑一周的内部测试:正常下单、取消单、退款、并发下单、网络重试、陪护换班,把这些场景全部过一遍,再谈对外开放。对于这种类型的平台来说,平台本身的技术复杂度并不高,真正高的是业务责任心和细节敬畏心。