bindbox-game/internal/api/activity/lottery_helper.go

277 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}