Files
zt-closet-backend/.codebuddy/plans/会员额度不叠加_76777dd5.md
T
zhengchiliu b94dd6dffb feat(order): 重构退款全流程并新增申请退款中状态
- 订单状态新增「6 申请退款中」,用户申请退款原子流转并落待审核退款流水
- 退款失败回退订单至已支付并恢复被取消的权益发放任务
- 新增虚拟支付退款查单兜底任务,反查超时订单并自动订正
- 会员包来源额度不叠加,扣减/回退/回收口径统一为只取生效一份
- 退款回收流水按吊销前后有效剩余差值记账,避免无效回收记录
- 同步更新 README 与 Stitch 设计文档
2026-09-16 03:44:09 +08:00

6.0 KiB
Raw Blame History

name, overview, todos
name overview todos
会员额度不叠加 修复月度会员包与年度会员包赠送额度叠加的问题:会员包来源(source=1)的额度凭证不再求和,只取其中 amount 最大的一份计入有效额度;Daily 额度与 Cos 永久额度均适用,含扣减/回退/剩余计算全链路;其他来源(加量包/广告/体验)维持叠加;存量数据无需迁移。
id content status
quota-select-core 新增会员包生效份选择纯函数,改造 getEffectiveQuota 聚合(Cos 与每日额度) completed
id content status dependencies
cos-quota-chain 改造 Cos 扣减、剩余与回退链路复用选择规则,未生效份不参与扣减与回退 completed
quota-select-core
id content status dependencies
refund-expire-consistency 退款回收流水改为吊销前后重算差值记账,核对到期失效补记逻辑 completed
quota-select-core
id content status dependencies
quota-tests 补充选择/聚合/扣减/回退/退款单元测试并跑通构建与全量回归 completed
cos-quota-chain
refund-expire-consistency
id content status dependencies
docs-sync 更新接口文档与 README 的额度叠加规则说明 completed
quota-select-core

用户需求

修复"购买的月度会员与年度会员额度会叠加"的问题:额度规则调整为年度会员与月度会员额度相同、不叠加。

产品概述

用户同时持有多份会员包(如月度包 + 年度包)时,同类额度不再多份相加,只按"一份"生效;该规则即时生效,历史数据无需迁移。

核心功能

  • 额度聚合调整:会员包来源(source=1)的同类额度只取 amount 最大的一份计入总额;月/年额度相同时即等价于只有一份
  • 全链路一致:额度概览、额度校验、消费扣减、失败回退、退款回收、到期记账统一按单份口径计算
  • 范围限定:仅会员包来源不叠加;加量包、广告激励、体验会员等其他来源仍按原逻辑叠加
  • 存量兼容:历史凭证保留,查询/消费时按新聚合规则即时生效,无需数据修复
  • 用完顺延:选中份额度用尽后自动启用下一份有效会员包凭证,已购额度不浪费,且任一时刻展示额度不翻倍(如 200 变 100)

技术栈

沿用现有 Go + Gin + GORM(MySQL)+ Redis 架构;不改表结构、不引入新依赖、不做数据迁移。

实现思路

在 service 层引入"会员包生效份选择"纯函数,替换现有对全部凭证直接 SUM 的聚合方式:

  • 选择规则(仅 source=1 会员包):从有效凭证中排除已用尽(amount-used<=0)的份,按 amount 降序 → used 降序(保持正在使用的一份,避免剩余展示抖动)→ id 降序(新购优先) 取第一条;无候选则会员包贡献为 0。
  • 其他来源(source=2/3/4):维持 SUM 叠加,行为不变。
  • 改造点:
  1. getEffectiveQuota:总额 = 免费额度 + 选中份 amount + 其他来源 SUM;Cos 已用 = 免费已用 + 选中份 used + 其他来源 used;每日额度仅改 Total(已用为当日全局计数)。额度概览 InfoGetBySelf 自动继承。
  2. consumeCosQuota:事务内仍按"先到期先用"扣减其他来源凭证,会员包组只扣选中份,再扣免费额度;未生效份不参与扣减。
  3. cosRemainTx:免费剩余 + 其他来源剩余 + 选中份剩余,与展示口径一致。
  4. refundCosQuotaTx:先退免费,再退凭证;其他来源按原顺序回退,会员包仅回退选中份。
  5. buildQuotaRevokeFlows:改为"吊销前剩余 vs 排除该订单凭证后重算剩余"的差值记账(diff=0 不写流水),保证退款回收与实额一致;quota_expire.go 到期补记自动适配,核对即可。
  • 关键决策与权衡:选择为纯内存 O(n)(n 为用户凭证数,通常个位数),无新增 DB 往返;选择在每处自治重算,无需持久化状态;已用尽自动切换下一位,兼顾"不叠加展示"与"已购额度不浪费"。
  • 安全与可靠性:仍依赖凭证行的条件更新(used+n<=amount)防并发超扣;聚合失败保持既有降级告警策略,不新增泄密日志。

关键代码结构

// pickEffectiveMemberGrant 在有效凭证中选出会员包来源(source=1)的生效份:
// 排除已用尽,按 amount 降序 → used 降序 → id 降序取一份;无候选返回 nil。
// 纯函数:供 getEffectiveQuota / consumeCosQuota / cosRemainTx / refundCosQuotaTx 复用。
func pickEffectiveMemberGrant(grants []mysqlmodel.QuotaGrant) *mysqlmodel.QuotaGrant

目录结构

zt-closet-backend/
├── internal/service/
│   ├── service.go                 # [MODIFY] 新增 pickEffectiveMemberGrant 纯函数;改造 getEffectiveQuota、consumeCosQuota、cosRemainTx、refundCosQuotaTx,统一复用选择规则并更新注释
│   ├── order.go                   # [MODIFY] buildQuotaRevokeFlows 改为吊销前后重算差值记账(排除订单凭证后按同一规则聚合)
│   ├── quota_select_test.go       # [NEW] 选择与聚合纯函数单测:月+年同额只取一份、取最大、used 稳定性、用尽顺延、其他来源仍叠加
│   └── cos_quota_test.go          # [NEW] sqlmock 集成测试:Cos 扣减只落选中份、回退、剩余计算、退款回收流水差值
├── 接口文档.md                     # [MODIFY] 更新额度规则(629-653 行附近):会员包来源不叠加,其他来源仍叠加
└── README.md                      # [MODIFY] 同步额度叠加规则说明

测试与验证

  • 覆盖:月度+年度同时持有、amount 不同取最大、选中份用尽顺延、加量包/体验/广告仍叠加、Cos 消费/回退/退款回收、每日额度 Total 不翻倍。
  • 回归:go build ./...、go vet ./...、go test ./... 全量通过,确保现有发放幂等与补偿测试不受影响。