bindbox-game/internal/service/activity/activity_order_service.go
2025-12-26 12:22:32 +08:00

321 lines
11 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 activity
import (
"bindbox-game/internal/pkg/core"
"bindbox-game/internal/pkg/logger"
"bindbox-game/internal/repository/mysql"
"bindbox-game/internal/repository/mysql/dao"
"bindbox-game/internal/repository/mysql/model"
titlesvc "bindbox-game/internal/service/title"
usersvc "bindbox-game/internal/service/user"
"context"
"encoding/json"
"fmt"
"time"
)
// ActivityOrderService 活动订单创建服务
// 统一处理一番赏和对对碰的订单创建逻辑
type ActivityOrderService interface {
// CreateActivityOrder 创建活动订单
// 统一处理优惠券、称号折扣、道具卡记录等
CreateActivityOrder(ctx core.Context, req CreateActivityOrderRequest) (*CreateActivityOrderResult, error)
}
// CreateActivityOrderRequest 订单创建请求
type CreateActivityOrderRequest struct {
UserID int64 // 用户ID
ActivityID int64 // 活动ID
IssueID int64 // 期ID
Count int64 // 数量
UnitPrice int64 // 单价(分)
SourceType int32 // 订单来源类型: 2=抽奖, 3=对对碰
CouponID *int64 // 优惠券ID可选
ItemCardID *int64 // 道具卡ID可选
ExtraRemark string // 额外备注信息
}
// CreateActivityOrderResult 订单创建结果
type CreateActivityOrderResult struct {
Order *model.Orders // 创建的订单
AppliedCouponVal int64 // 应用的优惠券抵扣金额
}
type activityOrderService struct {
logger logger.CustomLogger
readDB *dao.Query
writeDB *dao.Query
repo mysql.Repo
title titlesvc.Service
user usersvc.Service
}
// NewActivityOrderService 创建活动订单服务
func NewActivityOrderService(l logger.CustomLogger, db mysql.Repo) ActivityOrderService {
return &activityOrderService{
logger: l,
readDB: dao.Use(db.GetDbR()),
writeDB: dao.Use(db.GetDbW()),
repo: db,
title: titlesvc.New(l, db),
user: usersvc.New(l, db),
}
}
// CreateActivityOrder 创建活动订单
func (s *activityOrderService) CreateActivityOrder(ctx core.Context, req CreateActivityOrderRequest) (*CreateActivityOrderResult, error) {
userID := req.UserID
count := req.Count
if count <= 0 {
count = 1
}
total := req.UnitPrice * count
// 1. 创建订单基础信息
orderNo := fmt.Sprintf("O%s", time.Now().Format("20060102150405"))
order := &model.Orders{
UserID: userID,
OrderNo: orderNo,
SourceType: req.SourceType,
TotalAmount: total,
ActualAmount: total,
Status: 1, // Pending
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// 设置备注
if req.ExtraRemark != "" {
order.Remark = req.ExtraRemark
} else {
order.Remark = fmt.Sprintf("activity:%d|issue:%d|count:%d", req.ActivityID, req.IssueID, count)
}
// 记录优惠券和道具卡信息(显式字段 + 备注追加)
if req.CouponID != nil && *req.CouponID > 0 {
order.CouponID = *req.CouponID
order.Remark += fmt.Sprintf("|coupon:%d", *req.CouponID)
}
if req.ItemCardID != nil && *req.ItemCardID > 0 {
order.ItemCardID = *req.ItemCardID
order.Remark += fmt.Sprintf("|itemcard:%d", *req.ItemCardID)
}
// 2. 应用称号折扣 (Title Discount)
titleEffects, _ := s.title.ResolveActiveEffects(ctx.RequestContext(), userID, titlesvc.EffectScope{
ActivityID: &req.ActivityID,
IssueID: &req.IssueID,
})
for _, ef := range titleEffects {
if ef.EffectType == 2 { // Discount effect
var p struct {
DiscountType string `json:"discount_type"`
ValueX1000 int64 `json:"value_x1000"`
MaxDiscountX1000 int64 `json:"max_discount_x1000"`
}
if jsonErr := json.Unmarshal([]byte(ef.ParamsJSON), &p); jsonErr == nil {
var discount int64
if p.DiscountType == "percentage" {
discount = order.ActualAmount * p.ValueX1000 / 1000
} else if p.DiscountType == "fixed" {
discount = p.ValueX1000
}
if p.MaxDiscountX1000 > 0 && discount > p.MaxDiscountX1000 {
discount = p.MaxDiscountX1000
}
if discount > order.ActualAmount {
discount = order.ActualAmount
}
if discount > 0 {
order.ActualAmount -= discount
order.Remark += fmt.Sprintf("|title_discount:%d:%d", ef.ID, discount)
}
}
}
}
// 3. 应用优惠券 (using applyCouponWithCap logic)
var appliedCouponVal int64
if req.CouponID != nil && *req.CouponID > 0 {
fmt.Printf("[订单服务] 尝试优惠券 用户券ID=%d 应用前实付(分)=%d\n", *req.CouponID, order.ActualAmount)
appliedCouponVal = s.applyCouponWithCap(ctx.RequestContext(), userID, order, req.ActivityID, *req.CouponID)
fmt.Printf("[订单服务] 优惠后 实付(分)=%d 累计优惠(分)=%d\n", order.ActualAmount, order.DiscountAmount)
}
// 4. 记录道具卡到备注 (Removed duplicate append here as it was already done in Step 1)
// Log for debugging
if req.ItemCardID != nil && *req.ItemCardID > 0 {
fmt.Printf("[订单服务] 尝试道具卡 用户卡ID=%d 订单号=%s\n", *req.ItemCardID, order.OrderNo)
}
// 5. 保存订单
if err := s.writeDB.Orders.WithContext(ctx.RequestContext()).Omit(s.writeDB.Orders.PaidAt, s.writeDB.Orders.CancelledAt).Create(order); err != nil {
return nil, err
}
// 6. 记录优惠券使用明细
if appliedCouponVal > 0 && req.CouponID != nil && *req.CouponID > 0 {
_ = s.user.RecordOrderCouponUsage(ctx.RequestContext(), order.ID, *req.CouponID, appliedCouponVal)
}
// 7. 处理0元订单自动支付
if order.ActualAmount == 0 {
now := time.Now()
_, _ = s.writeDB.Orders.WithContext(ctx.RequestContext()).Where(s.writeDB.Orders.OrderNo.Eq(orderNo)).Updates(map[string]any{
s.writeDB.Orders.Status.ColumnName().String(): 2,
s.writeDB.Orders.PaidAt.ColumnName().String(): now,
})
order.Status = 2
// 核销优惠券
if req.CouponID != nil && *req.CouponID > 0 {
s.consumeCouponOnZeroPay(ctx.RequestContext(), userID, order.ID, *req.CouponID, appliedCouponVal, now)
}
}
fmt.Printf("[订单服务] 订单创建完成 订单号=%s 总额(分)=%d 优惠(分)=%d 实付(分)=%d 状态=%d\n",
order.OrderNo, order.TotalAmount, order.DiscountAmount, order.ActualAmount, order.Status)
return &CreateActivityOrderResult{
Order: order,
AppliedCouponVal: appliedCouponVal,
}, nil
}
// applyCouponWithCap 优惠券抵扣含50%封顶与金额券部分使用)
func (s *activityOrderService) applyCouponWithCap(ctx context.Context, userID int64, order *model.Orders, activityID int64, userCouponID int64) int64 {
uc, _ := s.readDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID), s.readDB.UserCoupons.UserID.Eq(userID), s.readDB.UserCoupons.Status.Eq(1)).First()
if uc == nil {
return 0
}
fmt.Printf("[优惠券] 用户券ID=%d 开始=%s 结束=%s\n", userCouponID, uc.ValidStart.Format(time.RFC3339), func() string {
if uc.ValidEnd.IsZero() {
return "无截止"
}
return uc.ValidEnd.Format(time.RFC3339)
}())
sc, _ := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID), s.readDB.SystemCoupons.Status.Eq(1)).First()
now := time.Now()
if sc == nil {
fmt.Printf("[优惠券] 模板不存在 用户券ID=%d\n", userCouponID)
return 0
}
if uc.ValidStart.After(now) {
fmt.Printf("[优惠券] 未到开始时间 用户券ID=%d\n", userCouponID)
return 0
}
if !uc.ValidEnd.IsZero() && uc.ValidEnd.Before(now) {
fmt.Printf("[优惠券] 已过期 用户券ID=%d\n", userCouponID)
return 0
}
scopeOK := (sc.ScopeType == 1) || (sc.ScopeType == 2 && sc.ActivityID == activityID)
if !scopeOK {
fmt.Printf("[优惠券] 范围不匹配 用户券ID=%d scope_type=%d\n", userCouponID, sc.ScopeType)
return 0
}
if order.TotalAmount < sc.MinSpend {
fmt.Printf("[优惠券] 未达使用门槛 用户券ID=%d min_spend(分)=%d 订单总额(分)=%d\n", userCouponID, sc.MinSpend, order.TotalAmount)
return 0
}
// 50% 封顶
cap := order.TotalAmount / 2
remainingCap := cap - order.DiscountAmount
if remainingCap <= 0 {
fmt.Printf("[优惠券] 已达封顶\n")
return 0
}
applied := int64(0)
switch sc.DiscountType {
case 1: // 金额券
var bal int64
_ = s.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.ActualAmount -= applied
order.DiscountAmount += applied
order.Remark += fmt.Sprintf("|c:%d:%d", userCouponID, applied)
fmt.Printf("[优惠券] 本次抵扣(分)=%d\n", applied)
return applied
}
// consumeCouponOnZeroPay 0元支付时核销优惠券
func (s *activityOrderService) consumeCouponOnZeroPay(ctx context.Context, userID int64, orderID int64, userCouponID int64, applied int64, now time.Time) {
uc, _ := s.readDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID), s.readDB.UserCoupons.UserID.Eq(userID)).First()
if uc == nil {
return
}
sc, _ := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(uc.CouponID)).First()
if sc == nil {
return
}
if sc.DiscountType == 1 { // 金额券 - 部分扣减
var bal int64
_ = s.repo.GetDbR().Raw("SELECT COALESCE(balance_amount,0) FROM user_coupons WHERE id=?", userCouponID).Scan(&bal).Error
nb := bal - applied
if nb < 0 {
nb = 0
}
if nb == 0 {
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
"balance_amount": nb,
s.readDB.UserCoupons.Status.ColumnName().String(): 2,
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
})
} else {
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
"balance_amount": nb,
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
})
}
} else { // 满减/折扣券 - 直接核销
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).Updates(map[string]any{
s.readDB.UserCoupons.Status.ColumnName().String(): 2,
s.readDB.UserCoupons.UsedOrderID.ColumnName().String(): orderID,
s.readDB.UserCoupons.UsedAt.ColumnName().String(): now,
})
}
}