x
This commit is contained in:
parent
55e22086e8
commit
25c44c2064
@ -9,6 +9,7 @@ import (
|
||||
"bindbox-game/internal/pkg/core"
|
||||
"bindbox-game/internal/pkg/validation"
|
||||
"bindbox-game/internal/repository/mysql/model"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
// ==================== 用户次数卡 API ====================
|
||||
@ -197,8 +198,9 @@ func (h *handler) GetGamePassPackages() core.HandlerFunc {
|
||||
// ==================== 购买套餐 API ====================
|
||||
|
||||
type purchasePackageRequest struct {
|
||||
PackageID int64 `json:"package_id" binding:"required"`
|
||||
Count int32 `json:"count"` // 购买数量
|
||||
PackageID int64 `json:"package_id" binding:"required"`
|
||||
Count int32 `json:"count"` // 购买数量
|
||||
CouponIDs []int64 `json:"coupon_ids"` // 优惠券ID列表
|
||||
}
|
||||
|
||||
type purchasePackageResponse struct {
|
||||
@ -208,7 +210,7 @@ type purchasePackageResponse struct {
|
||||
|
||||
// PurchaseGamePassPackage 购买次数卡套餐(创建订单)
|
||||
// @Summary 购买次数卡套餐
|
||||
// @Description 购买次数卡套餐,创建订单等待支付
|
||||
// @Description 购买次数卡套餐,创建订单等待支付,支持使用优惠券
|
||||
// @Tags APP端.用户
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@ -245,7 +247,7 @@ func (h *handler) PurchaseGamePassPackage() core.HandlerFunc {
|
||||
// Calculate total price
|
||||
totalPrice := pkg.Price * int64(req.Count)
|
||||
|
||||
// 创建订单
|
||||
// 创建订单 (支持优惠券)
|
||||
now := time.Now()
|
||||
orderNo := now.Format("20060102150405") + fmt.Sprintf("%04d", now.UnixNano()%10000)
|
||||
order := &model.Orders{
|
||||
@ -255,11 +257,33 @@ func (h *handler) PurchaseGamePassPackage() core.HandlerFunc {
|
||||
TotalAmount: totalPrice,
|
||||
ActualAmount: totalPrice,
|
||||
Status: 1, // 待支付
|
||||
Remark: fmt.Sprintf("game_pass_package:%s|count:%d", pkg.Name, req.Count),
|
||||
Remark: fmt.Sprintf("game_pass_package:%s|pkg_id:%d|count:%d", pkg.Name, pkg.ID, req.Count),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// 应用优惠券 (如果有)
|
||||
var appliedCouponVal int64
|
||||
var couponID int64
|
||||
|
||||
if len(req.CouponIDs) > 0 {
|
||||
couponID = req.CouponIDs[0]
|
||||
// 调用优惠券应用函数
|
||||
applied, err := h.applyCouponToGamePassOrder(ctx, order, userID, couponID)
|
||||
if err != nil {
|
||||
ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
appliedCouponVal = applied
|
||||
|
||||
// 记录优惠券到订单
|
||||
if appliedCouponVal > 0 {
|
||||
order.CouponID = couponID
|
||||
order.Remark += fmt.Sprintf("|coupon:%d", couponID)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存订单
|
||||
if err := h.writeDB.Orders.WithContext(ctx.RequestContext()).
|
||||
Omit(h.writeDB.Orders.PaidAt, h.writeDB.Orders.CancelledAt).
|
||||
Create(order); err != nil {
|
||||
@ -267,14 +291,222 @@ func (h *handler) PurchaseGamePassPackage() core.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 在备注中记录套餐ID和数量
|
||||
remark := fmt.Sprintf("%s|pkg_id:%d|count:%d", order.Remark, pkg.ID, req.Count)
|
||||
h.writeDB.Orders.WithContext(ctx.RequestContext()).
|
||||
Where(h.writeDB.Orders.ID.Eq(order.ID)).
|
||||
Updates(map[string]any{"remark": remark})
|
||||
// 如果使用了优惠券,记录到order_coupons表
|
||||
if appliedCouponVal > 0 {
|
||||
_ = h.writeDB.OrderCoupons.WithContext(ctx.RequestContext()).UnderlyingDB().Exec(
|
||||
"INSERT IGNORE INTO order_coupons (order_id, user_coupon_id, applied_amount, created_at) VALUES (?,?,?,NOW(3))",
|
||||
order.ID, couponID, appliedCouponVal)
|
||||
}
|
||||
|
||||
// 处理0元订单
|
||||
if order.ActualAmount == 0 {
|
||||
order.Status = 2
|
||||
order.PaidAt = now
|
||||
h.writeDB.Orders.WithContext(ctx.RequestContext()).
|
||||
Where(h.writeDB.Orders.OrderNo.Eq(orderNo)).
|
||||
Updates(map[string]any{
|
||||
h.writeDB.Orders.Status.ColumnName().String(): 2,
|
||||
h.writeDB.Orders.PaidAt.ColumnName().String(): now,
|
||||
})
|
||||
|
||||
// 0元订单确认优惠券扣减
|
||||
if appliedCouponVal > 0 {
|
||||
h.confirmCouponUsage(ctx, couponID, order.ID, now)
|
||||
}
|
||||
}
|
||||
|
||||
res.OrderNo = order.OrderNo
|
||||
res.Message = "订单创建成功,请完成支付"
|
||||
ctx.Payload(res)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 优惠券辅助函数 ====================
|
||||
|
||||
// applyCouponToGamePassOrder 应用优惠券到次卡购买订单
|
||||
// 返回应用的优惠金额 (分)
|
||||
func (h *handler) applyCouponToGamePassOrder(ctx core.Context, order *model.Orders, userID int64, userCouponID int64) (int64, error) {
|
||||
// 使用 SELECT ... FOR UPDATE 锁定行
|
||||
uc, _ := h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where(
|
||||
h.writeDB.UserCoupons.ID.Eq(userCouponID),
|
||||
h.writeDB.UserCoupons.UserID.Eq(userID),
|
||||
).First()
|
||||
|
||||
if uc == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 检查状态 (必须是可用状态)
|
||||
if uc.Status != 1 {
|
||||
return 0, fmt.Errorf("优惠券不可用")
|
||||
}
|
||||
|
||||
// 获取优惠券模板
|
||||
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).
|
||||
First()
|
||||
|
||||
now := time.Now()
|
||||
if sc == nil {
|
||||
return 0, fmt.Errorf("优惠券模板不存在")
|
||||
}
|
||||
|
||||
// 验证有效期
|
||||
if uc.ValidStart.After(now) {
|
||||
return 0, fmt.Errorf("优惠券未到开始时间")
|
||||
}
|
||||
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
|
||||
return 0, fmt.Errorf("优惠券已过期")
|
||||
}
|
||||
|
||||
// 验证使用范围 (次卡购买只支持全场券 scope_type=1)
|
||||
if sc.ScopeType != 1 {
|
||||
return 0, fmt.Errorf("次卡购买仅支持全场通用优惠券")
|
||||
}
|
||||
|
||||
// 验证门槛
|
||||
if order.TotalAmount < sc.MinSpend {
|
||||
return 0, fmt.Errorf("未达优惠券使用门槛")
|
||||
}
|
||||
|
||||
// 50% 封顶
|
||||
cap := order.TotalAmount / 2
|
||||
remainingCap := cap - order.DiscountAmount
|
||||
if remainingCap <= 0 {
|
||||
return 0, fmt.Errorf("已达优惠封顶限制")
|
||||
}
|
||||
|
||||
// 计算优惠金额
|
||||
applied := int64(0)
|
||||
switch sc.DiscountType {
|
||||
case 1: // 金额券
|
||||
bal := uc.BalanceAmount
|
||||
if bal > 0 {
|
||||
if bal > remainingCap {
|
||||
applied = remainingCap
|
||||
} else {
|
||||
applied = bal
|
||||
}
|
||||
}
|
||||
case 2: // 满减券
|
||||
applied = sc.DiscountValue
|
||||
if applied > remainingCap {
|
||||
applied = remainingCap
|
||||
}
|
||||
case 3: // 折扣券
|
||||
rate := sc.DiscountValue
|
||||
if rate < 0 {
|
||||
rate = 0
|
||||
}
|
||||
if rate > 1000 {
|
||||
rate = 1000
|
||||
}
|
||||
newAmt := order.ActualAmount * rate / 1000
|
||||
d := order.ActualAmount - newAmt
|
||||
if d > remainingCap {
|
||||
applied = remainingCap
|
||||
} else {
|
||||
applied = d
|
||||
}
|
||||
}
|
||||
|
||||
if applied > order.ActualAmount {
|
||||
applied = order.ActualAmount
|
||||
}
|
||||
if applied <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// 更新订单金额
|
||||
order.ActualAmount -= applied
|
||||
order.DiscountAmount += applied
|
||||
|
||||
// 扣减优惠券余额或标记为冻结
|
||||
if sc.DiscountType == 1 {
|
||||
// 金额券:直接扣余额
|
||||
newBal := uc.BalanceAmount - applied
|
||||
newStatus := int32(1)
|
||||
if newBal <= 0 {
|
||||
newBal = 0
|
||||
newStatus = 2 // 已使用
|
||||
}
|
||||
res := h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).UnderlyingDB().Exec(
|
||||
"UPDATE user_coupons SET balance_amount = ?, status = ? WHERE id = ?",
|
||||
newBal, newStatus, userCouponID)
|
||||
if res.Error != nil {
|
||||
return 0, res.Error
|
||||
}
|
||||
|
||||
// 记录流水
|
||||
_ = h.writeDB.UserCouponLedger.WithContext(ctx.RequestContext()).Create(&model.UserCouponLedger{
|
||||
UserID: userID,
|
||||
UserCouponID: userCouponID,
|
||||
ChangeAmount: -applied,
|
||||
BalanceAfter: newBal,
|
||||
OrderID: order.ID,
|
||||
Action: "reserve", // 预扣
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
} else {
|
||||
// 满减/折扣券:标记为冻结 (状态4)
|
||||
res := h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).UnderlyingDB().Exec(
|
||||
"UPDATE user_coupons SET status = 4 WHERE id = ? AND status = 1", userCouponID)
|
||||
if res.Error != nil {
|
||||
return 0, res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return 0, fmt.Errorf("优惠券已被使用")
|
||||
}
|
||||
|
||||
// 记录流水
|
||||
_ = h.writeDB.UserCouponLedger.WithContext(ctx.RequestContext()).Create(&model.UserCouponLedger{
|
||||
UserID: userID,
|
||||
UserCouponID: userCouponID,
|
||||
ChangeAmount: 0,
|
||||
BalanceAfter: 0,
|
||||
OrderID: order.ID,
|
||||
Action: "reserve",
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
return applied, nil
|
||||
}
|
||||
|
||||
// confirmCouponUsage 确认优惠券使用 (支付成功后调用)
|
||||
func (h *handler) confirmCouponUsage(ctx core.Context, userCouponID int64, orderID int64, paidAt time.Time) {
|
||||
uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.UserCoupons.ID.Eq(userCouponID)).
|
||||
First()
|
||||
if uc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).
|
||||
Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID)).
|
||||
First()
|
||||
if sc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 金额券:余额已经扣减,只需记录used_order_id
|
||||
if sc.DiscountType == 1 {
|
||||
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).
|
||||
Where(h.writeDB.UserCoupons.ID.Eq(userCouponID)).
|
||||
Updates(map[string]any{
|
||||
h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||
h.writeDB.UserCoupons.UsedAt.ColumnName().String(): paidAt,
|
||||
})
|
||||
} else {
|
||||
// 满减/折扣券:状态从冻结(4)改为已使用(2)
|
||||
_, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).
|
||||
Where(h.writeDB.UserCoupons.ID.Eq(userCouponID)).
|
||||
Updates(map[string]any{
|
||||
h.writeDB.UserCoupons.Status.ColumnName().String(): 2,
|
||||
h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
|
||||
h.writeDB.UserCoupons.UsedAt.ColumnName().String(): paidAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,6 +378,69 @@ func (s *service) DeletePrize(ctx context.Context, prizeID int64) error {
|
||||
|
||||
// ========== 抽奖逻辑 ==========
|
||||
|
||||
// selectPrizeByWeight 根据随机值和权重选择奖品(不考虑库存)
|
||||
func selectPrizeByWeight(prizes []*model.LivestreamPrizes, randValue int64) *model.LivestreamPrizes {
|
||||
var cumulative int64
|
||||
for _, p := range prizes {
|
||||
cumulative += int64(p.Weight)
|
||||
if randValue < cumulative {
|
||||
return p
|
||||
}
|
||||
}
|
||||
if len(prizes) > 0 {
|
||||
return prizes[len(prizes)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findFallbackPrize 找到权重最大的有库存奖品作为兜底
|
||||
func findFallbackPrize(prizes []*model.LivestreamPrizes) *model.LivestreamPrizes {
|
||||
var fallback *model.LivestreamPrizes
|
||||
for _, p := range prizes {
|
||||
if p.Remaining == 0 {
|
||||
continue
|
||||
}
|
||||
if fallback == nil || p.Weight > fallback.Weight {
|
||||
fallback = p
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// drawWithFallback 抽奖:抽中售罄奖品时,穿透到权重最大的有库存奖品
|
||||
func (s *service) drawWithFallback(prizes []*model.LivestreamPrizes, totalWeight int64) (*model.LivestreamPrizes, int64, error) {
|
||||
randBig, err := rand.Int(rand.Reader, big.NewInt(totalWeight))
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("生成随机数失败: %w", err)
|
||||
}
|
||||
randValue := randBig.Int64()
|
||||
|
||||
selected := selectPrizeByWeight(prizes, randValue)
|
||||
if selected == nil {
|
||||
return nil, 0, fmt.Errorf("奖品选择失败")
|
||||
}
|
||||
|
||||
// 有库存,直接返回
|
||||
if selected.Remaining == -1 || selected.Remaining > 0 {
|
||||
return selected, randValue, nil
|
||||
}
|
||||
|
||||
// 售罄,穿透到权重最大的有库存奖品
|
||||
fallback := findFallbackPrize(prizes)
|
||||
if fallback == nil {
|
||||
return nil, 0, fmt.Errorf("没有可用奖品")
|
||||
}
|
||||
|
||||
s.logger.Info("抽中售罄奖品,穿透到兜底奖品",
|
||||
zap.Int64("original_prize_id", selected.ID),
|
||||
zap.String("original_prize_name", selected.Name),
|
||||
zap.Int64("fallback_prize_id", fallback.ID),
|
||||
zap.String("fallback_prize_name", fallback.Name),
|
||||
)
|
||||
|
||||
return fallback, randValue, nil
|
||||
}
|
||||
|
||||
func (s *service) Draw(ctx context.Context, input DrawInput) (*DrawResult, error) {
|
||||
// 0. 检查黑名单
|
||||
if input.DouyinUserID != "" {
|
||||
@ -390,27 +453,35 @@ func (s *service) Draw(ctx context.Context, input DrawInput) (*DrawResult, error
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 获取可用奖品
|
||||
// 1. 获取所有奖品
|
||||
prizes, err := s.ListPrizes(ctx, input.ActivityID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取奖品列表失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 过滤有库存的奖品
|
||||
var availablePrizes []*model.LivestreamPrizes
|
||||
if len(prizes) == 0 {
|
||||
return nil, fmt.Errorf("没有配置奖品")
|
||||
}
|
||||
|
||||
// 2. 计算总权重(所有奖品都参与,保持概率恒定)
|
||||
var totalWeight int64
|
||||
var hasAvailable bool
|
||||
for _, p := range prizes {
|
||||
if p.Remaining != 0 { // -1 表示无限
|
||||
availablePrizes = append(availablePrizes, p)
|
||||
totalWeight += int64(p.Weight)
|
||||
totalWeight += int64(p.Weight)
|
||||
if p.Remaining != 0 { // -1 表示无限,>0 表示有库存
|
||||
hasAvailable = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(availablePrizes) == 0 {
|
||||
return nil, fmt.Errorf("没有可用奖品")
|
||||
if totalWeight == 0 {
|
||||
return nil, fmt.Errorf("奖品权重配置异常")
|
||||
}
|
||||
|
||||
// 3. 生成随机种子
|
||||
if !hasAvailable {
|
||||
return nil, fmt.Errorf("没有可用奖品(全部售罄)")
|
||||
}
|
||||
|
||||
// 3. 生成随机种子(用于凭证)
|
||||
seedBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(seedBytes); err != nil {
|
||||
return nil, fmt.Errorf("生成随机种子失败: %w", err)
|
||||
@ -418,26 +489,10 @@ func (s *service) Draw(ctx context.Context, input DrawInput) (*DrawResult, error
|
||||
seedHash := sha256.Sum256(seedBytes)
|
||||
seedHex := hex.EncodeToString(seedHash[:])
|
||||
|
||||
// 4. 计算随机值
|
||||
randBig, err := rand.Int(rand.Reader, big.NewInt(totalWeight))
|
||||
// 4. 穿透抽奖:抽中售罄奖品时给权重最大的有库存奖品
|
||||
selectedPrize, randValue, err := s.drawWithFallback(prizes, totalWeight)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成随机数失败: %w", err)
|
||||
}
|
||||
randValue := randBig.Int64()
|
||||
|
||||
// 5. 按权重选择奖品
|
||||
var selectedPrize *model.LivestreamPrizes
|
||||
var cumulative int64
|
||||
for _, p := range availablePrizes {
|
||||
cumulative += int64(p.Weight)
|
||||
if randValue < cumulative {
|
||||
selectedPrize = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if selectedPrize == nil {
|
||||
selectedPrize = availablePrizes[len(availablePrizes)-1]
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 6. 事务:扣减库存 + 记录中奖
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user