重构优化套餐数据结构,修复提取计数问题

This commit is contained in:
2025-12-10 20:07:33 +08:00
parent c8c86081d9
commit 05fba68b3e
11 changed files with 310 additions and 250 deletions

View File

@@ -2,9 +2,11 @@ package services
import (
"context"
"errors"
"fmt"
"math/rand/v2"
"net/netip"
"platform/pkg/u"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
@@ -66,7 +68,7 @@ func genPassPair() (string, string) {
}
// 查找资源
func findResource(resourceId int32) (*ResourceView, error) {
func findResource(resourceId int32, now time.Time) (*ResourceView, error) {
resource, err := q.Resource.
Preload(field.Associations).
Where(
@@ -82,57 +84,43 @@ func findResource(resourceId int32) (*ResourceView, error) {
}
var info = &ResourceView{
Id: resource.ID,
User: *resource.User,
Active: resource.Active,
Type: resource.Type,
User: *resource.User,
}
switch resource.Type {
case m.ResourceTypeShort:
var sub = resource.Short
var dailyLast = time.Time{}
if sub.DailyLast != nil {
dailyLast = time.Time(*sub.DailyLast)
}
var expire = time.Time{}
if sub.Expire != nil {
expire = time.Time(*sub.Expire)
}
var quota int32
if sub.Quota != nil {
quota = *sub.Quota
}
info.Mode = sub.Type
info.ShortId = &sub.ID
info.ExpireAt = sub.ExpireAt
info.Live = time.Duration(sub.Live) * time.Second
info.DailyLimit = sub.DailyLimit
info.DailyUsed = sub.DailyUsed
info.DailyLast = dailyLast
info.Expire = expire
info.Quota = quota
info.Mode = sub.Type
info.Quota = sub.Quota
info.Used = sub.Used
info.Daily = sub.Daily
info.LastAt = sub.LastAt
if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) {
info.Today = int(sub.Daily)
}
case m.ResourceTypeLong:
var sub = resource.Long
var dailyLast = time.Time{}
if sub.DailyLast != nil {
dailyLast = time.Time(*sub.DailyLast)
}
var expire = time.Time{}
if sub.Expire != nil {
expire = time.Time(*sub.Expire)
}
var quota int32
if sub.Quota != nil {
quota = *sub.Quota
}
info.LongId = &sub.ID
info.ExpireAt = sub.ExpireAt
info.Live = time.Duration(sub.Live) * time.Hour
info.Mode = sub.Type
info.Live = time.Duration(sub.Live) * time.Hour * 24
info.DailyLimit = sub.DailyLimit
info.DailyUsed = sub.DailyUsed
info.DailyLast = dailyLast
info.Expire = expire
info.Quota = quota
info.Quota = sub.Quota
info.Used = sub.Used
info.Daily = sub.Daily
info.LastAt = sub.LastAt
if sub.LastAt != nil && u.IsSameDate(*sub.LastAt, now) {
info.Today = int(sub.Daily)
}
}
if info.Mode == m.ResourceModeTime && info.ExpireAt == nil {
return nil, errors.New("检查套餐获取时间失败")
}
return info, nil
@@ -140,18 +128,20 @@ func findResource(resourceId int32) (*ResourceView, error) {
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
type ResourceView struct {
Id int32
Active bool
Type m.ResourceType
Mode m.ResourceMode
Live time.Duration
DailyLimit int32
DailyUsed int32
DailyLast time.Time
Quota int32
Used int32
Expire time.Time
User m.User
Id int32
User m.User
Active bool
Type m.ResourceType
ShortId *int32
LongId *int32
Live time.Duration
Mode m.ResourceMode
Quota int32
ExpireAt *time.Time
Used int32
Daily int32
LastAt *time.Time
Today int // 今日用量
}
// 检查用户是否可提取
@@ -161,7 +151,7 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
}
// 获取用户套餐
resource, err := findResource(resourceId)
resource, err := findResource(resourceId, now)
if err != nil {
return nil, nil, err
}
@@ -200,16 +190,11 @@ func ensure(now time.Time, source netip.Addr, resourceId int32, count int) (*Res
// 包时
case m.ResourceModeTime:
// 检查过期时间
if resource.Expire.Before(now) {
if resource.ExpireAt.Before(now) {
return nil, nil, ErrResourceExpired
}
// 检查每日限额
used := 0
if now.Format("2006-01-02") == resource.DailyLast.Format("2006-01-02") {
used = int(resource.DailyUsed)
}
excess := used+count > int(resource.DailyLimit)
if excess {
if count+resource.Today > int(resource.Quota) {
return nil, nil, ErrResourceDailyLimit
}

View File

@@ -17,6 +17,7 @@ import (
"time"
"github.com/hibiken/asynq"
"gorm.io/gen"
"gorm.io/gen/field"
)
@@ -152,44 +153,48 @@ func (s *channelBaiyinService) CreateChannels(source netip.Addr, resourceId int3
// 保存数据
err = q.Q.Transaction(func(q *q.Query) error {
var rs gen.ResultInfo
// 更新套餐用量
used := int32(count)
if u.IsSameDate(now, resource.DailyLast) {
used += resource.DailyUsed
}
// 根据套餐类型和模式更新使用记录
isShortType := resource.Type == m.ResourceTypeShort
isLongType := resource.Type == m.ResourceTypeLong
switch resource.Type {
case m.ResourceTypeShort:
_, err = q.ResourceShort.
switch {
case isShortType:
rs, err = q.ResourceShort.Debug().
Where(
q.ResourceShort.ResourceID.Eq(resource.Id),
q.ResourceShort.ID.Eq(*resource.ShortId),
q.ResourceShort.Used.Eq(resource.Used),
q.ResourceShort.DailyUsed.Eq(resource.DailyUsed),
q.ResourceShort.DailyLast.Eq(resource.DailyLast),
q.ResourceShort.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceShort.Used.Add(int32(count)),
q.ResourceShort.DailyUsed.Value(used),
q.ResourceShort.DailyLast.Value(now),
q.ResourceShort.Daily.Value(int32(resource.Today+count)),
q.ResourceShort.LastAt.Value(now),
)
case m.ResourceTypeLong:
_, err = q.ResourceLong.
case isLongType:
rs, err = q.ResourceLong.Debug().
Where(
q.ResourceLong.ResourceID.Eq(resource.Id),
q.ResourceLong.ID.Eq(*resource.LongId),
q.ResourceLong.Used.Eq(resource.Used),
q.ResourceLong.DailyUsed.Eq(resource.DailyUsed),
q.ResourceLong.DailyLast.Eq(resource.DailyLast),
q.ResourceLong.Daily.Eq(resource.Daily),
).
UpdateSimple(
q.ResourceLong.Used.Add(int32(count)),
q.ResourceLong.DailyUsed.Value(used),
q.ResourceLong.DailyLast.Value(now),
q.ResourceLong.Daily.Value(int32(resource.Today+count)),
q.ResourceLong.LastAt.Value(now),
)
default:
return core.NewServErr("套餐类型不正确,无法更新", nil)
}
if err != nil {
return core.NewServErr("更新套餐使用记录失败", err)
}
if rs.RowsAffected == 0 {
return core.NewServErr("套餐使用记录不存在")
}
// 保存通道
err = q.Channel.

View File

@@ -2,6 +2,7 @@ package services
import (
"encoding/json"
"errors"
"fmt"
"platform/pkg/u"
"platform/web/core"
@@ -29,15 +30,19 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
}
// 检查余额
var amount = user.Balance.Sub(data.GetAmount())
if amount.IsNegative() {
amount, err := data.GetAmount()
if err != nil {
return err
}
balance := user.Balance.Sub(amount)
if balance.IsNegative() {
return ErrBalanceNotEnough
}
// 更新用户余额
_, err = q.User.
Where(q.User.ID.Eq(uid), q.User.Balance.Eq(user.Balance)).
UpdateSimple(q.User.Balance.Value(amount))
UpdateSimple(q.User.Balance.Value(balance))
if err != nil {
return core.NewServErr("更新用户余额失败", err)
}
@@ -49,7 +54,11 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
}
// 生成账单
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource))
subject, err := data.GetSubject()
if err != nil {
return err
}
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource))
if err != nil {
return core.NewServErr("生成账单失败", err)
}
@@ -61,6 +70,13 @@ func (s *resourceService) CreateResourceByBalance(uid int32, now time.Time, data
func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *CreateResourceData, trade *m.Trade) error {
return q.Q.Transaction(func(q *q.Query) error {
// 检查交易
if trade == nil {
return core.NewBizErr("交易数据不能为空")
}
if trade.Status != m.TradeStatusSuccess {
return core.NewBizErr("交易状态不正确")
}
// 保存套餐
resource, err := createResource(q, uid, now, data)
@@ -69,7 +85,15 @@ func (s *resourceService) CreateResourceByTrade(uid int32, now time.Time, data *
}
// 生成账单
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), data.GetSubject(), data.GetAmount(), resource, trade))
subject, err := data.GetSubject()
if err != nil {
return err
}
amount, err := data.GetAmount()
if err != nil {
return err
}
err = q.Bill.Create(newForConsume(uid, Bill.GenNo(), subject, amount, resource, trade))
if err != nil {
return core.NewServErr("生成账单失败", err)
}
@@ -95,13 +119,17 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
if short == nil {
return nil, core.NewBizErr("短效套餐数据不能为空")
}
var duration = time.Duration(short.Expire) * 24 * time.Hour
resource.Short = &m.ResourceShort{
Type: short.Mode,
Live: short.Live,
Quota: &short.Quota,
Expire: u.P(now.Add(duration)),
DailyLimit: short.DailyLimit,
Live: short.Live,
Type: short.Mode,
Quota: short.Quota,
}
if short.Mode == m.ResourceModeTime {
if short.Expire == nil {
return nil, core.NewBizErr("包时套餐过期时间不能为空")
}
var duration = time.Duration(*short.Expire) * 24 * time.Hour
resource.Short.ExpireAt = u.P(now.Add(duration))
}
// 长效套餐
@@ -110,13 +138,17 @@ func createResource(q *q.Query, uid int32, now time.Time, data *CreateResourceDa
if long == nil {
return nil, core.NewBizErr("长效套餐数据不能为空")
}
var duration = time.Duration(long.Expire) * 24 * time.Hour
resource.Long = &m.ResourceLong{
Type: long.Mode,
Live: long.Live,
Quota: &long.Quota,
Expire: u.P(now.Add(duration)),
DailyLimit: long.DailyLimit,
Live: long.Live,
Type: long.Mode,
Quota: long.Quota,
}
if long.Mode == m.ResourceModeTime {
if long.Expire == nil {
return nil, core.NewBizErr("包时套餐过期时间不能为空")
}
var duration = time.Duration(*long.Expire) * 24 * time.Hour
resource.Long.ExpireAt = u.P(now.Add(duration))
}
default:
return nil, core.NewBizErr("不支持的套餐类型")
@@ -137,22 +169,20 @@ type CreateResourceData struct {
}
type CreateShortResourceData struct {
Live int32 `json:"live" validate:"required,min=180"`
Mode m.ResourceMode `json:"mode" validate:"required"`
Expire int32 `json:"expire"`
DailyLimit int32 `json:"daily_limit" validate:"min=2000"`
Quota int32 `json:"quota" validate:"min=10000"`
Live int32 `json:"live" validate:"required,min=180"`
Mode m.ResourceMode `json:"mode" validate:"required"`
Quota int32 `json:"quota"`
Expire *int32 `json:"expire"`
name string
price *decimal.Decimal
}
type CreateLongResourceData struct {
Live int32 `json:"live" validate:"required,oneof=1 4 8 12 24"`
Mode m.ResourceMode `json:"mode" validate:"required,oneof=1 2"`
Expire int32 `json:"expire"`
DailyLimit int32 `json:"daily_limit" validate:"min=100"`
Quota int32 `json:"quota" validate:"min=500"`
Live int32 `json:"live" validate:"required"`
Mode m.ResourceMode `json:"mode" validate:"required"`
Quota int32 `json:"quota" validate:"required"`
Expire *int32 `json:"expire" validate:"required"`
name string
price *decimal.Decimal
@@ -162,24 +192,28 @@ func (c *CreateResourceData) GetType() m.TradeType {
return m.TradeTypePurchase
}
func (c *CreateResourceData) GetSubject() string {
func (c *CreateResourceData) GetSubject() (string, error) {
switch c.Type {
default:
return "", errors.New("无效的套餐类型")
case m.ResourceTypeShort:
return c.Short.GetSubject()
case m.ResourceTypeLong:
return c.Long.GetSubject()
}
panic("类型对应的数据为空")
}
func (c *CreateResourceData) GetAmount() decimal.Decimal {
func (c *CreateResourceData) GetAmount() (decimal.Decimal, error) {
switch c.Type {
default:
return decimal.Zero, errors.New("无效的套餐类型")
case m.ResourceTypeShort:
return c.Short.GetAmount()
case m.ResourceTypeLong:
return c.Long.GetAmount()
}
panic("类型对应的数据为空")
}
func (c *CreateResourceData) Serialize() (string, error) {
@@ -191,27 +225,37 @@ func (c *CreateResourceData) Deserialize(str string) error {
return json.Unmarshal([]byte(str), c)
}
func (data *CreateShortResourceData) GetSubject() string {
func (data *CreateShortResourceData) GetSubject() (string, error) {
if data.name == "" {
var mode string
switch data.Mode {
case 1:
default:
return "", errors.New("无效的套餐模式")
case m.ResourceModeTime:
mode = "包时"
case 2:
case m.ResourceModeQuota:
mode = "包量"
}
data.name = fmt.Sprintf("短效动态%s %v 分钟", mode, data.Live/60)
}
return data.name
return data.name, nil
}
func (data *CreateShortResourceData) GetAmount() decimal.Decimal {
func (data *CreateShortResourceData) GetAmount() (decimal.Decimal, error) {
if data.price == nil {
var factor int32
switch data.Mode {
case 1:
factor = data.DailyLimit * data.Expire
case 2:
default:
return decimal.Zero, errors.New("无效的套餐模式")
case m.ResourceModeTime:
if data.Expire == nil {
return decimal.Zero, errors.New("包时套餐过期时间不能为空")
}
factor = data.Quota * *data.Expire
case m.ResourceModeQuota:
factor = data.Quota
}
@@ -223,38 +267,53 @@ func (data *CreateShortResourceData) GetAmount() decimal.Decimal {
var dec = decimal.Decimal{}.
Add(decimal.NewFromInt32(base * factor)).
Div(decimal.NewFromInt(30000))
if dec.IsZero() {
return decimal.Zero, errors.New("计算金额错误")
}
data.price = &dec
}
return *data.price
return *data.price, nil
}
func (data *CreateLongResourceData) GetSubject() string {
func (data *CreateLongResourceData) GetSubject() (string, error) {
if data.name == "" {
var mode string
switch data.Mode {
case 1:
default:
return "", errors.New("无效的套餐模式")
case m.ResourceModeTime:
mode = "包时"
case 2:
case m.ResourceModeQuota:
mode = "包量"
}
data.name = fmt.Sprintf("长效动态%s %d 小时", mode, data.Live)
}
return data.name
return data.name, nil
}
func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
func (data *CreateLongResourceData) GetAmount() (decimal.Decimal, error) {
if data.price == nil {
var factor int32 = 0
switch data.Mode {
default:
return decimal.Zero, errors.New("无效的套餐模式")
case m.ResourceModeTime:
factor = data.Expire * data.DailyLimit
if data.Expire == nil {
return decimal.Zero, errors.New("包时套餐过期时间不能为空")
}
factor = *data.Expire * data.Quota
case m.ResourceModeQuota:
factor = data.Quota
}
var base int32
switch data.Live {
default:
return decimal.Zero, errors.New("无效的套餐时长")
case 1:
base = 30
case 4:
@@ -271,9 +330,13 @@ func (data *CreateLongResourceData) GetAmount() decimal.Decimal {
var dec = decimal.Decimal{}.
Add(decimal.NewFromInt32(base * factor)).
Div(decimal.NewFromInt(100))
if dec.IsZero() {
return decimal.Zero, errors.New("计算金额错误")
}
data.price = &dec
}
return *data.price
return *data.price, nil
}
type ResourceOnTradeComplete struct{}

View File

@@ -35,12 +35,18 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
platform := data.Platform
method := data.Method
tType := data.Product.GetType()
subject := data.Product.GetSubject()
amount := data.Product.GetAmount()
expire := time.Now().Add(30 * time.Minute)
subject, err := data.Product.GetSubject()
if err != nil {
return nil, err
}
amount, err := data.Product.GetAmount()
if err != nil {
return nil, err
}
// 实际支付金额,只在创建真实订单时使用
var amountReal = data.Product.GetAmount()
amountReal := amount
if env.RunMode == env.RunModeDev {
amountReal = decimal.NewFromFloat(0.01)
}
@@ -60,7 +66,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
return nil, err
}
var expireAt = time.Time(u.Z(coupon.ExpireAt))
expireAt := time.Time(u.Z(coupon.ExpireAt))
if !expireAt.IsZero() && expireAt.Before(now) {
_, err = q.Coupon.
Where(q.Coupon.ID.Eq(coupon.ID)).
@@ -99,7 +105,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
}
// 生成订单号
var tradeNo, err = ID.GenSerial()
tradeNo, err := ID.GenSerial()
if err != nil {
return nil, core.NewServErr("生成订单号失败", err)
}
@@ -692,8 +698,8 @@ type OnTradeCompletedData struct {
type ProductInfo interface {
GetType() m.TradeType
GetSubject() string
GetAmount() decimal.Decimal
GetSubject() (string, error)
GetAmount() (decimal.Decimal, error)
Serialize() (string, error)
Deserialize(str string) error
}

View File

@@ -25,7 +25,15 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
}
// 生成账单
err = q.Bill.Create(newForRecharge(uid, Bill.GenNo(), info.GetSubject(), info.GetAmount(), trade))
subject, err := info.GetSubject()
if err != nil {
return err
}
amount, err := info.GetAmount()
if err != nil {
return err
}
err = q.Bill.Create(newForRecharge(uid, Bill.GenNo(), subject, amount, trade))
if err != nil {
return core.NewServErr("生成账单失败", err)
}
@@ -39,23 +47,25 @@ func (s *userService) UpdateBalanceByTrade(uid int32, info *RechargeProductInfo,
return nil
}
func updateBalance(q *q.Query, uid int32, info *RechargeProductInfo) (err error) {
// 更新余额
func updateBalance(q *q.Query, uid int32, info *RechargeProductInfo) error {
user, err := q.User.
Where(q.User.ID.Eq(uid)).Take()
if err != nil {
return core.NewServErr("查询用户失败", err)
}
var amount = user.Balance.Add(info.GetAmount())
if amount.IsNegative() {
amount, err := info.GetAmount()
if err != nil {
return err
}
balance := user.Balance.Add(amount)
if balance.IsNegative() {
return core.NewServErr("用户余额不足")
}
_, err = q.User.
Where(q.User.ID.Eq(user.ID)).
UpdateSimple(q.User.Balance.Value(amount))
UpdateSimple(q.User.Balance.Value(balance))
if err != nil {
return core.NewServErr("更新用户余额失败", err)
}
@@ -75,12 +85,13 @@ func (r *RechargeProductInfo) GetType() m.TradeType {
return m.TradeTypeRecharge
}
func (r *RechargeProductInfo) GetSubject() string {
return fmt.Sprintf("账户充值 - %s元", r.GetAmount().StringFixed(2))
func (r *RechargeProductInfo) GetSubject() (string, error) {
amount, _ := r.GetAmount()
return fmt.Sprintf("账户充值 - %s元", amount.StringFixed(2)), nil
}
func (r *RechargeProductInfo) GetAmount() decimal.Decimal {
return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100))
func (r *RechargeProductInfo) GetAmount() (decimal.Decimal, error) {
return decimal.NewFromInt(int64(r.Amount)).Div(decimal.NewFromInt(100)), nil
}
func (r *RechargeProductInfo) Serialize() (string, error) {