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