277 lines
8.7 KiB
Go
277 lines
8.7 KiB
Go
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
|
||
}
|