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
+58
View File
@@ -0,0 +1,58 @@
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
}