package mysqldao import ( "context" "fmt" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/global" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel" "gorm.io/gorm" "gorm.io/gorm/clause" ) // MemberQuotaGrantCreateWithFlow 按用户、会员商品、额度类型仅发放一次。 // 锁定用户行将同一用户的并发续费串行化,检查与写入使用同一事务。 // 保留原始订单号用于退款追溯;历史凭证(包括已退款软删除的凭证)也参与去重, // 因而额度用完、会员过期或退款后再次购买都不会重新领取首次赠送。 func (d *QuotaGrantDao) MemberQuotaGrantCreateWithFlow(ctx context.Context, goodsID int64, grant *mysqlmodel.QuotaGrant, flow *mysqlmodel.QuotaFlowLog) (bool, error) { ctx, span := global.Tracer.Start(ctx, "mysqldao.MemberQuotaGrantCreateWithFlow") defer span.End() if goodsID <= 0 || grant == nil || flow == nil || grant.UserID <= 0 || grant.RefID == "" || grant.Source != 1 { return false, fmt.Errorf("invalid member quota grant") } 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 } // 当前读确保 MySQL REPEATABLE READ 下等待用户锁后可见上一事务的发放。 // 关联订单快照识别商品,不依赖当前商品名称或用户是否仍为新用户。 var previous []mysqlmodel.QuotaGrant if err := tx.Unscoped().Model(&mysqlmodel.QuotaGrant{}). Select("quota_grants.*"). Joins("JOIN orders ON orders.order_no = quota_grants.ref_id AND orders.user_id = quota_grants.user_id"). Where("quota_grants.user_id = ? AND orders.goods_id = ? AND quota_grants.quota_type = ? AND quota_grants.source = ?", grant.UserID, goodsID, grant.QuotaType, grant.Source). Clauses(clause.Locking{Strength: "UPDATE"}).Limit(1).Find(&previous).Error; err != nil { return err } if len(previous) > 0 { return nil } if err := tx.Create(grant).Error; err != nil { return err } ensureFlowOrderNo(flow) if err := tx.Create(flow).Error; err != nil { return err } created = true return nil }) if err != nil { span.RecordError(err) return false, err } return created, nil }