fix: 修复微信通知字段截断导致的编码错误 feat: 添加有效邀请相关字段和任务中心常量 refactor: 重构一番赏奖品格位逻辑 perf: 优化道具卡列表聚合显示 docs: 更新项目说明文档和API文档 test: 添加字符串截断工具测试
388 lines
10 KiB
Go
388 lines
10 KiB
Go
package app
|
||
|
||
import (
|
||
"bindbox-game/internal/pkg/core"
|
||
"bindbox-game/internal/repository/mysql/model"
|
||
"fmt"
|
||
"time"
|
||
)
|
||
|
||
// 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 {
|
||
uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(userCouponID), h.readDB.UserCoupons.UserID.Eq(userID), h.readDB.UserCoupons.Status.Eq(1)).First()
|
||
if uc == nil {
|
||
return 0
|
||
}
|
||
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
|
||
}
|
||
if uc.ValidStart.After(now) {
|
||
return 0
|
||
}
|
||
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
|
||
return 0
|
||
}
|
||
scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID)
|
||
if !scopeOK {
|
||
return 0
|
||
}
|
||
if order.TotalAmount < sc.MinSpend {
|
||
return 0
|
||
}
|
||
cap := order.TotalAmount / 2
|
||
remainingCap := cap - order.DiscountAmount
|
||
if remainingCap <= 0 {
|
||
return 0
|
||
}
|
||
applied := int64(0)
|
||
switch sc.DiscountType {
|
||
case 1:
|
||
var bal int64
|
||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
|
||
if bal <= 0 {
|
||
bal = sc.DiscountValue
|
||
}
|
||
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
|
||
}
|
||
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) {
|
||
uc, _ := h.readDB.UserCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.UserCoupons.ID.Eq(userCouponID), h.readDB.UserCoupons.UserID.Eq(userID), h.readDB.UserCoupons.Status.Eq(1)).First()
|
||
if uc == nil {
|
||
return
|
||
}
|
||
sc, _ := h.readDB.SystemCoupons.WithContext(ctx.RequestContext()).Where(h.readDB.SystemCoupons.ID.Eq(uc.CouponID), h.readDB.SystemCoupons.Status.Eq(1)).First()
|
||
if sc == nil {
|
||
return
|
||
}
|
||
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 sc.DiscountType == 1 {
|
||
var bal int64
|
||
_ = h.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
|
||
newBal := bal - 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()
|
||
return prefix + now.Format("20060102150405")
|
||
}
|
||
|
||
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(remark string) int64 {
|
||
if remark == "" {
|
||
return -1
|
||
}
|
||
p := 0
|
||
for i := 0; i < len(remark); i++ {
|
||
if remark[i] == '|' {
|
||
seg := remark[p:i]
|
||
if len(seg) > 5 && seg[:5] == "slot:" {
|
||
var n int64
|
||
for j := 5; j < len(seg); j++ {
|
||
c := seg[j]
|
||
if c < '0' || c > '9' {
|
||
break
|
||
}
|
||
n = n*10 + int64(c-'0')
|
||
}
|
||
return n
|
||
}
|
||
p = i + 1
|
||
}
|
||
}
|
||
if p < len(remark) {
|
||
seg := remark[p:]
|
||
if len(seg) > 5 && seg[:5] == "slot:" {
|
||
var n int64
|
||
for j := 5; j < len(seg); j++ {
|
||
c := seg[j]
|
||
if c < '0' || c > '9' {
|
||
break
|
||
}
|
||
n = n*10 + int64(c-'0')
|
||
}
|
||
return n
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
func parseItemCardIDFromRemark(remark string) int64 {
|
||
// remark segments separated by '|', find segment starting with "itemcard:"
|
||
p := 0
|
||
n := len(remark)
|
||
for i := 0; i <= n; i++ {
|
||
if i == n || remark[i] == '|' {
|
||
seg := remark[p:i]
|
||
if len(seg) > 9 && seg[:9] == "itemcard:" {
|
||
var val int64
|
||
valid := true
|
||
for j := 9; j < len(seg); j++ {
|
||
c := seg[j]
|
||
if c < '0' || c > '9' {
|
||
valid = false
|
||
break
|
||
}
|
||
val = val*10 + int64(c-'0')
|
||
}
|
||
if valid && val > 0 {
|
||
return val
|
||
}
|
||
}
|
||
p = i + 1
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
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(remark string) ([]int64, []int64) {
|
||
if remark == "" {
|
||
return nil, nil
|
||
}
|
||
p := 0
|
||
for i := 0; i < len(remark); i++ {
|
||
if remark[i] == '|' {
|
||
seg := remark[p:i]
|
||
if len(seg) > 6 && seg[:6] == "slots:" {
|
||
pairs := seg[6:]
|
||
idxs := make([]int64, 0)
|
||
cnts := make([]int64, 0)
|
||
start := 0
|
||
for start <= len(pairs) {
|
||
end := start
|
||
for end < len(pairs) && pairs[end] != ',' {
|
||
end++
|
||
}
|
||
if end > start {
|
||
a := pairs[start:end]
|
||
// a format: num:num
|
||
x, y := int64(0), int64(0)
|
||
j := 0
|
||
for j < len(a) && a[j] >= '0' && a[j] <= '9' {
|
||
x = x*10 + int64(a[j]-'0')
|
||
j++
|
||
}
|
||
if j < len(a) && a[j] == ':' {
|
||
j++
|
||
for j < len(a) && a[j] >= '0' && a[j] <= '9' {
|
||
y = y*10 + int64(a[j]-'0')
|
||
j++
|
||
}
|
||
}
|
||
if y > 0 {
|
||
idxs = append(idxs, x+1)
|
||
cnts = append(cnts, y)
|
||
}
|
||
}
|
||
start = end + 1
|
||
}
|
||
return idxs, cnts
|
||
}
|
||
p = i + 1
|
||
}
|
||
}
|
||
return nil, nil
|
||
}
|
||
|
||
func parseIssueIDFromRemark(remark string) int64 {
|
||
if remark == "" {
|
||
return 0
|
||
}
|
||
// Try "issue:" or "matching_game:issue:"
|
||
// Split by |
|
||
segs := make([]string, 0)
|
||
last := 0
|
||
for i := 0; i < len(remark); i++ {
|
||
if remark[i] == '|' {
|
||
segs = append(segs, remark[last:i])
|
||
last = i + 1
|
||
}
|
||
}
|
||
if last < len(remark) {
|
||
segs = append(segs, remark[last:])
|
||
}
|
||
|
||
for _, seg := range segs {
|
||
// handle 'issue:123'
|
||
if len(seg) > 6 && seg[:6] == "issue:" {
|
||
var n int64
|
||
for j := 6; j < len(seg); j++ {
|
||
c := seg[j]
|
||
if c < '0' || c > '9' {
|
||
break
|
||
}
|
||
n = n*10 + int64(c-'0')
|
||
}
|
||
return n
|
||
}
|
||
// handle 'matching_game:issue:123'
|
||
if len(seg) > 20 && seg[:20] == "matching_game:issue:" {
|
||
var n int64
|
||
for j := 20; j < len(seg); j++ {
|
||
c := seg[j]
|
||
if c < '0' || c > '9' {
|
||
break
|
||
}
|
||
n = n*10 + int64(c-'0')
|
||
}
|
||
return n
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func parseCountFromRemark(remark string) int64 {
|
||
if remark == "" {
|
||
return 1
|
||
}
|
||
p := 0
|
||
for i := 0; i < len(remark); i++ {
|
||
if remark[i] == '|' {
|
||
seg := remark[p:i]
|
||
if len(seg) > 6 && seg[:6] == "count:" {
|
||
var n int64
|
||
for j := 6; j < len(seg); j++ {
|
||
c := seg[j]
|
||
if c < '0' || c > '9' {
|
||
break
|
||
}
|
||
n = n*10 + int64(c-'0')
|
||
}
|
||
if n <= 0 {
|
||
return 1
|
||
}
|
||
return n
|
||
}
|
||
p = i + 1
|
||
}
|
||
}
|
||
return 1
|
||
}
|