package mysqldao import ( "context" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/global" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel" "gorm.io/gorm" "gorm.io/gorm/clause" ) // QuotaDao 用户配额数据访问 type QuotaDao struct { db *gorm.DB } // NewQuotaDao 创建配额 DAO func NewQuotaDao() *QuotaDao { return &QuotaDao{db: global.MysqlEngine} } // QuotaCreate 创建配额记录 func (d *QuotaDao) QuotaCreate(ctx context.Context, quota *mysqlmodel.Quota) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaCreate") defer span.End() if err := d.db.WithContext(ctx).Create(quota).Error; err != nil { span.RecordError(err) return err } return nil } // QuotaIncreaseUsed 原子增加已用量(扣减配额),剩余不足时返回 false。 // 已废弃:仅改数值不写流水,存在账实不符风险;新代码请使用 QuotaConsumeWithFlow。 func (d *QuotaDao) QuotaIncreaseUsed(ctx context.Context, userID int64, quotaType int8, n int64) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaIncreaseUsed") defer span.End() res := d.db.WithContext(ctx).Model(&mysqlmodel.Quota{}). Where("user_id = ? AND quota_type = ? AND used + ? <= total", userID, quotaType, n). UpdateColumn("used", gorm.Expr("used + ?", n)) if res.Error != nil { span.RecordError(res.Error) return false, res.Error } return res.RowsAffected > 0, nil } // QuotaDecreaseUsed 原子减少已用量(回退配额),已用量不足时返回 false。 // 已废弃:仅改数值不写流水,存在账实不符风险;新代码请使用 QuotaRefundWithFlow。 func (d *QuotaDao) QuotaDecreaseUsed(ctx context.Context, userID int64, quotaType int8, n int64) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaDecreaseUsed") defer span.End() res := d.db.WithContext(ctx).Model(&mysqlmodel.Quota{}). Where("user_id = ? AND quota_type = ? AND used >= ?", userID, quotaType, n). UpdateColumn("used", gorm.Expr("used - ?", n)) if res.Error != nil { span.RecordError(res.Error) return false, res.Error } return res.RowsAffected > 0, nil } // QuotaConsumeWithFlow 事务内扣减配额并写流水,保证数值变更与流水强一致。 // 条件更新不满足(额度不足)返回 (false, nil);其余错误(含流水写入失败整体回滚)返回 error。 func (d *QuotaDao) QuotaConsumeWithFlow(ctx context.Context, userID int64, quotaType int8, n int64, flow *mysqlmodel.QuotaFlowLog) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaConsumeWithFlow") defer span.End() var ok bool err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { res := tx.Model(&mysqlmodel.Quota{}). Where("user_id = ? AND quota_type = ? AND used + ? <= total", userID, quotaType, n). UpdateColumn("used", gorm.Expr("used + ?", n)) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return nil // 额度不足,无变更。 } ok = true // 回读真实值填充流水 old/new。 var cur mysqlmodel.Quota if err := tx.Where("user_id = ? AND quota_type = ?", userID, quotaType).First(&cur).Error; err != nil { return err } remain := cur.Total - cur.Used flow.OldValue = remain + n flow.NewValue = remain // 写流水(失败则整个事务回滚,扣减一并撤销)。 ensureFlowOrderNo(flow) return tx.Create(flow).Error }) if err != nil { span.RecordError(err) return false, err } return ok, nil } // QuotaRefundWithFlow 事务内回退配额并写流水,保证数值变更与流水强一致。 // 条件更新不满足(已用量不足/重复回退)返回 (false, nil);其余错误(含流水失败回滚)返回 error。 func (d *QuotaDao) QuotaRefundWithFlow(ctx context.Context, userID int64, quotaType int8, n int64, flow *mysqlmodel.QuotaFlowLog) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaRefundWithFlow") defer span.End() var ok bool err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { res := tx.Model(&mysqlmodel.Quota{}). Where("user_id = ? AND quota_type = ? AND used >= ?", userID, quotaType, n). UpdateColumn("used", gorm.Expr("used - ?", n)) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return nil // 已用量不足,无变更。 } ok = true // 回读真实值填充流水 old/new。 var cur mysqlmodel.Quota if err := tx.Where("user_id = ? AND quota_type = ?", userID, quotaType).First(&cur).Error; err != nil { return err } remain := cur.Total - cur.Used flow.OldValue = remain - n flow.NewValue = remain // 写流水(失败则整个事务回滚,回退一并撤销)。 ensureFlowOrderNo(flow) return tx.Create(flow).Error }) if err != nil { span.RecordError(err) return false, err } return ok, nil } // QuotaInitWithFlow 事务内初始化配额并写流水。 // 依赖 uk_user_quota 唯一索引(OnConflict DoNothing)实现并发安全幂等:已存在则跳过且不写流水。 func (d *QuotaDao) QuotaInitWithFlow(ctx context.Context, userID int64, quotaType int8, total int64, flow *mysqlmodel.QuotaFlowLog) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaInitWithFlow") defer span.End() return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { q := &mysqlmodel.Quota{ UserID: userID, QuotaType: quotaType, Total: total, Used: 0, } res := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "user_id"}, {Name: "quota_type"}}, DoNothing: true, }).Create(q) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return nil // 已存在,幂等跳过。 } // 回读真实值填充流水 old/new。 var cur mysqlmodel.Quota if err := tx.Where("user_id = ? AND quota_type = ?", userID, quotaType).First(&cur).Error; err != nil { return err } remain := cur.Total - cur.Used flow.OldValue = remain - total flow.NewValue = remain // 写流水(失败则整个事务回滚,初始化一并撤销)。 ensureFlowOrderNo(flow) return tx.Create(flow).Error }) } // ensureFlowOrderNo 流水联合唯一索引 (order_no, quota_type, change_type) 的兜底: // 消费/回退/初始化流水无业务订单号时(order_no 为空串),MySQL 唯一索引将空串视为相同值, // 第二条同类流水会 1062 冲突。此处用流水自身 UUID 填充 order_no 保证唯一,不影响 // 购买/退款类幂等流水(其 order_no 非空,直接沿用业务订单号)。 func ensureFlowOrderNo(flow *mysqlmodel.QuotaFlowLog) { if flow != nil && flow.OrderNo == "" { flow.OrderNo = flow.QuotaFlowUUID } } // QuotaGrantIdempotent 幂等发放额度:事务内先累加 total(不存在则创建,存在则累加), // 回读真实值后写流水;流水唯一索引(order_no+quota_type+change_type)冲突时事务回滚, // 调用方据此识别为「已发放」从而幂等。 func (d *QuotaDao) QuotaGrantIdempotent(ctx context.Context, userID int64, quotaType int8, n int64, flow *mysqlmodel.QuotaFlowLog) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantIdempotent") defer span.End() return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1. 累加 total(记录不存在则创建,存在则原子累加)。 q := &mysqlmodel.Quota{ UserID: userID, QuotaType: quotaType, Total: n, Used: 0, } if err := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "user_id"}, {Name: "quota_type"}}, DoUpdates: clause.Assignments(map[string]interface{}{"total": gorm.Expr("total + ?", n)}), }).Create(q).Error; err != nil { return err } // 2. 回读真实值填充流水 old/new。 var cur mysqlmodel.Quota if err := tx.Where("user_id = ? AND quota_type = ?", userID, quotaType).First(&cur).Error; err != nil { return err } remain := cur.Total - cur.Used flow.OldValue = remain - n flow.NewValue = remain // 3. 写流水(唯一索引冲突会回滚整个事务,保证不重复发放)。 return tx.Create(flow).Error }) } // QuotaRevokeIdempotent 幂等回退额度(退款回收):事务内先扣减 total(GREATEST 保证 total 不低于 used, // 即已用部分不回退),回读真实值后写回退流水;流水唯一索引(order_no+quota_type+change_type)冲突时 // 事务回滚,调用方据此识别为「已回退」从而幂等。 func (d *QuotaDao) QuotaRevokeIdempotent(ctx context.Context, userID int64, quotaType int8, n int64, flow *mysqlmodel.QuotaFlowLog) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaRevokeIdempotent") defer span.End() return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1. 扣减 total(GREATEST(used, total - n) 保证 total >= used)。 res := tx.Model(&mysqlmodel.Quota{}). Where("user_id = ? AND quota_type = ?", userID, quotaType). UpdateColumn("total", gorm.Expr("GREATEST(used, total - ?)", n)) if res.Error != nil { return res.Error } // 2. 回读真实值填充流水 old/new。 var cur mysqlmodel.Quota if err := tx.Where("user_id = ? AND quota_type = ?", userID, quotaType).First(&cur).Error; err != nil { return err } remain := cur.Total - cur.Used flow.OldValue = remain + n flow.NewValue = remain // 3. 写回退流水(唯一索引冲突回滚整个事务,保证不重复回退)。 return tx.Create(flow).Error }) } // QuotaGetByUserIDAndType 查询用户某类配额 func (d *QuotaDao) QuotaGetByUserIDAndType(ctx context.Context, userID int64, quotaType int8) (*mysqlmodel.Quota, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGetByUserIDAndType") defer span.End() var quota mysqlmodel.Quota if err := d.db.WithContext(ctx).Where("user_id = ? AND quota_type = ?", userID, quotaType).First("a).Error; err != nil { span.RecordError(err) return nil, err } return "a, nil } // QuotaListByUserID 查询用户的所有配额 func (d *QuotaDao) QuotaListByUserID(ctx context.Context, userID int64) ([]mysqlmodel.Quota, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaListByUserID") defer span.End() var quotas []mysqlmodel.Quota if err := d.db.WithContext(ctx).Where("user_id = ?", userID).Find("as).Error; err != nil { span.RecordError(err) return nil, err } return quotas, nil } // UserDailyUsageDao 每日使用计数数据访问 type UserDailyUsageDao struct { db *gorm.DB } // NewUserDailyUsageDao 创建每日使用计数 DAO func NewUserDailyUsageDao() *UserDailyUsageDao { return &UserDailyUsageDao{db: global.MysqlEngine} } // UserDailyUsageCreate 创建每日使用计数 func (d *UserDailyUsageDao) UserDailyUsageCreate(ctx context.Context, usage *mysqlmodel.UserDailyUsage) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.UserDailyUsageCreate") defer span.End() if err := d.db.WithContext(ctx).Create(usage).Error; err != nil { span.RecordError(err) return err } return nil } // UserDailyUsageIncrementAI 当日AI推荐次数 +1 func (d *UserDailyUsageDao) UserDailyUsageIncrementAI(ctx context.Context, userID int64, statDate int64) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.UserDailyUsageIncrementAI") defer span.End() if err := d.db.WithContext(ctx).Model(&mysqlmodel.UserDailyUsage{}). Where("user_id = ? AND stat_date = ?", userID, statDate). UpdateColumn("ai_recommend_count", gorm.Expr("ai_recommend_count + ?", 1)).Error; err != nil { span.RecordError(err) return err } return nil } // UserDailyUsageIncrementOutfitShare 当日社区分享次数 +1 func (d *UserDailyUsageDao) UserDailyUsageIncrementOutfitShare(ctx context.Context, userID int64, statDate int64) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.UserDailyUsageIncrementOutfitShare") defer span.End() if err := d.db.WithContext(ctx).Model(&mysqlmodel.UserDailyUsage{}). Where("user_id = ? AND stat_date = ?", userID, statDate). UpdateColumn("outfit_share_count", gorm.Expr("outfit_share_count + ?", 1)).Error; err != nil { span.RecordError(err) return err } return nil } // UserDailyUsageIncrementClosetShare 当日衣柜分享次数 +1 func (d *UserDailyUsageDao) UserDailyUsageIncrementClosetShare(ctx context.Context, userID int64, statDate int64) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.UserDailyUsageIncrementClosetShare") defer span.End() if err := d.db.WithContext(ctx).Model(&mysqlmodel.UserDailyUsage{}). Where("user_id = ? AND stat_date = ?", userID, statDate). UpdateColumn("closet_share_count", gorm.Expr("closet_share_count + ?", 1)).Error; err != nil { span.RecordError(err) return err } 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") defer span.End() var usage mysqlmodel.UserDailyUsage if err := d.db.WithContext(ctx).Where("user_id = ? AND stat_date = ?", userID, statDate).First(&usage).Error; err != nil { span.RecordError(err) return nil, err } return &usage, nil } // QuotaFlowLogDao 配额流水数据访问 type QuotaFlowLogDao struct { db *gorm.DB } // NewQuotaFlowLogDao 创建配额流水 DAO func NewQuotaFlowLogDao() *QuotaFlowLogDao { return &QuotaFlowLogDao{db: global.MysqlEngine} } // QuotaFlowLogCreate 创建配额流水 func (d *QuotaFlowLogDao) QuotaFlowLogCreate(ctx context.Context, log *mysqlmodel.QuotaFlowLog) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaFlowLogCreate") defer span.End() if err := d.db.WithContext(ctx).Create(log).Error; err != nil { span.RecordError(err) return err } return nil } // QuotaFlowLogExistsByOrderNoAndType 判断指定订单+额度类型是否已有发放流水(补偿重试幂等判断)。 func (d *QuotaFlowLogDao) QuotaFlowLogExistsByOrderNoAndType(ctx context.Context, orderNo string, quotaType int8) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaFlowLogExistsByOrderNoAndType") defer span.End() var count int64 if err := d.db.WithContext(ctx).Model(&mysqlmodel.QuotaFlowLog{}). Where("order_no = ? AND quota_type = ?", orderNo, quotaType). Count(&count).Error; err != nil { span.RecordError(err) return false, err } return count > 0, nil } // QuotaFlowLogListByUserID 分页查询用户配额流水(quotaType 为 0 不筛选) func (d *QuotaFlowLogDao) QuotaFlowLogListByUserID(ctx context.Context, userID int64, quotaType int8, page, pageSize int) ([]mysqlmodel.QuotaFlowLog, int64, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaFlowLogListByUserID") defer span.End() var list []mysqlmodel.QuotaFlowLog var total int64 q := d.db.WithContext(ctx).Model(&mysqlmodel.QuotaFlowLog{}).Where("user_id = ?", userID) if quotaType != 0 { q = q.Where("quota_type = ?", quotaType) } if err := q.Count(&total).Error; err != nil { span.RecordError(err) return nil, 0, err } if err := q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&list).Error; err != nil { span.RecordError(err) return nil, 0, err } return list, total, nil } // QuotaGrantDao 额度凭证数据访问(有有效期赠送额度)。 type QuotaGrantDao struct { db *gorm.DB } // NewQuotaGrantDao 创建额度凭证 DAO。 func NewQuotaGrantDao() *QuotaGrantDao { return &QuotaGrantDao{db: global.MysqlEngine} } // QuotaGrantCreateWithFlow 事务内创建额度凭证并写发放流水(购买赠送/活动赠送)。 // 返回值 created 表示本次是否新建了凭证:依赖 uk_grant_ref_type(ref_id, quota_type) 唯一索引实现幂等, // 凭证已存在时返回 (false, nil) 且不写流水,避免重复回调/补偿重试重复记账 // (流水侧另有 (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() created := false err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { res := tx.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "ref_id"}, {Name: "quota_type"}}, DoNothing: true, }).Create(grant) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return nil // 凭证已存在:幂等跳过,不重复写流水。 } created = true ensureFlowOrderNo(flow) return tx.Create(flow).Error }) if err != nil { span.RecordError(err) return false, err } 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) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantListByUserIDAndType") defer span.End() var list []mysqlmodel.QuotaGrant if err := d.db.WithContext(ctx). Where("user_id = ? AND quota_type = ?", userID, quotaType). Order("CASE WHEN expire_time = 0 THEN 1 ELSE 0 END ASC"). Order("expire_time ASC"). Find(&list).Error; err != nil { span.RecordError(err) return nil, err } return list, nil } // QuotaGrantConsume 条件扣减单条凭证已用量(used + n <= amount),返回是否扣减成功。 // 用于 Cos 永久额度凭证维度的扣减,避免并发超扣。 func (d *QuotaGrantDao) QuotaGrantConsume(ctx context.Context, grantID int64, n int64) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantConsume") defer span.End() res := d.db.WithContext(ctx).Model(&mysqlmodel.QuotaGrant{}). Where("id = ? AND used + ? <= amount", grantID, n). UpdateColumn("used", gorm.Expr("used + ?", n)) if res.Error != nil { span.RecordError(res.Error) return false, res.Error } return res.RowsAffected > 0, nil } // QuotaGrantListByRefID 查询某关联ID(订单号/广告记录ID)下未删除的额度凭证。 func (d *QuotaGrantDao) QuotaGrantListByRefID(ctx context.Context, refID string) ([]mysqlmodel.QuotaGrant, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantListByRefID") defer span.End() var list []mysqlmodel.QuotaGrant if err := d.db.WithContext(ctx).Where("ref_id = ?", refID).Find(&list).Error; err != nil { span.RecordError(err) return nil, err } return list, nil } // QuotaGrantInvalidateByRefIDWithFlows 事务内按关联ID软删除凭证,并写「退款回收」流水。 // 凭证软删除后不再计入有效额度;已用部分不追回,故流水变更量由调用方按 (amount - used) 记负数。 // 凭证失效与流水同事务,保证「额度被回收」与「额度明细」一致。 func (d *QuotaGrantDao) QuotaGrantInvalidateByRefIDWithFlows(ctx context.Context, refID string, flows []*mysqlmodel.QuotaFlowLog) error { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantInvalidateByRefIDWithFlows") defer span.End() return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("ref_id = ?", refID).Delete(&mysqlmodel.QuotaGrant{}).Error; err != nil { return err } for _, f := range flows { ensureFlowOrderNo(f) if err := tx.Create(f).Error; err != nil { return err } } return nil }) } // QuotaGrantExistsByRefIDAndType 判断凭证是否已存在(广告激励防重复领取)。 func (d *QuotaGrantDao) QuotaGrantExistsByRefIDAndType(ctx context.Context, refID string, quotaType int8) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantExistsByRefIDAndType") defer span.End() var count int64 if err := d.db.WithContext(ctx).Model(&mysqlmodel.QuotaGrant{}). Where("ref_id = ? AND quota_type = ?", refID, quotaType). Count(&count).Error; err != nil { span.RecordError(err) return false, err } return count > 0, nil }