This commit is contained in:
zhengchiliu
2026-09-15 00:53:04 +08:00
parent 32a0b87e9e
commit 112c5f8e46
42 changed files with 1121 additions and 304 deletions
+104 -1
View File
@@ -346,6 +346,80 @@ func (d *UserDailyUsageDao) UserDailyUsageIncrementClosetShare(ctx context.Conte
return nil
}
// UserDailyUsageAdjust 原子调整每日使用量;正数使用 upsert,负数保证不低于 0。
func (d *UserDailyUsageDao) UserDailyUsageAdjust(ctx context.Context, userID, statDate int64, quotaType int8, delta int64) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserDailyUsageAdjust")
defer span.End()
column := ""
switch quotaType {
case 2:
column = "ai_recommend_count"
case 3:
column = "outfit_share_count"
case 4:
column = "closet_share_count"
default:
return nil
}
var err error
if delta > 0 {
usage := &mysqlmodel.UserDailyUsage{UserID: userID, StatDate: statDate}
switch quotaType {
case 2:
usage.AIRecommendCount = delta
case 3:
usage.OutfitShareCount = delta
case 4:
usage.ClosetShareCount = delta
}
err = d.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "stat_date"}},
DoUpdates: clause.Assignments(map[string]any{column: gorm.Expr(column+" + ?", delta)}),
}).Create(usage).Error
} else if delta < 0 {
err = d.db.WithContext(ctx).Model(&mysqlmodel.UserDailyUsage{}).
Where("user_id = ? AND stat_date = ?", userID, statDate).
UpdateColumn(column, gorm.Expr("GREATEST(0, "+column+" + ?)", delta)).Error
}
if err != nil {
span.RecordError(err)
}
return err
}
// UserDailyUsageConsume 以 MySQL 为权威账本原子预占每日额度,避免 Redis 丢键后重复消费。
func (d *UserDailyUsageDao) UserDailyUsageConsume(ctx context.Context, userID, statDate int64, quotaType int8, n, limit int64) (bool, error) {
column := ""
switch quotaType {
case 2:
column = "ai_recommend_count"
case 3:
column = "outfit_share_count"
case 4:
column = "closet_share_count"
default:
return false, nil
}
var consumed bool
err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
usage := &mysqlmodel.UserDailyUsage{UserID: userID, StatDate: statDate}
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "stat_date"}}, DoNothing: true,
}).Create(usage).Error; err != nil {
return err
}
res := tx.Model(&mysqlmodel.UserDailyUsage{}).
Where("user_id = ? AND stat_date = ? AND "+column+" + ? <= ?", userID, statDate, n, limit).
UpdateColumn(column, gorm.Expr(column+" + ?", n))
if res.Error != nil {
return res.Error
}
consumed = res.RowsAffected > 0
return nil
})
return consumed, err
}
// UserDailyUsageGetByUserIDAndDate 查询用户某日使用计数
func (d *UserDailyUsageDao) UserDailyUsageGetByUserIDAndDate(ctx context.Context, userID int64, statDate int64) (*mysqlmodel.UserDailyUsage, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserDailyUsageGetByUserIDAndDate")
@@ -431,7 +505,7 @@ func NewQuotaGrantDao() *QuotaGrantDao {
// QuotaGrantCreateWithFlow 事务内创建额度凭证并写发放流水(购买赠送/活动赠送)。
// 返回值 created 表示本次是否新建了凭证:依赖 uk_grant_ref_type(ref_id, quota_type) 唯一索引实现幂等,
// 凭证已存在时返回 (false, nil) 且不写流水,避免重复回调/补偿重试重复记账
//(流水侧另有 (order_no, quota_type, change_type) 唯一索引兜底)。
// (流水侧另有 (order_no, quota_type, change_type) 唯一索引兜底)。
func (d *QuotaGrantDao) QuotaGrantCreateWithFlow(ctx context.Context, grant *mysqlmodel.QuotaGrant, flow *mysqlmodel.QuotaFlowLog) (bool, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantCreateWithFlow")
defer span.End()
@@ -459,6 +533,35 @@ func (d *QuotaGrantDao) QuotaGrantCreateWithFlow(ctx context.Context, grant *mys
return created, nil
}
// AdRewardGrantCreateWithFlow 在用户行锁内执行每日限额、事件幂等、凭证与流水原子写入。
func (d *QuotaGrantDao) AdRewardGrantCreateWithFlow(ctx context.Context, grant *mysqlmodel.QuotaGrant, flow *mysqlmodel.QuotaFlowLog, dayStart, dayEnd int64, maxClaims int) (bool, error) {
created := false
err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var user mysqlmodel.User
if err := tx.Select("id").Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", grant.UserID).First(&user).Error; err != nil {
return err
}
var count int64
if err := tx.Model(&mysqlmodel.QuotaGrant{}).Where("user_id = ? AND quota_type = ? AND source = ? AND created_at >= ? AND created_at < ?", grant.UserID, grant.QuotaType, grant.Source, dayStart, dayEnd).Count(&count).Error; err != nil {
return err
}
if count >= int64(maxClaims) {
return nil
}
res := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "ref_id"}, {Name: "quota_type"}}, DoNothing: true}).Create(grant)
if res.Error != nil || res.RowsAffected == 0 {
return res.Error
}
ensureFlowOrderNo(flow)
if err := tx.Create(flow).Error; err != nil {
return err
}
created = true
return nil
})
return created, err
}
// QuotaGrantListByUserIDAndType 查询用户某类型全部凭证(未删除)。
// 按到期时间升序(expire_time=0 表示跟随 VIP,排最后),配合「先到期先用」扣减策略。
func (d *QuotaGrantDao) QuotaGrantListByUserIDAndType(ctx context.Context, userID int64, quotaType int8) ([]mysqlmodel.QuotaGrant, error) {