CASE STUDY / GO TRANSACTION SYSTEM

Homestay

核心交易链路完成

从房态日历到幂等支付的 Go 民宿预订后端

面向微信小程序的民宿预订 MVP。Homestay 使用 Go、Gin、GORM、MySQL、Redis 与微信支付 API v3,围绕按日房态、动态价格、服务端计价、幂等下单、库存锁定、订单过期释放和支付回调确认建立完整交易链路。

01 / BACKEND BRIEF

为什么这个预订系统值得单独设计

民宿预订并不是简单的房源 CRUD:同一房型的库存按入住日期变化,价格也可能按天不同;用户连击下单、订单超时和支付回调重试还会同时修改库存与订单状态。项目把 MySQL 作为金额和库存的事实来源,把 Redis 限定为可失效的房态缓存,并将价格计算、库存锁定和支付确认收敛在服务端事务边界内。

  • 用按日库存与价格模型表达可售房态,而不是只保存房型总库存
  • 让服务端重新计算报价,并用事务与行锁保护并发下单
  • 通过 Idempotency-Key 和唯一约束抵御重复提交与网络重试
  • 把微信支付准备、回调验签和支付确认接入订单状态链路
  • 在不影响 MySQL 事实来源的前提下,用 Redis 缓存房态读取
Homestay booking architecture showing the WeChat Mini Program, Nginx, Go API, MySQL, Redis, worker and WeChat Pay
微信小程序、Go API、MySQL 事实来源、Redis 房态缓存、过期订单 worker 与微信支付之间的边界。

02 / BOOKING FLOW

一次预订如何走完库存与支付链路

  1. 01

    Discover

    微信小程序通过 Gin 路由获取房源、房型和指定日期范围的房态日历;日历读取优先访问 Redis,未命中或缓存异常时回到 MySQL。

  2. 02

    Preview

    客户端提交入住/退房日期和房间数,Order Service 从数据库读取每日价格与库存,在服务端计算总价并检查每晚可用量。

  3. 03

    Create

    创建订单必须携带 Idempotency-Key;事务先查询既有幂等订单,再按日期升序锁定库存行,增加 locked_stock 并写入订单和订单夜间快照。

  4. 04

    Expire

    独立 worker 每 5 秒扫描已过期的待支付订单,复用取消逻辑释放 locked_stock,并使对应房态缓存失效。

  5. 05

    Pay

    支付准备阶段在生产环境调用微信支付 API v3 创建 JSAPI 预支付单,开发环境才允许使用显式的 mock 支付路径。

  6. 06

    Confirm

    微信回调经过 API v3 验签与解密后进入确认事务;服务端锁定订单和库存,将 locked_stock 转为 sold_stock,创建唯一支付记录并将重复通知安全地幂等返回。

03 / ENGINEERING DECISIONS

代码中真实存在的后端设计

  1. 01

    用模块化单体承载交易域

    实现server/internal 按 auth、user、homestay、room、order、payment 划分领域包,各自保留 model、repository、service 和 handler 边界;Gin router 只负责组装依赖和路由。

    价值在单体部署复杂度可控的前提下,把房态、订单和支付的业务规则集中在明确的服务边界中,后续仍有拆分或替换实现的空间。

  2. 02

    MySQL 是金额与库存的事实来源

    实现金额统一使用 int64 分,订单保存房源、房型和每日价格快照;Redis 只保存带版本号的房态日历,缓存异常时 fail-open 回源 MySQL。

    价值避免浮点金额误差和缓存写入失败导致的库存错乱,也让支付金额校验可以对照订单持久化数据完成。

  3. 03

    按日库存模型表达真实可售量

    实现room_inventory_daily 为每个房型和日期保存 total_stock、locked_stock、sold_stock、daily_price 和 closed;退房日按 checkout exclusive 处理,最多预览 93 天、预订 30 晚。

    价值价格和库存可以随日期变化,周末价、关闭日期和跨夜预订都能在服务端以同一套数据模型校验。

  4. 04

    幂等键与唯一索引共同保护下单

    实现orders 使用 user_id + idempotency_key 联合唯一约束;Order Service 在事务中先查既有订单,遇到并发唯一键竞争时再读取已创建订单并返回。

    价值网络重试、按钮连击或客户端超时不会轻易产生重复订单,幂等语义由数据库约束和服务逻辑共同兜底。

  5. 05

    固定锁顺序并用行锁防止超卖

    实现创建、取消和支付确认都会按日期升序锁定库存行,使用 SELECT ... FOR UPDATE 检查 available stock,再移动 locked_stock / sold_stock;更新后还检查 RowsAffected 是否符合预期。

    价值并发预订同一房型时由 MySQL 串行化关键库存判断,固定锁顺序也降低了跨日期锁竞争形成死锁的概率。

  6. 06

    缓存采用版本号主动失效

    实现房态缓存 key 包含 roomTypeID、版本、起止日期;库存发生变化后 Redis INCR 版本号,旧 key 自然失效,读写缓存失败不会阻断主交易。

    价值不需要枚举并删除所有日期区间 key,就能让订单创建、取消和支付确认后的房态读取回到新版本。

  7. 07

    支付网关与订单确认分离

    实现Payment Service 通过 WechatGateway 抽象生产支付创建与通知验签;真正的库存扣减仍由本地确认事务完成,并以订单金额、商户号、AppID 和交易状态做校验。

    价值第三方支付 SDK 不会直接改写业务库存,回调重复、金额异常或订单已过期时都有明确的本地状态判断。

  8. 08

    迁移与运行角色分离

    实现SQL migration 通过 go:embed 和 schema_migrations 记录版本,生产环境单独运行 migrate;API 和 worker 分别部署,避免把过期订单扫描塞进 HTTP 请求进程。

    价值数据库结构变更和业务进程启动解耦,过期回收任务也能拥有独立的运行与扩容边界。

04 / CURRENT BOUNDARIES

当前实现的边界与风险

这些限制可以从现有 Go 代码、部署配置和运行方式直接确认,不应在项目介绍中包装成已经完成的生产能力。

  1. 高优先级

    领域服务仍耦合 HTTP 响应类型

    部分 service 直接返回 response.Error 或 net/http 语义,领域规则与传输层边界还没有完全分开。后续更换 RPC、异步任务或复用领域服务时,需要先统一错误码和领域错误映射。

  2. 高优先级

    身份与跨域策略仍偏 MVP

    JWT 使用自定义 HS256 实现,当前没有刷新/撤销链路;CORS 仍允许任意 Origin,生产环境还需要收紧可信来源并增加请求限流。

  3. 中优先级

    可观测性与运行时生命周期尚未完整

    当前混用 Gin Logger 与标准日志,缺少 request-id、指标和分布式追踪;API/worker 的 context、readiness、draining 以及数据库连接显式关闭也仍有完善空间。

  4. 中优先级

    过期回收是轮询任务而不是可靠任务队列

    worker 通过固定间隔扫描数据库并复用取消逻辑,适合 MVP 的低复杂度部署,但没有任务租约、重试退避、失败告警或多实例抢占语义。

  5. 中优先级

    订单状态模型已经预留但业务驱动仍有限

    模型包含 CHECKED_IN、COMPLETED、退款等扩展状态,但当前核心链路主要覆盖待支付、已确认、取消和支付回调;状态迁移规则还需要继续集中化。

05 / NEXT ITERATION

下一轮应该怎么做

  1. 01

    统一领域错误、request-id、结构化日志、指标和追踪边界,形成可定位的交易观测链路

  2. 02

    用 signal.NotifyContext、readiness/draining 和可取消 worker 收敛 API 与后台任务生命周期,并显式释放数据库与 Redis 连接

  3. 03

    收紧 JWT、CORS 与支付回调的安全策略,增加请求限流和敏感操作审计

  4. 04

    补充 OpenAPI 契约、缓存响应头与 ETag,减少前后端接口演进时的隐式约定

  5. 05

    为过期回收和支付对账增加重试退避、失败记录与告警,明确异常订单的人工处理入口