package app import ( "bindbox-game/internal/pkg/core" "bindbox-game/internal/pkg/util/remark" "bindbox-game/internal/repository/mysql/model" "fmt" "time" ) // couponJoinResult 优惠券联合查询结果 type couponJoinResult struct { // UserCoupon fields UserCouponID int64 `gorm:"column:uc_id"` CouponID int64 `gorm:"column:coupon_id"` ValidStart time.Time `gorm:"column:valid_start"` ValidEnd time.Time `gorm:"column:valid_end"` BalanceAmount int64 `gorm:"column:balance_amount"` // SystemCoupon fields ScopeType int32 `gorm:"column:scope_type"` ActivityID int64 `gorm:"column:sc_activity_id"` MinSpend int64 `gorm:"column:min_spend"` DiscountType int32 `gorm:"column:discount_type"` DiscountValue int64 `gorm:"column:discount_value"` } // applyCouponWithCap 优惠券抵扣(含50%封顶与金额券部分使用) // 功能:在订单上应用一张用户券,实施总价50%封顶;金额券支持"部分使用",在 remark 记录明细 // 参数: // - ctx:请求上下文 // - userID:用户ID // - order:待更新的订单对象(入参引用,被本函数更新 discount_amount/actual_amount/remark) // - activityID:活动ID用于范围校验 // - userCouponID:用户持券ID // // 返回:本次实际应用的抵扣金额(分);若不适用或受封顶为0则返回0 func (h *handler) applyCouponWithCap(ctx core.Context, userID int64, order *model.Orders, activityID int64, userCouponID int64) int64 { // 使用单次 JOIN 查询替代 3 次分离查询,减少数据库往返 var result couponJoinResult err := h.repo.GetDbR().Raw(` SELECT uc.id as uc_id, uc.coupon_id, uc.valid_start, uc.valid_end, COALESCE(uc.balance_amount, 0) as balance_amount, sc.scope_type, sc.activity_id as sc_activity_id, sc.min_spend, sc.discount_type, sc.discount_value FROM user_coupons uc INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1 WHERE uc.id = ? AND uc.user_id = ? AND uc.status = 1 LIMIT 1 `, userCouponID, userID).Scan(&result).Error if err != nil || result.UserCouponID == 0 { return 0 } now := time.Now() if result.ValidStart.After(now) { return 0 } if !result.ValidEnd.IsZero() && result.ValidEnd.Before(now) { return 0 } scopeOK := (result.ScopeType == 1) || (result.ScopeType == 2 && result.ActivityID == activityID) if !scopeOK { return 0 } if order.TotalAmount < result.MinSpend { return 0 } cap := order.TotalAmount / 2 remainingCap := cap - order.DiscountAmount if remainingCap <= 0 { return 0 } applied := int64(0) switch result.DiscountType { case 1: // 金额券 bal := result.BalanceAmount if bal <= 0 { bal = result.DiscountValue } if bal > 0 { if bal > remainingCap { applied = remainingCap } else { applied = bal } } case 2: // 满减券 applied = result.DiscountValue if applied > remainingCap { applied = remainingCap } case 3: // 折扣券 rate := result.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 } order.DiscountAmount += applied order.ActualAmount -= applied order.Remark = order.Remark + fmt.Sprintf("|c:%d:%d", userCouponID, applied) return applied } // updateUserCouponAfterApply 应用后更新用户券(扣减余额或核销) // 功能:根据订单 remark 中记录的 applied_amount, // // 对直金额券扣减余额并在余额为0时核销;满减/折扣券一次性核销 // // 参数: // - ctx:请求上下文 // - userID:用户ID // - order:订单(用于读取 remark 和写入 used_order_id) // - userCouponID:用户持券ID // // 返回:无 func (h *handler) updateUserCouponAfterApply(ctx core.Context, userID int64, order *model.Orders, userCouponID int64) { // 使用单次 JOIN 查询替代 3 次分离查询 var result couponJoinResult err := h.repo.GetDbR().Raw(` SELECT uc.id as uc_id, uc.coupon_id, uc.valid_start, uc.valid_end, COALESCE(uc.balance_amount, 0) as balance_amount, sc.scope_type, sc.activity_id as sc_activity_id, sc.min_spend, sc.discount_type, sc.discount_value FROM user_coupons uc INNER JOIN system_coupons sc ON uc.coupon_id = sc.id AND sc.status = 1 WHERE uc.id = ? AND uc.user_id = ? AND uc.status = 1 LIMIT 1 `, userCouponID, userID).Scan(&result).Error if err != nil || result.UserCouponID == 0 { return } // 从 remark 中解析 applied amount applied := int64(0) remark := order.Remark p := 0 for i := 0; i < len(remark); i++ { if remark[i] == '|' { seg := remark[p:i] if len(seg) > 2 && seg[:2] == "c:" { j := 2 var id int64 for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' { id = id*10 + int64(seg[j]-'0') j++ } if j < len(seg) && seg[j] == ':' { j++ var amt int64 for j < len(seg) && seg[j] >= '0' && seg[j] <= '9' { amt = amt*10 + int64(seg[j]-'0') j++ } if id == userCouponID { applied = amt } } } p = i + 1 } } if result.DiscountType == 1 { // 金额券 newBal := result.BalanceAmount - applied if newBal < 0 { newBal = 0 } if newBal == 0 { _, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{"balance_amount": newBal, h.writeDB.UserCoupons.Status.ColumnName().String(): 2, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()}) } else { _, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{"balance_amount": newBal, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()}) } } else { // 满减/折扣券 _, _ = h.writeDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.writeDB.UserCoupons.ID.Eq(userCouponID), h.writeDB.UserCoupons.UserID.Eq(userID)).Updates(map[string]any{h.writeDB.UserCoupons.Status.ColumnName().String(): 2, h.writeDB.UserCoupons.UsedOrderID.ColumnName().String(): order.ID, h.writeDB.UserCoupons.UsedAt.ColumnName().String(): time.Now()}) } } // markOrderPaid 将订单标记为已支付 // 功能:用于0元订单直接置为已支付并写入支付时间 // 参数: // - ctx:请求上下文 // - orderNo:订单号 // // 返回:无 func (h *handler) markOrderPaid(ctx core.Context, orderNo string) { now := time.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}) } func (h *handler) randomID(prefix string) string { now := time.Now() // 增加随机数防止同1秒内并发生成相同的ID return fmt.Sprintf("%s%s%03d", prefix, now.Format("20060102150405"), time.Now().UnixNano()%1000) } func (h *handler) orderModel(userID int64, orderNo string, amount int64, activityID int64, issueID int64, count int64) *model.Orders { return &model.Orders{UserID: userID, OrderNo: orderNo, SourceType: 2, TotalAmount: amount, DiscountAmount: 0, PointsAmount: 0, ActualAmount: amount, Status: 1, IsConsumed: 0, Remark: fmt.Sprintf("lottery:activity:%d|issue:%d|count:%d", activityID, issueID, count)} } func parseSlotFromRemark(remarkStr string) int64 { rmk := remark.Parse(remarkStr) if len(rmk.Slots) > 0 { return rmk.Slots[0].SlotIndex } return -1 } func parseItemCardIDFromRemark(remarkStr string) int64 { return remark.Parse(remarkStr).ItemCardID } func (h *handler) joinSigPayload(userID int64, issueID int64, ts int64, nonce int64) string { return fmt.Sprintf("%d|%d|%d|%d", userID, issueID, ts, nonce) } func buildSlotsRemarkWithScalarCount(slots []int64) string { s := "" for i := range slots { if i > 0 { s += "," } s += fmt.Sprintf("%d:%d", slots[i]-1, 1) } return s } func parseSlotsCountsFromRemark(remarkStr string) ([]int64, []int64) { rmk := remark.Parse(remarkStr) idxs := make([]int64, 0, len(rmk.Slots)) cnts := make([]int64, 0, len(rmk.Slots)) for _, s := range rmk.Slots { idxs = append(idxs, s.SlotIndex+1) // 补偿前端展示逻辑,保持原样 cnts = append(cnts, s.Count) } return idxs, cnts } func parseIssueIDFromRemark(remarkStr string) int64 { return remark.Parse(remarkStr).IssueID } func parseCountFromRemark(remarkStr string) int64 { return remark.Parse(remarkStr).Count }