前阵子一个做社区团购的朋友找我,说想给小区业主搞个“共享遛狗”服务,需求很简单:业主出差或者加班,家里猫狗没人管,找人上门喂食、遛弯,按次付费。我一听,这不就是典型的同城轻服务场景嘛,需求确实真实,但市面上能直接拿来用的现成系统不多,自己开发又怕踩坑。聊到最后他问我能不能给一套完整的技术方案,于是就有了这篇基于SpringBoot的同城上门喂遛宠物预约系统。
这套系统表面上看就是个“需求发布+接单”的C2C平台,但真正落地的时候,涉及的模块远比想象中多:用户端和接单端分离、订单状态流转、支付结算、地理位置匹配、日程提醒、评价体系,还有最容易被忽视的“时间窗口冲突检测”。如果你正打算做类似的同城服务系统,比如上门保洁、代取快递、陪诊,这套架构基本都能复用。本文我会从需求拆解、数据库设计、核心接口实现到部署避坑,把整个开发链路讲透,确保你拿到就能开工。
1. 项目需求分析与整体设计思路
1.1 同城上门服务的业务特点与核心痛点
在做任何系统之前,先得把业务场景捋清楚。上门喂遛宠物和普通电商交易有本质区别:电商的核心是人货匹配,而服务类系统要解决的是“人、时间、地点”三者的精确匹配,尤其是时间,往往比地点更难处理。
比如一个宠物主下单时,需求是“明天早上9点到10点之间,上门喂猫并铲屎,猫粮在门口柜子里”,这句看似简单的话,拆成系统需求就变成了三条约束:服务人员必须在指定时间段内到达,服务人员必须有权限获取地址和钥匙信息,服务人员的日程里不能同时承接其他冲突订单。
再看接单方,一个专业遛狗师一天能接的订单有限,如果把通勤时间也算进去,满打满算8小时能服务6到8家。系统如果不能在派单或抢单阶段就把时间冲突过滤掉,服务方经常会被无效订单骚扰,体验会很差。
我梳理下来,这类系统在架构层面必须解决四个核心问题:
- 时间窗口建模:下单不只是一个时间点,而是一个时间段,系统要用时间段做重叠检测。
- LBS距离排序:同城服务必须优先展示距离近的订单,所以经纬度存库和距离计算绕不开。
- 状态机设计:一个订单从创建到完成,中间有接单、上门、服务中、待评价等多个状态,任何状态变更都必须有约束。
- 资金安全隔离:平台涉及预付款、服务费、保证金,钱不能直接打到对方账户,需要引入虚拟账户体系。
1.2 为什么选择SpringBoot作为核心框架
这个项目选SpringBoot,不是因为它最炫,而是因为它最“省事”。SpringBoot用自动配置帮我们省掉了一大堆XML配置,内嵌Tomcat让部署变成“一个jar包跑起来”,配合Spring Cloud或单体Maven模块化,无论你要做微服务还是先跑通单体,都进退自如。
具体到这个项目,SpringBoot带来的直接收益有三点。
第一,生态整合顺手。我们后面要用MyBatis-Plus操作数据库、用Spring Security做登录认证、用Spring Task做日程提醒定时任务、用Redisson做分布式锁,这些组件在SpringBoot体系里都有非常成熟的starter,几乎能做到即插即用。相比早期Spring时代手动拼配置,开发效率至少提升三倍。
第二,项目结构足够标准。SpringBoot推荐的启动类、控制层、服务层、数据访问层分层的目录结构,特别适合多人协作和后续维护。哪怕是刚毕业的新人,看到这个结构也能快速定位代码位置,降低沟通成本。
第三,部署和运维简单。SpringBoot支持打包成可执行Jar直接运行,配合Docker可以做到一条命令启动整个环境。宝塔面板、阿里云构建服务也都有专门的SpringBoot部署方案,我后面会专门讲我自己的部署过程。
1.3 系统角色与模块划分
这套系统我按角色和功能拆成了六个核心模块,清晰划分边界是避免后续需求膨胀时改到崩溃的关键。
首先是用户端(宠物主)模块,负责注册登录、宠物档案管理、发布预约订单、在线支付、查看服务进度和评价。这个模块是用户感知最强的部分,UI交互和接口响应速度都要优先保证。
然后是服务端(遛狗师)模块,包括接单大厅、订单抢单/派单、日程管理、服务打卡和收入统计。这里有一个细节需要注意:服务方抢单后会有一定比例的用户“被鸽”,所以系统必须提供申诉和取消机制。
平台管理端模块就不用多说了,核心是审核、仲裁和统计。开发管理端时我个人建议优先做“订单仲裁”功能,因为服务类平台只要单量上来,纠纷必然会出现,常见的就是服务时间对不上、宠物状态异常、额外费用争议。
支付模块我独立拆出来,因为它牵扯资金安全问题。用户下单是预付款进平台,服务完成后确认收货再结算给服务方。这一块请务必从一开始就做好虚拟账户和提现流水表,否则后期对账会非常痛苦。
消息通知模块负责订单状态变更提醒,包括短信验证码、站内信和微信模板消息。别小看这个模块,上门服务最怕信息不同步,服务方到了却没通知用户,用户还以为人没来,直接给差评。
最后还有LBS与地图模块,负责距离计算、坐标转换和服务范围圈定。这个模块不复杂,但要注意国内地图坐标系的问题,后面实操板块我会具体说。
2. 数据库设计与核心表结构
2.1 数据库选型与表关系规划
数据库我用的是MySQL 8.0,因为这套系统的数据量级在初期远达不到需要分库分表的程度,但事务性要求非常高。服务单的创建、支付、状态变更必须保证强一致,MySQL加上InnoDB引擎足够应对十万级订单量。
整个库我规划了11张表:用户表、宠物表、服务类型表、服务者信息表、预约订单表、订单状态流水表、日程表、支付流水表、账户余额表、提现记录表、评价表。
这里我要特别说两个容易被人忽略的表设计。
第一个是订单状态流水表。很多人做订单表就在订单上放一个status字段,一改就完了。但服务类业务,用户经常要投诉“我的订单怎么被取消了”,没有流水账,你根本说不清是系统关单还是服务方取消。所以每个状态变更都必须往流水表插一条记录,记录操作人、操作时间和前后状态。
第二个是日程表。服务方接单后,系统需要把订单的时间段同步到服务者的日程表里。这个表不能省,因为后续所有的时间冲突检测都是基于日程表的查询,而不是直接查订单表,两个表职责不同,性能上也差异很大。
2.2 核心表字段设计详解
先说预约订单主表,这是整个系统的核心。我贴一段关键字段设计:
CREATE TABLE `service_order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单主键ID', `order_no` varchar(32) NOT NULL COMMENT '订单编号:业务展示用', `user_id` bigint NOT NULL COMMENT '下单用户ID', `provider_id` bigint DEFAULT NULL COMMENT '接单服务者ID,初始为空', `pet_id` bigint NOT NULL COMMENT '宠物ID', `service_type` tinyint NOT NULL COMMENT '服务类型:1喂食 2遛狗 3喂食+遛狗 4其他', `service_start_time` datetime NOT NULL COMMENT '服务开始时间', `service_end_time` datetime NOT NULL COMMENT '服务结束时间', `address` varchar(255) NOT NULL COMMENT '上门服务地址', `latitude` decimal(10,6) DEFAULT NULL COMMENT '经度', `longitude` decimal(10,6) DEFAULT NULL COMMENT '纬度', `service_fee` decimal(10,2) NOT NULL COMMENT '服务费用,单位元', `status` tinyint NOT NULL COMMENT '订单状态:0待支付 1待接单 2已接单 3服务中 4待确认 5已完成 6已取消', `remark` varchar(500) DEFAULT NULL COMMENT '用户备注,如钥匙位置等', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_provider_id_status` (`provider_id`, `status`), KEY `idx_service_time_range` (`service_start_time`, `service_end_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='预约订单主表';这个表设计我有几个心得的点。
一,业务订单号和数据库主键分离。数据库主键是自增的,但用户看到的信息不能用自增ID,太容易被人猜测订单数量,所以单独设计了order_no,用时间戳加随机数生成。
二,状态机字段。我故意把订单状态设计成数字枚举,而不是字符串。原因很简单,后面用状态机做流转判断时,数字比较性能高,代码也简洁,而且规范了入口,不可能出现“中文状态”这种脏数据。
三,联合索引的设计。idx_provider_id_status联合索引专门优化服务方端“查询我的待接单/服务中订单”的请求频率。idx_service_time_range则是用来做时间重叠检测的,后面讲冲突检测时会看到具体SQL。
2.3 订单状态机的设计思路
状态机必须在一开始就定死,否则后期改业务流程时测试成本极高。我这里的状态定义是:
0待支付 → 1待接单 → 2已接单 → 3服务中 → 4待确认 → 5已完成,任何状态下都可以跳转到6已取消。
关键的业务规则要在Service层强制校验。比如只有“待接单”状态的订单能被服务者抢单,抢单成功跳到“已接单”;只有“已接单”状态的订单,服务者到达现场点击“开始服务”才有效;用户确认完成后,订单进入“已完成”,此时触发资金结算。
我见过很多团队把这个校验散落在Controller里,这是特别不明智的做法。正确的姿势是做一个状态机组件,把合法性校验和状态流转封装成一个枚举类,所有入口统一调用。
public enum OrderStatusEnum { WAIT_PAY(0, "待支付"), WAIT_ACCEPT(1, "待接单"), ACCEPTED(2, "已接单"), SERVING(3, "服务中"), WAIT_CONFIRM(4, "待确认"), COMPLETED(5, "已完成"), CANCELED(6, "已取消"); // 根据当前状态计算下一个允许的状态 public List<Integer> nextStatus() { switch (this) { case WAIT_PAY: return Arrays.asList(WAIT_ACCEPT.getCode(), CANCELED.getCode()); case WAIT_ACCEPT: return Arrays.asList(ACCEPTED.getCode(), CANCELED.getCode()); // 其余状态同理 default: return Collections.emptyList(); } } }这样设计之后,每次业务操作只需要拿到当前状态,然后判断目标状态是否在allowed列表里,合法才执行更新,非法直接抛异常。很多线上问题其实是我自己先设计得不严谨导致绕晕了,后来用了这个方案,维护省心不少。
3. 核心功能模块与关键接口实现
3.1 基于Redis的抢单与幂等性设计
服务端接单是整个系统瞬时压力最大的接口。用户发布订单,所有符合范围的服务者都在接单大厅刷单,抢单请求极可能在同几秒内同时到达。
最简单的实现是直接UPDATE订单表,把provider_id从0改成当前用户ID,再加个条件WHERE status = 1。这一步在MySQL层面就完成了原子限制。但问题是,如果同一个服务者手速过快连续点了两次,第二次更新会因为provider_id已经被改掉而失败,这是我们要的。但服务者接到“抢单失败”提示后,会立刻刷新去抢别的单,这时候如果有100个人同时抢不同订单,数据库连接池容易被打满。
我的做法是在Redis上做前置过滤:抢单时先设置订单维度分布式锁(用Redisson),只有拿到锁的线程才允许查询订单状态并执行更新。锁的Key设计为order:accept:{orderId},过期时间10秒。这样即使同一时间有50个请求进来,50个都在等锁,拿到锁的只有第一个,后面的依次排队,但排队几毫秒后查询订单状态已经是“已接单”,直接返回失败,不再打数据库。
另外,幂等性处理也必须要做。用户在前端点了一次“发布订单”,但因为网络抖动又点了一次,系统不能生成两笔订单。解决办法是在前端提交时带一个requestId,后端用Redis的SETNX缓存在提交前先判重。这个思路同样应用在支付回调上,否则支付平台的重试机制会把你的事务搅乱。
3.2 上门服务时间冲突检测的实际方案
前面提到过,日程表是专门为服务者的时间排期准备的。我们来实践一下冲突检测的SQL。
场景:一个遛狗师想抢一个“明天14:00-15:00”的单,系统需要确认他在这个时间段内没有别的已接订单。
先往日程表里插入每条已接订单的时间段,然后执行:
SELECT COUNT(*) FROM provider_schedule WHERE provider_id = #{providerId} AND status = 1 AND service_start_time < #{endTime} AND service_end_time > #{startTime};这个SQL用到的就是时间段重叠判断公式:只要已有的开始时间小于新订单的结束时间,且已有的结束时间大于新订单的开始时间,就必然存在重叠。这是闭区间重叠判定的左右交叉法,属于最经典的时间段碰撞检测算法。
复杂之处在于,服务订单不止一个时间点,有些是半小时喂食,有些是两小时遛狗加洗澡,所以这个SQL不能用“等于”去匹配,必须用区间比较。
实操的时候还有一个隐藏问题:索引失效。如果服务者的日程表数据量到了几万条,上面的查询如果没有正确索引,MySQL会做全表扫描。所以必须建立provider_id + service_start_time + service_end_time的联合索引,并且注意SQL里字段的顺序要和索引顺序匹配,不能把service_end_time条件写在前面。
我实际压测过,加了联合索引后,单次冲突检测耗时从80毫秒降到3毫秒,这个体验差别在抢单高峰期是非常明显的。
3.3 基于Redis的定时任务与自动关单
上门服务系统里定时任务特别多,列举几个:超时30分钟未支付自动取消订单,接单后服务者超时未到店自动提醒,服务完成后24小时未确认自动触发结算。
这些在SpringBoot里可以用@EnableScheduling加@Scheduled注解来实现,但有个大坑:默认的Spring Task是单线程串行执行的。如果你把多个@Scheduled任务放在同一个类里,某个任务执行时间长了,后面的任务就会被阻塞,而且没有任何告警。
我推荐改造方案是用ShedLock或者Quartz做分布式锁支持的调度。如果你不想引入太多组件,至少要把每个定时任务写在自己的独立Service里,并且用线程池配置让不同任务并行执行。
订单支付超时关单是这类系统里最容易出Bug的地方。最简单也最稳的做法是懒取消加定时补偿,双管齐下。懒取消的意思是用户发起支付时,先检查订单创建时间是否已超过30分钟,超了就返回“订单已失效”并同步更新状态。定时补偿则是每隔5分钟扫一次所有“待支付且创建时间超30分钟”的订单,批量关单。这样做的好处是既不会出现用户卡在支付页却被告知订单失效的尴尬,也不会因为数据库全表扫描到超时订单导致系统慢。
3.4 LBS距离计算与服务半径圈定
LBS这块,我建议如果有条件,优先使用高德或腾讯的Web服务API来获取用户和服务者的经纬度。但在后端做距离排序时,不能每次都去调地图API,成本太高,性能也差。正确做法是在业务数据表里存好经纬度,然后自己写一个距离计算公式。
一般距离排序用Haoversine公式就够用了,精度能到几十米级别,计算公式在MySQL里也可以直接写成SQL,但我更喜欢用Java侧计算加内存排序,因为这个场景的数据量不会超过几千条。
这里有个国内开发的经典大坑:高德地图返回的是高德坐标系(GCJ-02),而微信小程序里获取的位置是WGS-84原始坐标系,两者之间大约有几百米的偏移,如果直接用,服务者的距离列表就会偏得离谱。这个偏移量要靠坐标系转换来纠正,高德提供的坐标转换API可以直接用,千万别自己去网上找转换公式,手工算误差会很大。
服务半径圈定我采用的是瓦片格网思路:并不是给每个服务者画一个圆,然后在圆里找订单,那样性能太差。我的做法是把城市按网格划分,服务者在注册时选择服务范围对应的网格ID,新订单发布时系统把订单的网格ID广播给匹配网格内的服务者。这样订单匹配完全不需要遍历全城,查询直接走索引走网格ID,效率提升非常明显。
3.5 支付结算与虚拟账户体系
这个模块我强烈建议一开始就要想清楚,否则后期对账会是一场噩梦。我采用的方式是虚拟账户体系,核心是两张表:账户余额表和资金流水表。
用户支付时,钱进入平台的“监管账户”,同时在用户的虚拟账户里增加“冻结余额”,真正支付完成后冻结余额变成可用余额,但这笔钱在法律意义上还不属于服务者,只有当订单确认完成后,才从平台的“待结算”池子划转到服务者的虚拟账户余额里,服务者才能发起提现。
每一步资金操作都必须写一条资金流水记录。这样做的好处是,当用户投诉“我付了钱为什么没人服务”时,你只要拉出资金流水就能说清资金的完整流向,平台的严谨度和合规度都高很多。
支付回调接口要特别注意幂等性。微信和支付宝的支付回调会进行多次重试,如果你在回调里写的是“先查订单状态,再更新状态”,那重复回调时会重复执行更新。最稳妥的做法是在回调处理前,先去流水表查这个交易号是否已经处理过,处理过就直接返回成功。这个“查询后再插入”的操作一定要放在事务里,并且要对transaction_id加唯一索引,才能彻底挡住重复请求。
4. 开发过程中踩过的坑与解决方案
4.1 状态更新丢失与乐观锁
线上环境经常出现的一个问题是:用户已经确认完成,系统也结算了资金,但订单状态还停留在“待确认”,用户一直能看到“待确认”的订单。我排查下来,问题出在超时自动确认的定时任务和用户手动确认并发执行,两次更新都查到了旧状态,后一次的更新覆盖了前一次的结果。
这个问题的根因是更新任务没有加乐观锁。我后来在订单表加了version字段,每次更新时执行:
UPDATE service_order SET status = #{newStatus}, version = version + 1 WHERE id = #{orderId} AND version = #{oldVersion};如果更新影响行数为0,说明版本冲突,需要重新查询后再执行一次操作逻辑。这样虽然多一点代码,但彻底解决了并发覆盖的问题。
4.2 SpringBoot版本过高引发的第三方库兼容性
我在开发时习惯用最新的SpringBoot版本,结果整合某些老牌的第三方库的时候报了各种奇怪的错,比如新版SpringBoot的javax.servlet变成了jakarta.servlet,很多库如果不升级,启动就会直接抛ClassNotFoundException。后来我老老实实按照项目需求选版本,现在稳定用的是SpringBoot 2.7.x,3.x虽然新,但对于传统业务系统真没必要尝鲜。第三方兼容性问题会让你耗费大量时间在环境排查而不是业务开发上。
4.3 用户端的“找不到订单”问题排查
上线初期用户反馈“我下单成功了,但我的订单列表里看不到”,好几个用户同时遇到。我排查后定位到分页查询的参数绑定错误:前端传的是页号page=1,但后端用PageHelper时,pageNum的起始值没有从0改成1,导致第一页数据被截断了。后来我统一封装了分页请求对象,并且加了接口文档注释,这个问题就再没出现过。这种字段约定问题,在前后端分离开发中是特别常见的,最好用接口文档或者直接代码里的统一返回对象来约束。
4.4 服务者迟到和无故爽约的预防
服务行业最怕的就是服务者素质参差不齐。后来我在接单流程里加了保证金机制,服务者接单前必须缴纳一笔保证金,如果接单后取消,会从保证金里扣除违约金给用户补偿。这个改动上线后,无故取消率从8%降到了不到1%,用户投诉量明显降低。
保证金机制意味着你又多了一套金额冻结和解冻的逻辑,这块代码务必放在资金模块里统一管理,不要散落到各个业务Service里。
4.5 定时任务在分布式环境下的重复执行
如果你单机部署,用Spring Task没问题。但如果你将来做了多实例部署,一个定时任务可能被多个实例同时执行,比如凌晨的自动结算,两个机器同时跑,就会导致重复结算。我建议要么用ShedLock引入分布式锁,要么把定时任务单独拆成一个独立部署的调度服务。定时任务的编写规范是“任务执行前先尝试获取分布式锁,获取不到就直接跳过,获取到了执行完再释放”,千万别裸写逻辑。
5. 部署上线与后续扩展建议
5.1 从开发到生产环境的部署实操
我把自己的部署过程记录下来,方便你照抄。开发机是Windows,生产环境是CentOS 7,数据库用宝塔面板管理。整体流程是这样的。
第一步,写Dockerfile打包后端服务。SpringBoot项目直接用Maven插件构建,我的Dockerfile非常简单:
FROM openjdk:8-jdk-alpine VOLUME /tmp ARG JAR_FILE=target/pet-service.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]第二步,构建容器并启动。我用docker-compose统一管理MySQL、Redis、后端、Nginx四个容器。Nginx负责前端静态资源的托管和后端API的反向代理,配置了轻量的请求限制,防止抢单瞬间的高并发把后端打挂。
第三步,配置HTTPS证书。如果后端要用微信登录、微信支付,就必须在微信公众平台配置安全域名和支付授权目录,这属于上线前最容易遗漏的一步。
第四步,配置数据库初始化脚本。上线前要把建表语句、初始数据、索引脚本都准备好,我习惯把表结构变更和索引变更单独写SQL文件,方便后续回滚。
5.2 同城服务系统的功能扩展方向
基础版上线后,如果你还有精力,可以往这些方向扩展。
智能推荐派单是首选方向。目前是抢单模式,但抢单模式有一个问题:好的服务者往往集中在少数几个热门时段,偏远地区订单几乎没人接。我后续计划做一个基于服务者历史接单时效、距离评分、用户评价的综合打分模型,把新订单按评分推送给最合适的Top3服务者,提高接单效率。
会员体系和优惠券模块也很值得做。宠物主和服务者都需要留存机制,可以考虑做次卡、包月遛狗套餐,对高频用户非常友好。优惠券的发放和核销逻辑必须注意防刷,优惠券领取接口要做用户维度限流,不然被羊毛党盯上,一夜之间能把你的营销预算薅光。
社区化运营也可以考虑。宠物主之间可以共享宠物保姆信息,服务者之间可以互推订单,这块如果做成了,产品的核心壁垒就不是系统本身,而是社区网络。
5.3 关于系统性能的提前优化建议
我不想等系统被用户骂卡了才来写这篇建议,所以提前做几个预判。
索引优化永远是最便宜也最有效的路径。接单大厅的订单列表查询、服务者确认完成订单的查询、后台管理端的运营统计,三个高频查询场景的SQL一定要用EXPLAIN验证一遍,凡是出现type=ALL的都要看能不能加索引解决。
Redis缓存不能只用在登录验证上。服务列表中已确认订单的详情数据、服务者热门的日程汇总、宠物档案的常量信息,都可以放进Redis。我习惯把缓存操作封装成模板方法,避免在业务代码里出现一堆重复的Redis读写代码。
日志链路也要提前设计。线上排查问题最怕的就是没有上下文。我在全局过滤器里为每个请求生成了一个traceId,并且在所有日志输出里带上traceId,这样乱成一锅粥的日志也能按请求串起来,排查Bug的效率至少提升一半。
6. 常见问题与排查技巧实录
6.1 服务者收不到新订单通知
我遇到过最离奇的Bug是某位服务者一直收不到新订单的APP推送通知,但其他人都正常。排查后发现问题出在他的手机系统关闭了通知权限。但为了定位到这个问题,我们排查了消息队列、手机型号、网络环境,走了不少弯路,最后发现是权限这么简单的原因。
所以做消息通知模块,一开始就要在服务端做好推送送达记录,消息投递成功与否、客户端是否展示都要有统计。否则用户投诉“我没收到通知”,你根本无从查起。
6.2 订单发布后一直显示“待支付”
用户下单后卡在待支付,同时支付成功了,但订单状态没有变成待接单。排除支付配置问题后,最大的可能性是支付回调的签名校验写错了。
微信支付回调的验签方式是对所有参数按ASCII字典序排序生成签名串,任何一个参数拼接多了或者少了都会导致验签失败。我建议你从回调里把原始报文先原样插进日志表,再写一个验签工具类,方便随时用日志去对比。如果验签一直失败,直接在微信支付商户平台的调试工具里把示例报文拿来测试,省时省力。
6.3 数据库连接池连接被占满
高峰期抢单导致数据库连接池被打满,这是一个高频问题。除了过度依赖数据库查询,还有一个隐藏原因:某个方法里的事务注解加在了Controller层,导致一个请求持有数据库连接的时间过长。事务应该只开到Service层需要保证一致性的最小范围,Controller里绝对不要加@Transactional。
除此以外,所有只有查询需求的接口,尽量保证走只读数据源或者使用MyBatis的只读标识,减少不必要的连接占用。
6.4 前端页面打包后的“白屏”问题
最后提一个和SpringBoot无关但经常发生的部署问题:前端打包后放进SpringBoot的static目录,打开页面一片空白。这个现象绝大多数是因为前端路由使用了history模式,刷新页面时请求到后端的静态资源路径是/order/xxx,而后端没有对应的Controller处理,导致返回404。
解决办法有两个:要么前端路由改成hash模式,简单粗暴;要么在后端加一个Controller,把非API的全部路径转发到index.html。我建议在开发阶段就统一用hash模式,省心。
写在后面的一些体会
这套系统从出新到稳定运行,前前后后大概花了两个月时间,中间经历过半夜爬起来排查生产环境异常的低谷,也经历过第一笔订单成交时的兴奋。回头总结,最想说的是:技术选型不是越高级越好,SpringBoot加MySQL加Redis这套组合,在这个业务场景下已经足够应付十万级用户量,而真正让系统变得好用的,往往是那些一开始不太起眼的设计:状态机、幂等性、资金流水、时间冲突检测。
如果你正在做类似的系统,我的建议是先把订单的整个生命周期建模想通透,这是灵魂;再把资金和状态变更的约束守住,这是底线;最后才是页面的交互和服务的美化,这些都是加分项,但不是根基。
最后再分享一个小技巧:上线初期,需求一定会变,我建议数据库字段尽量用可扩展的格式,比如把用户的偏好设置、服务者的服务标签都用JSON字段存,这样前端加需求时后端不用频繁改表,能省下很多加班时间。