starmall:一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析
- 约 14 分钟读完 - 全文 5330 字starmall:一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析
这是我 2021 年做的一套完整电商系统 starmall(六爻星辰):微信小程序端 + Java 后端 + PC 管理后台,支持商品购买、老师咨询时段预约、择吉日查询、在线咨询等业务。整理开源出来,写篇文章做一次完整的技术复盘。
- 前端(uni-app 小程序):https://gitee.com/soulnew/starmall
- 后端(Java + 管理端):https://gitee.com/soulnew/crm_server
- 技术栈:Spring Boot 2.2 + MyBatis-Plus + MySQL + Redis / uni-app + uView / Vue 2 + Element UI(管理端)
- 说明:后端是在开源商城 CRMEB Java 版 基础上二次开发的,分销/秒杀/拼团/砍价等营销能力来自底座,预约系统、择吉日、名片、咨询聊天等是自研部分
一、整体架构
graph TD
MP["微信小程序端<br/>starmall uni-app + uView"] -->|"https://ftmao.com/api/front/**"| API["SpringBoot 单体后端<br/>62 个 Mapper / 23 个前台 Controller"]
H5["H5 / App<br/>uni-app 多端编译"] --> API
ADMIN["PC 管理后台<br/>Vue 2 + Element UI + ECharts"] -->|"后台接口"| API
API --> MYSQL["MySQL 5.7<br/>62 张表 Druid 连接池"]
API --> REDIS["Redis<br/>token / 订单缓存 / 任务队列 / 配置缓存"]
API --> WX["微信开放平台<br/>登录 / 支付 V2 / 订阅消息"]
API --> QN["七牛云 OSS<br/>图片上传"]
API --> QY["企业微信机器人<br/>部署结果通知"]
MP -->|"wss://... websocket"| CHAT["咨询聊天服务"]
代码规模:后端约 850 个 Java 文件、12.6 万行;小程序前端业务代码约 1.2 万行;数据库初始化 SQL 全量在仓库里,共 62 张表。后端按业务分包:front(小程序接口)、order/payment、marketing、seckill、task(定时任务)、wechat,以及自研的 appoint(预约)、lunar(黄历)等模块。
二、功能介绍
2.1 商品购买全流程
标准的商城闭环:商品详情(多规格 SKU)→ 立即购买/加购物车 → 确认订单(优惠券、积分抵扣、运费计算)→ 创建订单 → 微信支付 → 轮询支付结果 → 收货评价 → 退款/售后。支付是真实对接的微信支付 V2——后端手工实现 XML 报文 + MD5 签名调统一下单(WeChatPayServiceImpl,746 行),前端弹起 wx.requestPayment,不是 mock。
此外还支持:余额支付、运费模板(满额包邮/指定城市包邮/按件数重量体积三种首续费)、物流查询、自提核销码、再次下单。营销玩法(秒杀、砍价、拼团、优惠券)来自 CRMEB 底座,一应俱全。
2.2 老师咨询时段预约(自研核心)
这是这个项目区别于普通商城的地方——卖的不只是实物商品,还有"老师的时间":
- 商品分类为"老师类"时,商品详情页出现一个时段选择器(每天 6 个时段:9/11/13/15/17/19 点);
teacher_plan表按「日期 × 时段」预生成号源,被约走的时段前端置灰;- 下单时携带
teacherPlanId,创建订单的同一个事务里把该时段标记为已约——号源占用和订单创建要么一起成功要么一起回滚,不会出现"钱付了时段没了"; - 号源由定时任务
TeacherPlanTask每 8 小时自动向前补齐未来 8 天,并清理过期的空闲号源。
2.3 其他自研功能
- 择吉日查询:4.7 万行黄历数据入库(
lunar系列表),按"宜"事反查吉日,比如查"最近适合搬家的日子"; - 在线咨询:基于 WebSocket 长连接的实时聊天,支持文字/语音/图片消息(详见第三章「长连接聊天」);
- 老师名片、文章资讯、签到积分、充值、小程序订阅消息提醒;
- 管理后台:商品/订单/营销/分销/财务/短信/按钮级权限/数据统计大屏(ECharts)。
三、后端能力全景
后端虽然是 SpringBoot 单体,但能力面相当完整:前台 23 个 Controller、62 个 MyBatis Mapper,按业务分包组织:
graph TD
subgraph M1["业务模块"]
A1["front 前台接口<br/>23 个 Controller"]
A2["order / payment<br/>订单与支付"]
A3["marketing / seckill / bargain / combination<br/>优惠券 秒杀 砍价 拼团"]
A4["appoint / lunar / businessCard<br/>预约 择吉日 名片(自研)"]
A5["finance / statistics / export<br/>充值提现 数据统计 Excel 导出"]
A6["wechat / wechatMessage / sms<br/>微信生态与短信"]
A7["task<br/>定时任务体系"]
end
subgraph M2["公共层"]
B1["拦截器链<br/>前台 Token / 后台 Token+权限 / Swagger"]
B2["工具层 RedisUtil / WxPayUtil / CrmebUtil"]
B3["ThreadLocal 上下文传递当前用户"]
end
A1 --> B1
A2 --> B2
B1 --> B3
商城主链路之外,后端还有一批容易被忽略的能力:
- 微信公众号消息自动回复:
wechatMessage模块支持文字/语音/图片/图文四种回复类型,关注、取关、扫码等事件都有对应处理; - 模板消息与订阅消息:小程序订阅消息、公众号模板消息的增删查与群发,且发送走异步任务(
AsyncWeChatProgramTempMessage等),不阻塞主流程; - 渠道二维码与素材管理:带参数的推广二维码、微信素材的上传管理;
- 数据导出与统计:POI 实现的 Excel 导出、交易/商品/用户多维统计接口(配合管理端 ECharts 大屏);
- 短信与物流:短信验证码/通知、快递物流轨迹查询;
- 接口文档:Swagger 在线文档 + 仓库内置 admin/app/公共三份离线接口文档。
3.1 长连接聊天(WebSocket)架构
在线咨询是这个系统的"第二前台",采用的是 WebSocket 长连接 + HTTP 接口的组合方案。部署上两个服务跑在同一台服务器,由 nginx 按路径前缀分流:
graph TD
MP["小程序聊天页<br/>pages/chat"] -->|"wss://.../websocket/userId/sessionId"| NGX["nginx 按路径分流"]
MP -->|"HTTP /chat/session/** /chat/msg/**"| NGX
MP -->|"HTTP /api/front/**"| NGX
NGX --> CHAT["聊天服务<br/>WebSocket 长连接 + 会话/消息接口"]
NGX --> MALL["商城后端 crm_server<br/>SpringBoot 18080"]
从开源的小程序端实现和接口契约,可以看到这套长连接聊天的几个设计要点:
- 连接即身份:WebSocket 握手 URL 直接携带
userId/sessionId,连接建立的同时就完成了身份识别与会话路由,不需要额外的登录帧; - 实时走长连接,持久化走 HTTP:收发消息走 WebSocket(消息体为 JSON);会话绑定(
session/bindSession)、会话列表、删除会话、历史消息拉取(msg/msgList)都走/chat/前缀的 HTTP 接口——长连接管"实时推送",HTTP 管"可查询、可回溯",职责分得很干净; - 统一的消息类型协议:
contentType1=文字、2=语音(带contentDuration时长)、3=图片(先经user/upload/image上传拿到 URL,再把 URL 作为消息内容发出); - 语音消息的移动端交互:按住说话(
touchstart/touchend控制录音起止)、上滑取消(touchmove/touchcancel),体验和微信原生聊天对齐; - 发送体验乐观化:消息发出后先本地 push 进消息列表立即上屏,
send的 fail 回调再 toast 兜底提示。
需要如实说明:聊天服务端的代码没有随这两个仓库开源(它部署在同一台服务器上,由 nginx 分流),仓库里能看到的生产实现是小程序端;crm_server 里的 com/im/SocketServer.java 只是当时练手的原生 TCP Socket demo(12345 端口),并非线上实现。
四、技术亮点
4.1 下单幂等:两段式 + 唯一键兜底
小程序网络环境下用户双击、重试、断网重发是常态,下单接口必须幂等。这里用了一个两段式设计:
graph TD
A["order/confirm 确认订单页"] --> B["订单数据序列化存 Redis<br/>key = md5 当前时间<br/>有效期 60 分钟"]
B --> C["order/create/orderKey 创建订单"]
C --> D{"第一步:查库判重<br/>StoreOrder.unique = orderKey"}
D -->|"已存在"| E["直接返回已有订单<br/>不重复创建"]
D -->|"不存在"| F{"第二步:校验 Redis 缓存<br/>是否过期/被用过"}
F -->|"过期"| G["拒绝 提示重新确认"]
F -->|"有效"| H["事务内创建订单<br/>成功后删除缓存"]
unique 字段是最后的兜底——即使 Redis 挂了、缓存丢了,数据库唯一约束也保证同一个 orderKey 不会建出两张单。
4.2 原子扣库存防超卖
库存扣减没有用"先查后改",而是一条 SQL 完成:
// setSql + 条件改写,数据库层面原子完成
stock = stock - n 且要求 stock - n >= 0数据库单行更新的原子性天然挡住并发超卖,不需要分布式锁。秒杀/砍价/拼团场景更复杂:同一事务内要扣活动表 + 活动 SKU 表 + 商品表 + 商品 SKU 表四份库存,任何一步失败整体回滚。
4.3 编程式事务包住整个建单过程
创建订单是个多写操作:扣库存、占预约时段、写订单主表、写订单详情、核销优惠券、写订单日志。代码用 TransactionTemplate 编程式事务(而不是 @Transactional 注解)把这一串操作包起来——好处是事务边界精确可控,不会把整个方法(包括不需要事务的远程调用)都圈进去。订单核心逻辑集中在 OrderUtils.java(1500+ 行)和 OrderServiceImpl.java(近 1000 行)。
4.4 支付回调:防重 + 异步化
微信支付的回调处理(CallbackServiceImpl.weChat,448 行)有几个关键点:
- 解析 XML、校验
return_code/result_code,按attach里携带的 userId + type 区分是订单支付还是充值; - 已支付直接返回 SUCCESS——微信会多次重推回调,幂等是回调接口的生死线;
- 事务内完成
updatePaid(置订单已支付)+ 扣积分 + 拼团状态处理; - 后续的"慢动作"——发短信、推订阅消息、分销结算——不放在回调里同步做,而是
lPush进 Redis 队列异步执行,回调接口尽快给微信返回,避免超时重推。
4.5 没有 MQ:Redis List + 定时任务的务实替代
项目没有引入 RabbitMQ/RocketMQ,而是用 Redis List 当队列 + Spring @Scheduled 定时任务消费,撑起了一整套异步任务体系:
graph LR
A["下单成功<br/>lPush ORDER_AUTO_CANCEL_KEY"] --> Q["Redis List 队列"]
P["支付成功回调<br/>lPush 后续动作"] --> Q
Q --> T1["OrderAutoCancelTask<br/>每分钟消费<br/>超时未支付自动取消 + 回滚库存"]
Q --> T2["自动收货 / 自动好评"]
Q --> T3["优惠券过期 / 砍价截止<br/>佣金解冻"]
Q --> T4["TeacherPlanTask<br/>每8小时补齐号源"]
task/order/ 下共有 8 个定时任务。对这个体量的系统,省掉一个 MQ 的部署运维成本,换来的是完全够用的可靠性——这是小项目技术选型上我很认可的一个取舍。
4.6 鉴权链路:UUID Token + Redis + ThreadLocal
没有用 JWT,而是更传统的方案:登录后签发 UUID 存 Redis(按模块区分 key 前缀),拦截器 FrontTokenInterceptor → CheckFrontToken → TokenManagerImpl 逐级校验,通过后把当前用户放进 ThreadLocal 供全链路取用。管理端是 Token + 权限双拦截器。好处是服务端可随时踢人下线(删 Redis key 即可),这是纯 JWT 做不到的。
4.7 其他值得提的实现
- 订单快照:下单时把购物车内容序列化成 JSON 存进
StoreOrderInfo.info,之后商品改价改名都不影响历史订单的展示; - 运费计算引擎:
getOrderPriceGroup支持满额包邮、指定城市包邮(件数 + 金额双条件)、按件数/重量/体积三种首续计费模式; - XSS 转义:用户输入统一
escapeHtml4; - 部署脚本:
start.shnohup 启动 + 轮询日志判断启动成功 + 企业微信机器人推送部署结果——单机部署也搞出了仪式感。
五、复盘与遗憾
- 底座与自研的边界:秒杀/拼团/砍价/分销这些"重"能力直接复用 CRMEB 底座,把精力集中在业务差异化(预约、择吉日、聊天)上——对个人开发者来说这是正确的杠杆;
- 单体的局限:所有东西在一个 SpringBoot 应用里,定时任务、支付、前台接口互相影响,量大了必然要拆;
- 原子扣库存的返回值没检查:
setSql扣减失败(库存不足)时影响的行数为 0,但代码没有校验这个返回值,极端情况下会漏判超卖——这是现在回头看想补的一个洞; - 聊天服务端未开源:如第三章所述,长连接聊天的服务端实现没有随仓库开源,小程序端和接口契约是完整的,算是开源的一点遗憾。
六、写在最后
这个项目让我完整走了一遍"真实收钱"的系统该有的样子:幂等、事务、防超卖、回调防重、异步补偿——每一个点背后都是电商系统趟过的坑。代码在 https://gitee.com/soulnew/starmall 和 https://gitee.com/soulnew/crm_server,建库 SQL、接口文档都在仓库里,希望能给做小程序商城的朋友一个可参照的完整样本。