Zuncle 4ffd8e8326 fix: 修复过期优惠券仍可兑换/使用的漏洞
- store.go: 积分商城优惠券列表加 valid_end > now 过滤
- coupons_list.go: 修复 NULL valid_end 被错误排除,无截止日期券正确显示为有效
- activity_order_service.go: 过期/不可用券下单返回明确错误,不再静默跳过
- points_redeem_coupon_app.go: 积分兑换前校验模板 valid_end
- coupon_add.go: 发券前校验模板 valid_end,过期拒绝发放
2026-03-18 21:58:25 +08:00

112 lines
3.4 KiB
Go
Executable File
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 user
import (
"context"
"errors"
"fmt"
"time"
"bindbox-game/internal/repository/mysql/model"
"gorm.io/gorm"
)
func (s *service) AddCoupon(ctx context.Context, userID int64, couponID int64) error {
tpl, err := s.readDB.SystemCoupons.WithContext(ctx).Where(s.readDB.SystemCoupons.ID.Eq(couponID)).First()
if err != nil {
return err
}
if tpl == nil || tpl.Status != 1 {
// 模板不存在或未启用
fmt.Printf("[发券] 模板不可用 coupon_id=%d status=%d\n", couponID, func() int32 {
if tpl != nil {
return tpl.Status
}
return -1
}())
return errors.New("coupon not found or disabled")
}
if !tpl.ValidEnd.IsZero() && tpl.ValidEnd.Before(time.Now()) {
return errors.New("coupon template expired")
}
// 配额检查:若 TotalQuantity > 0 则限制发放总量
if tpl.TotalQuantity > 0 {
issued, ierr := s.readDB.UserCoupons.WithContext(ctx).Where(s.readDB.UserCoupons.CouponID.Eq(couponID)).Count()
if ierr != nil {
return ierr
}
if issued >= tpl.TotalQuantity {
fmt.Printf("[发券] 模板配额已满 coupon_id=%d issued=%d total=%d\n", couponID, issued, tpl.TotalQuantity)
return gorm.ErrInvalidData
}
}
// 允许用户重复持有相同模板的未使用优惠券
item := &model.UserCoupons{UserID: userID, CouponID: couponID, Status: 1}
if !tpl.ValidStart.IsZero() {
item.ValidStart = tpl.ValidStart
} else {
item.ValidStart = time.Now()
}
if !tpl.ValidEnd.IsZero() {
item.ValidEnd = tpl.ValidEnd
}
do := s.writeDB.UserCoupons.WithContext(ctx).Omit(s.readDB.UserCoupons.UsedAt, s.readDB.UserCoupons.UsedOrderID)
if tpl.ValidEnd.IsZero() {
do = do.Omit(s.writeDB.UserCoupons.ValidEnd)
}
if err := do.Create(item); err != nil {
return err
}
// 直减金额券初始化余额其他类型余额置0
if tpl.DiscountType == 1 && tpl.DiscountValue > 0 {
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.writeDB.UserCoupons.ID.Eq(item.ID)).Updates(map[string]any{"balance_amount": tpl.DiscountValue})
} else {
_, _ = s.writeDB.UserCoupons.WithContext(ctx).Where(s.writeDB.UserCoupons.ID.Eq(item.ID)).Updates(map[string]any{"balance_amount": 0})
}
return nil
}
func (s *service) VoidUserCoupon(ctx context.Context, adminID int64, userID int64, userCouponID int64) error {
if userID <= 0 || userCouponID <= 0 {
return fmt.Errorf("invalid_params")
}
uc, err := s.readDB.UserCoupons.WithContext(ctx).
Where(s.readDB.UserCoupons.ID.Eq(userCouponID)).
Where(s.readDB.UserCoupons.UserID.Eq(userID)).
First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("not_found")
}
return err
}
if uc.Status != 1 {
return fmt.Errorf("invalid_status")
}
db := s.repo.GetDbW()
if uc.BalanceAmount > 0 {
nb := int64(0)
if err := db.Exec("UPDATE user_coupons SET status=?, balance_amount=?, updated_at=NOW(3) WHERE id=? AND user_id=? AND status=1", 3, nb, userCouponID, userID).Error; err != nil {
return err
}
ledger := &model.UserCouponLedger{
UserID: userID,
UserCouponID: userCouponID,
ChangeAmount: -uc.BalanceAmount,
BalanceAfter: nb,
OrderID: 0,
Action: "void_by_admin",
CreatedAt: time.Now(),
}
if err := db.Create(ledger).Error; err != nil {
return err
}
return nil
}
if err := db.Exec("UPDATE user_coupons SET status=?, updated_at=NOW(3) WHERE id=? AND user_id=? AND status=1", 3, userCouponID, userID).Error; err != nil {
return err
}
_ = adminID
return nil
}