CASE STUDY / GO TRANSACTION SYSTEM
Homestay
01 / BACKEND BRIEF
为什么这个预订系统值得单独设计
民宿预订并不是简单的房源 CRUD:同一房型的库存按入住日期变化,价格也可能按天不同;用户连击下单、订单超时和支付回调重试还会同时修改库存与订单状态。项目把 MySQL 作为金额和库存的事实来源,把 Redis 限定为可失效的房态缓存,并将价格计算、库存锁定和支付确认收敛在服务端事务边界内。
- 用按日库存与价格模型表达可售房态,而不是只保存房型总库存
- 让服务端重新计算报价,并用事务与行锁保护并发下单
- 通过 Idempotency-Key 和唯一约束抵御重复提交与网络重试
- 把微信支付准备、回调验签和支付确认接入订单状态链路
- 在不影响 MySQL 事实来源的前提下,用 Redis 缓存房态读取
02 / BOOKING FLOW
一次预订如何走完库存与支付链路
- 01
Discover
微信小程序通过 Gin 路由获取房源、房型和指定日期范围的房态日历;日历读取优先访问 Redis,未命中或缓存异常时回到 MySQL。
- 02
Preview
客户端提交入住/退房日期和房间数,Order Service 从数据库读取每日价格与库存,在服务端计算总价并检查每晚可用量。
- 03
Create
创建订单必须携带 Idempotency-Key;事务先查询既有幂等订单,再按日期升序锁定库存行,增加 locked_stock 并写入订单和订单夜间快照。
- 04
Expire
独立 worker 每 5 秒扫描已过期的待支付订单,复用取消逻辑释放 locked_stock,并使对应房态缓存失效。
- 05
Pay
支付准备阶段在生产环境调用微信支付 API v3 创建 JSAPI 预支付单,开发环境才允许使用显式的 mock 支付路径。
- 06
Confirm
微信回调经过 API v3 验签与解密后进入确认事务;服务端锁定订单和库存,将 locked_stock 转为 sold_stock,创建唯一支付记录并将重复通知安全地幂等返回。
03 / ENGINEERING DECISIONS
代码中真实存在的后端设计
- 01
用模块化单体承载交易域
实现server/internal 按 auth、user、homestay、room、order、payment 划分领域包,各自保留 model、repository、service 和 handler 边界;Gin router 只负责组装依赖和路由。
价值在单体部署复杂度可控的前提下,把房态、订单和支付的业务规则集中在明确的服务边界中,后续仍有拆分或替换实现的空间。
- 02
MySQL 是金额与库存的事实来源
实现金额统一使用 int64 分,订单保存房源、房型和每日价格快照;Redis 只保存带版本号的房态日历,缓存异常时 fail-open 回源 MySQL。
价值避免浮点金额误差和缓存写入失败导致的库存错乱,也让支付金额校验可以对照订单持久化数据完成。
- 03
按日库存模型表达真实可售量
实现room_inventory_daily 为每个房型和日期保存 total_stock、locked_stock、sold_stock、daily_price 和 closed;退房日按 checkout exclusive 处理,最多预览 93 天、预订 30 晚。
价值价格和库存可以随日期变化,周末价、关闭日期和跨夜预订都能在服务端以同一套数据模型校验。
- 04
幂等键与唯一索引共同保护下单
实现orders 使用 user_id + idempotency_key 联合唯一约束;Order Service 在事务中先查既有订单,遇到并发唯一键竞争时再读取已创建订单并返回。
价值网络重试、按钮连击或客户端超时不会轻易产生重复订单,幂等语义由数据库约束和服务逻辑共同兜底。
- 05
固定锁顺序并用行锁防止超卖
实现创建、取消和支付确认都会按日期升序锁定库存行,使用 SELECT ... FOR UPDATE 检查 available stock,再移动 locked_stock / sold_stock;更新后还检查 RowsAffected 是否符合预期。
价值并发预订同一房型时由 MySQL 串行化关键库存判断,固定锁顺序也降低了跨日期锁竞争形成死锁的概率。
- 06
缓存采用版本号主动失效
实现房态缓存 key 包含 roomTypeID、版本、起止日期;库存发生变化后 Redis INCR 版本号,旧 key 自然失效,读写缓存失败不会阻断主交易。
价值不需要枚举并删除所有日期区间 key,就能让订单创建、取消和支付确认后的房态读取回到新版本。
- 07
支付网关与订单确认分离
实现Payment Service 通过 WechatGateway 抽象生产支付创建与通知验签;真正的库存扣减仍由本地确认事务完成,并以订单金额、商户号、AppID 和交易状态做校验。
价值第三方支付 SDK 不会直接改写业务库存,回调重复、金额异常或订单已过期时都有明确的本地状态判断。
- 08
迁移与运行角色分离
实现SQL migration 通过 go:embed 和 schema_migrations 记录版本,生产环境单独运行 migrate;API 和 worker 分别部署,避免把过期订单扫描塞进 HTTP 请求进程。
价值数据库结构变更和业务进程启动解耦,过期回收任务也能拥有独立的运行与扩容边界。
04 / CURRENT BOUNDARIES
当前实现的边界与风险
这些限制可以从现有 Go 代码、部署配置和运行方式直接确认,不应在项目介绍中包装成已经完成的生产能力。
- 高优先级
领域服务仍耦合 HTTP 响应类型
部分 service 直接返回 response.Error 或 net/http 语义,领域规则与传输层边界还没有完全分开。后续更换 RPC、异步任务或复用领域服务时,需要先统一错误码和领域错误映射。
- 高优先级
身份与跨域策略仍偏 MVP
JWT 使用自定义 HS256 实现,当前没有刷新/撤销链路;CORS 仍允许任意 Origin,生产环境还需要收紧可信来源并增加请求限流。
- 中优先级
可观测性与运行时生命周期尚未完整
当前混用 Gin Logger 与标准日志,缺少 request-id、指标和分布式追踪;API/worker 的 context、readiness、draining 以及数据库连接显式关闭也仍有完善空间。
- 中优先级
过期回收是轮询任务而不是可靠任务队列
worker 通过固定间隔扫描数据库并复用取消逻辑,适合 MVP 的低复杂度部署,但没有任务租约、重试退避、失败告警或多实例抢占语义。
- 中优先级
订单状态模型已经预留但业务驱动仍有限
模型包含 CHECKED_IN、COMPLETED、退款等扩展状态,但当前核心链路主要覆盖待支付、已确认、取消和支付回调;状态迁移规则还需要继续集中化。
05 / NEXT ITERATION
下一轮应该怎么做
- 01
统一领域错误、request-id、结构化日志、指标和追踪边界,形成可定位的交易观测链路
- 02
用 signal.NotifyContext、readiness/draining 和可取消 worker 收敛 API 与后台任务生命周期,并显式释放数据库与 Redis 连接
- 03
收紧 JWT、CORS 与支付回调的安全策略,增加请求限流和敏感操作审计
- 04
补充 OpenAPI 契约、缓存响应头与 ETag,减少前后端接口演进时的隐式约定
- 05
为过期回收和支付对账增加重试退避、失败记录与告警,明确异常订单的人工处理入口