Files
zt-closet-backend/.codebuddy/plans/额度体系重构_每日额度_凭证有效期_广告激励_defdda63.md
2026-09-11 01:41:52 +08:00

9.9 KiB
Raw Permalink Blame History

name, overview, todos
name overview todos
额度体系重构:每日额度+凭证有效期+广告激励 重构额度体系:引入统一额度凭证表(quota_grants)承载会员包/加量包/广告激励三类有有效期赠送;AI/Outfit/Closet 改为每日重置额度,Cos 保持永久库存;新增广告激励发放接口与加量包有效期配置。
id content status
dao-migration 新增 quota_grants 凭证表与 goods.valid_days 迁移,扩展模型与 DAO(凭证 CRUD/汇总/扣减) completed
id content status
redis-daily 实现 Redis 每日额度原子限流:带限额 Lua 脚本及 consume/refund/value 封装 completed
id content status dependencies
service-core 实现 Service 层额度核心:getEffectiveQuota、consumeDailyQuota、refundDailyQuota、Cos 凭证消费 completed
dao-migration
redis-daily
id content status dependencies
biz-switch 切换 AI/Outfit/Closet/Cos 业务使用点为每日额度与凭证消费 completed
service-core
id content status dependencies
order-benefit 改造订单权益发放/退款/补偿链路为凭证模型,仅保留 VIP 实际发放 completed
dao-migration
service-core
id content status dependencies
ad-reward 新增广告激励发放接口(服务/路由/DTO/幂等),创建当天有效凭证 completed
dao-migration
service-core
id content status dependencies
info-config-doc 更新额度信息接口响应、配置语义注释与 README/接口文档 completed
biz-switch
order-benefit
ad-reward

产品概述

将当前 Cos/AI/Outfit/Closet 四类统一永久累计的额度体系,重构为「永久额度 + 每日额度 + 有有效期赠送凭证」的混合模型,并新增广告激励额度来源。

核心功能

免费基础额度

  • Cos 免费上传额度(配置 20):永久,不每天重置。
  • AI/Outfit/Closet 免费额度(配置 3/3/0):每日额度,每天 0 点重置;ClosetShare 为 0 表示新用户无免费衣柜分享次数。

有有效期赠送额度(统一凭证模型)

  • 会员包赠送:随 VIP 有效期,VIP 续期时自动延长(跟随 vip_expire 动态变化)。
  • 加量包赠送:独立有效期,每个商品可配置 valid_days 天数,到期失效。
  • 广告激励:可配置多类额度(AI/Cos 等),当天有效(当天 23:59:59 到期),后端新增发放接口并幂等防重复领取。

额度计算与消耗

  • Cos:有效总额度 = 免费配置 + 有效凭证剩余(amount-used);used 永久累计,凭证按先到期先用。
  • AI/Outfit/Closet:每日总额度 = 免费配置 + 有效凭证总额;当日已用每天 0 点清零。
  • 消耗顺序:免费与赠送合并为单一额度池,无需区分顺序。

额度信息接口

  • Cos 返回永久总额度/累计已用/剩余;AI/Outfit/Closet 返回每日总额度/当日已用/当日剩余。

技术栈

  • 后端:Go + Gin(复用现有)
  • 数据层:GORM + MySQL(复用现有)
  • 缓存与原子计数:Redis + go-redis(复用现有,新增带限额 Lua 脚本)
  • 迁移:golang-migrate(复用现有 pkg/migration 机制)

实现方案

核心策略:统一额度凭证表 quota_grants

新增凭证表承载所有「有有效期赠送」来源(会员包/加量包/广告激励),不再把赠送额度累加进 user_quotas.total,而是每次查询/消费时动态计算有效总额度:

  • Cos 有效总额 = 免费配置 + SUM(有效凭证 amount - used)
  • AI/Outfit/Closet 每日总额 = 免费配置 + SUM(有效凭证 amount)

「有效凭证」= expire_time > now,或 expire_time=0 且 user.vip_expire > now(会员包跟随 VIP)。

分渠道消费

  • Cos(永久库存):MySQL 事务 + 凭证维度扣减(先到期先用,减少过期浪费),写 quota_flow_logs。
  • AI/Outfit/Closet(每日额度):Redis Lua 原子限流(读当日计数 → 判断小于每日总额 → INCR → 首次设置 48h TTL),MySQL user_daily_usages 保留为统计与兜底。

为什么这样设计

  • 赠送额度不落库为 total,动态计算天然跟随 VIP 状态、加量包有效期与退款状态,消除过期回收任务与账实不符风险。
  • Cos 需永久累计 used 并写流水,走 MySQL 事务;每日额度需高频原子增减与每日清零,走 Redis INCR + TTL。
  • 复用现有 user_daily_usages 表与 QuotaDaily* Redis 半成品,只补齐「带限额原子校验」与业务接入。

并发与一致性

  • 每日额度:Redis Lua 原子完成校验+计数,保证不超发;业务失败 refundDailyQuota(DECR 且不小于 0)。
  • Cos:事务内凭证行锁/条件更新,动态限额作为参数传入,避免并发超扣。
  • 广告激励:ref_id 唯一索引防重复领取。
  • 退款:将对应订单凭证失效/删除(未用部分回收),VIP 走现有 revokeVIP。

性能

  • SUM 有效凭证已有 user_id+quota_type+expire_time 索引可支撑;商品与单用户凭证量小,直接聚合开销可控。
  • 每日总额度可加 Redis 当日缓存(key 带 YYYYMMDD,TTL 到当天结束),订单支付/退款、VIP 变更时失效;作为增量优化。

架构设计

flowchart TD
    subgraph 额度查询/消费
        Q[GET /quota/info] --> E[getEffectiveQuota]
        C1[AI/Outfit/Closet 业务] --> D[consumeDailyQuota]
        C2[Cos 上传] --> P[consumeCosQuota]
        A[广告激励接口] --> G[写 quota_grants 当天凭证]
    end

    E --> V{vip_expire 或凭证 expire_time}
    V --> S[SUM 有效 quota_grants]
    S --> T[每日/永久总额度]

    D --> L[Redis Lua 原子限流]
    L --> M[user_daily_usages 统计]

    P --> TX[MySQL 事务凭证扣减]
    TX --> FLOW[写 quota_flow_logs]

    G --> QGRANT[quota_grants]

目录结构

internal/
├── mysqlmodel/
│   ├── quota.go            # [MODIFY] 新增 QuotaGrant 凭证模型;Quota 注释语义更新
│   └── goods.go            # [MODIFY] 新增 ValidDays 字段
├── mysqldao/
│   ├── quota.go            # [MODIFY] 凭证 CRUD/汇总/扣减;Cos 动态限额消费
│   └── goods.go            # [MODIFY] ValidDays 相关查询/更新
├── service/
│   ├── service.go          # [MODIFY] getEffectiveQuota、consumeDailyQuota、refundDailyQuota、Cos 消费改造
│   ├── constants.go        # [MODIFY] 额度来源/有效期常量补充
│   ├── quota.go            # [MODIFY] ensureDefaultQuota 调整;InfoGetBySelf 返回每日额度
│   ├── airecommend.go      # [MODIFY] AI 消费改 consumeDailyQuota
│   ├── outfit.go           # [MODIFY] OutfitShare 消费改 consumeDailyQuota
│   ├── closet.go           # [MODIFY] ClosetShare 消费改 consumeDailyQuota
│   ├── clothes.go          # [MODIFY] Cos 消费适配凭证扣减
│   ├── order.go            # [MODIFY] 发放/退款改写凭证,不再 grantQuota 累加 total
│   ├── benefit_compensation.go # [MODIFY] 额度类补偿任务兼容
│   └── adreward.go         # [NEW] 广告激励发放服务(按类型、当天有效、幂等)
├── routers/
│   ├── api/v1/adreward.go  # [NEW] 广告激励发放 handler
│   └── router.go           # [MODIFY] 注册广告激励路由
├── redisdao/
│   └── quota.go            # [MODIFY] 带限额 Lua 脚本 + DailyQuotaConsume/Refund/Value
├── redismodel/
│   └── quota.go            # [MODIFY] 每日额度 key 定义复用/补充
└── dto/
    ├── quota.go            # [MODIFY] 响应结构区分永久/每日额度语义
    ├── goods.go            # [MODIFY] 响应增加 valid_days
    └── adreward.go         # [NEW] 广告激励发放请求/响应

pkg/
├── migration/              # [NEW] quota_grants 表、goods.valid_days、广告激励相关索引
└── setting/section.go      # [MODIFY] 配置语义注释/新增广告激励配置

global/setting.go           # [MODIFY] 配置解析补充
configs/config.example.yaml # [MODIFY] 配置语义注释
README.md                   # [MODIFY] 额度语义说明
接口文档.md                  # [MODIFY] /quota/info 与广告激励接口文档

关键代码结构

// internal/mysqlmodel/quota.go
// QuotaGrant 有有效期额度凭证(会员包/加量包/广告激励)
type QuotaGrant struct {
    Model
    UserID     int64 `gorm:"column:user_id;index:idx_grant_user_type_expire,priority:1"`
    QuotaType  int8  `gorm:"column:quota_type;index:idx_grant_user_type_expire,priority:2"`
    Amount     int64 `gorm:"column:amount"`      // 赠送总量
    Used       int64 `gorm:"column:used"`        // 已用(Cos 凭证扣减用)
    Source     int8  `gorm:"column:source"`      // 1=会员包 2=加量包 3=广告激励
    ExpireTime int64 `gorm:"column:expire_time;index:idx_grant_user_type_expire,priority:3"` // 0=跟随VIP
    RefID      string `gorm:"column:ref_id;uniqueIndex:uk_grant_ref"`
}
// internal/service/service.go
// getEffectiveQuota 返回某类额度有效总额度(Cos 永久库存 / 每日额度上限)
func getEffectiveQuota(ctx context.Context, userID int64, quotaType int8) (int64, *errcode.Error)

// consumeDailyQuota 原子扣减 AI/Outfit/Closet 当日额度
func consumeDailyQuota(ctx context.Context, userID int64, quotaType int8, n int64, reason string) *errcode.Error

// refundDailyQuota 业务失败时回退当日计数
func refundDailyQuota(ctx context.Context, userID int64, quotaType int8, n int64)
// internal/dto/adreward.go
type AdRewardGrantRequest struct {
    RewardType string `json:"reward_type" binding:"required"` // ai/cos/... 可配置映射到 quota_type
    Amount     int64  `json:"amount" binding:"required,min=1"`
    RefID      string `json:"ref_id" binding:"required"`     // 广告记录ID,幂等键
}