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

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

@@ -713,14 +713,13 @@ drop table if exists resource_short cascade;
create table resource_short (
id int generated by default as identity primary key,
resource_id int not null,
type int not null,
live int not null,
expire timestamptz,
quota int,
type int not null,
quota int not null,
expire_at timestamptz,
used int not null default 0,
daily_limit int not null default 0,
daily_used int not null default 0,
daily_last timestamptz
daily int not null default 0,
last_at timestamptz
);
create index idx_resource_short_resource_id on resource_short (resource_id);
@@ -728,28 +727,26 @@ create index idx_resource_short_resource_id on resource_short (resource_id);
comment on table resource_short is '短效动态套餐表';
comment on column resource_short.id is 'ID';
comment on column resource_short.resource_id is '套餐ID';
comment on column resource_short.type is '套餐类型1-包时2-包量';
comment on column resource_short.live is '可用时长(秒)';
comment on column resource_short.quota is '配额数';
comment on column resource_short.used is '已用数量';
comment on column resource_short.expire is '过期时间';
comment on column resource_short.daily_limit is '每日限制';
comment on column resource_short.daily_used is '今日已用数';
comment on column resource_short.daily_last is '今日最后使用时间';
comment on column resource_short.type is '套餐类型1-包时2-包';
comment on column resource_short.quota is '每日配额(包时)或总配额(包量)';
comment on column resource_short.expire_at is '套餐过期时间,包时模式可用';
comment on column resource_short.used is '总用量';
comment on column resource_short.daily is '当日用';
comment on column resource_short.last_at is '最后使用时间';
-- resource_long
drop table if exists resource_long cascade;
create table resource_long (
id int generated by default as identity primary key,
resource_id int not null,
type int not null,
live int not null,
expire timestamptz,
quota int,
type int not null,
quota int not null,
expire_at timestamptz,
used int not null default 0,
daily_limit int not null default 0,
daily_used int not null default 0,
daily_last timestamptz
daily int not null default 0,
last_at timestamptz
);
create index idx_resource_long_resource_id on resource_long (resource_id);
@@ -757,14 +754,13 @@ create index idx_resource_long_resource_id on resource_long (resource_id);
comment on table resource_long is '长效动态套餐表';
comment on column resource_long.id is 'ID';
comment on column resource_long.resource_id is '套餐ID';
comment on column resource_long.live is '可用时长(小时)';
comment on column resource_long.type is '套餐类型1-包时2-包量';
comment on column resource_long.live is '可用时长(天)';
comment on column resource_long.quota is '配额数量';
comment on column resource_long.used is '已用数';
comment on column resource_long.expire is '过期时间';
comment on column resource_long.daily_limit is '每日限制';
comment on column resource_long.daily_used is '今日已用数量';
comment on column resource_long.daily_last is '今日最后使用时间';
comment on column resource_long.quota is '每日配额(包时)或总配额(包量)';
comment on column resource_long.expire_at is '套餐过期时间,包时模式可用';
comment on column resource_long.used is '总用';
comment on column resource_long.daily is '当日用量';
comment on column resource_long.last_at is '最后使用时间';
-- endregion

View File

@@ -60,10 +60,10 @@ func ListResourceShort(c *fiber.Ctx) error {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
}
if req.ExpireAfter != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Gte(*req.ExpireAfter))
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Gte(*req.ExpireAfter))
}
if req.ExpireBefore != nil {
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).Expire.Lte(*req.ExpireBefore))
do.Where(q.ResourceShort.As(q.Resource.Short.Name()).ExpireAt.Lte(*req.ExpireBefore))
}
resource, err := q.Resource.Where(do).
@@ -141,10 +141,10 @@ func ListResourceLong(c *fiber.Ctx) error {
do.Where(q.Resource.CreatedAt.Lte(*req.CreateBefore))
}
if req.ExpireAfter != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Gte(*req.ExpireAfter))
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Gte(*req.ExpireAfter))
}
if req.ExpireBefore != nil {
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).Expire.Lte(*req.ExpireBefore))
do.Where(q.ResourceLong.As(q.Resource.Long.Name()).ExpireAt.Lte(*req.ExpireBefore))
}
resource, err := q.Resource.Where(do).
@@ -204,10 +204,10 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeShort)),
q.ResourceShort.As(q.Resource.Short.Name()).Where(
short.Type.Eq(int(m.ResourceModeTime)),
short.Expire.Gte(now),
short.ExpireAt.Gte(now),
q.ResourceShort.As(q.Resource.Short.Name()).
Where(short.DailyLast.Lt(u.Today())).
Or(short.DailyLimit.GtCol(short.DailyUsed)),
Where(short.LastAt.Lt(u.Today())).
Or(short.Quota.GtCol(short.Daily)),
).Or(
short.Type.Eq(int(m.ResourceModeQuota)),
short.Quota.GtCol(short.Used),
@@ -216,10 +216,10 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Type.Eq(int(m.ResourceTypeLong)),
q.ResourceLong.As(q.Resource.Long.Name()).Where(
long.Type.Eq(int(m.ResourceModeTime)),
long.Expire.Gte(now),
long.ExpireAt.Gte(now),
q.ResourceLong.As(q.Resource.Long.Name()).
Where(long.DailyLast.Lt(u.Today())).
Or(long.DailyLimit.GtCol(long.DailyUsed)),
Where(long.LastAt.Lt(u.Today())).
Or(long.Quota.GtCol(long.Daily)),
).Or(
long.Type.Eq(int(m.ResourceModeQuota)),
long.Quota.GtCol(long.Used),
@@ -282,39 +282,39 @@ func StatisticResourceFree(c *fiber.Ctx) error {
// 短效包量
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeQuota:
if u.Z(resource.Short.Quota) > resource.Short.Used {
if resource.Short.Quota > resource.Short.Used {
shortCount++
shortQuotaSum += int(u.Z(resource.Short.Quota) - resource.Short.Used)
shortQuotaSum += int(resource.Short.Quota - resource.Short.Used)
}
// 长效包量
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeQuota:
if u.Z(resource.Long.Quota) > resource.Long.Used {
if resource.Long.Quota > resource.Long.Used {
longCount++
longQuotaSum += int(u.Z(resource.Long.Quota) - resource.Long.Used)
longQuotaSum += int(resource.Long.Quota - resource.Long.Used)
}
// 短效包时
case resource.Type == m.ResourceTypeShort && resource.Short.Type == m.ResourceModeTime:
if time.Time(*resource.Short.Expire).After(time.Now()) {
if resource.Short.DailyLast == nil || u.IsToday(time.Time(*resource.Short.DailyLast)) == false {
if time.Time(*resource.Short.ExpireAt).After(time.Now()) {
if resource.Short.LastAt == nil || u.IsToday(time.Time(*resource.Short.LastAt)) == false {
shortCount++
shortDailyFreeSum += int(resource.Short.DailyLimit)
} else if resource.Short.DailyLimit > resource.Short.DailyUsed {
shortDailyFreeSum += int(resource.Short.Quota)
} else if resource.Short.Quota > resource.Short.Daily {
shortCount++
shortDailyFreeSum += int(resource.Short.DailyLimit - resource.Short.DailyUsed)
shortDailyFreeSum += int(resource.Short.Quota - resource.Short.Daily)
}
}
// 长效包时
case resource.Type == m.ResourceTypeLong && resource.Long.Type == m.ResourceModeTime:
if time.Time(*resource.Long.Expire).After(time.Now()) {
if resource.Long.DailyLast == nil || u.IsToday(time.Time(*resource.Long.DailyLast)) == false {
if time.Time(*resource.Long.ExpireAt).After(time.Now()) {
if resource.Long.LastAt == nil || u.IsToday(time.Time(*resource.Long.LastAt)) == false {
longCount++
longDailyFreeSum += int(resource.Long.DailyLimit)
} else if resource.Long.DailyLimit > resource.Long.DailyUsed {
longDailyFreeSum += int(resource.Long.Quota)
} else if resource.Long.Quota > resource.Long.Daily {
longCount++
longDailyFreeSum += int(resource.Long.DailyLimit - resource.Long.DailyUsed)
longDailyFreeSum += int(resource.Long.Quota - resource.Long.Daily)
}
}
}
@@ -443,7 +443,11 @@ func ResourcePrice(c *fiber.Ctx) error {
}
// 获取套餐价格
amount, err := req.GetAmount()
if err != nil {
return err
}
return c.JSON(fiber.Map{
"price": req.GetAmount().StringFixed(2),
"price": amount.StringFixed(2),
})
}

View File

@@ -8,12 +8,11 @@ import (
type ResourceLong struct {
ID int32 `json:"id" gorm:"column:id"` // ID
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
Live int32 `json:"live" gorm:"column:live"` // 可用时长(小时)
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型1-包时2-包量
Live int32 `json:"live" gorm:"column:live"` // 可用时长(天)
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数
Used int32 `json:"used" gorm:"column:used"` // 已用数
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数量
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
ExpireAt *time.Time `json:"expire_at" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
Used int32 `json:"used" gorm:"column:used"` // 总用
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用
LastAt *time.Time `json:"last_at" gorm:"column:last_at"` // 最后使用时间
}

View File

@@ -8,12 +8,11 @@ import (
type ResourceShort struct {
ID int32 `json:"id" gorm:"column:id"` // ID
ResourceID int32 `json:"resource_id" gorm:"column:resource_id"` // 套餐ID
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型1-包时2-包量
Live int32 `json:"live" gorm:"column:live"` // 可用时长(秒)
Expire *time.Time `json:"expire" gorm:"column:expire"` // 过期时间
Quota *int32 `json:"quota" gorm:"column:quota"` // 配额数量
Used int32 `json:"used" gorm:"column:used"` // 已用数量
DailyLimit int32 `json:"daily_limit" gorm:"column:daily_limit"` // 每日限制
DailyUsed int32 `json:"daily_used" gorm:"column:daily_used"` // 今日已用数
DailyLast *time.Time `json:"daily_last" gorm:"column:daily_last"` // 今日最后使用时间
Type ResourceMode `json:"type" gorm:"column:type"` // 套餐类型1-包时2-包量
Quota int32 `json:"quota" gorm:"column:quota"` // 每日配额(包时)或总配额(包量)
ExpireAt *time.Time `json:"expire_at" gorm:"column:expire_at"` // 套餐过期时间,包时模式可用
Used int32 `json:"used" gorm:"column:used"` // 总用量
Daily int32 `json:"daily" gorm:"column:daily"` // 当日用
LastAt *time.Time `json:"last_at" gorm:"column:last_at"` // 最后使用时间
}

View File

@@ -29,14 +29,13 @@ func newResourceLong(db *gorm.DB, opts ...gen.DOOption) resourceLong {
_resourceLong.ALL = field.NewAsterisk(tableName)
_resourceLong.ID = field.NewInt32(tableName, "id")
_resourceLong.ResourceID = field.NewInt32(tableName, "resource_id")
_resourceLong.Type = field.NewInt(tableName, "type")
_resourceLong.Live = field.NewInt32(tableName, "live")
_resourceLong.Expire = field.NewTime(tableName, "expire")
_resourceLong.Type = field.NewInt(tableName, "type")
_resourceLong.Quota = field.NewInt32(tableName, "quota")
_resourceLong.ExpireAt = field.NewTime(tableName, "expire_at")
_resourceLong.Used = field.NewInt32(tableName, "used")
_resourceLong.DailyLimit = field.NewInt32(tableName, "daily_limit")
_resourceLong.DailyUsed = field.NewInt32(tableName, "daily_used")
_resourceLong.DailyLast = field.NewTime(tableName, "daily_last")
_resourceLong.Daily = field.NewInt32(tableName, "daily")
_resourceLong.LastAt = field.NewTime(tableName, "last_at")
_resourceLong.fillFieldMap()
@@ -49,14 +48,13 @@ type resourceLong struct {
ALL field.Asterisk
ID field.Int32
ResourceID field.Int32
Type field.Int
Live field.Int32
Expire field.Time
Type field.Int
Quota field.Int32
ExpireAt field.Time
Used field.Int32
DailyLimit field.Int32
DailyUsed field.Int32
DailyLast field.Time
Daily field.Int32
LastAt field.Time
fieldMap map[string]field.Expr
}
@@ -75,14 +73,13 @@ func (r *resourceLong) updateTableName(table string) *resourceLong {
r.ALL = field.NewAsterisk(table)
r.ID = field.NewInt32(table, "id")
r.ResourceID = field.NewInt32(table, "resource_id")
r.Type = field.NewInt(table, "type")
r.Live = field.NewInt32(table, "live")
r.Expire = field.NewTime(table, "expire")
r.Type = field.NewInt(table, "type")
r.Quota = field.NewInt32(table, "quota")
r.ExpireAt = field.NewTime(table, "expire_at")
r.Used = field.NewInt32(table, "used")
r.DailyLimit = field.NewInt32(table, "daily_limit")
r.DailyUsed = field.NewInt32(table, "daily_used")
r.DailyLast = field.NewTime(table, "daily_last")
r.Daily = field.NewInt32(table, "daily")
r.LastAt = field.NewTime(table, "last_at")
r.fillFieldMap()
@@ -99,17 +96,16 @@ func (r *resourceLong) GetFieldByName(fieldName string) (field.OrderExpr, bool)
}
func (r *resourceLong) fillFieldMap() {
r.fieldMap = make(map[string]field.Expr, 10)
r.fieldMap = make(map[string]field.Expr, 9)
r.fieldMap["id"] = r.ID
r.fieldMap["resource_id"] = r.ResourceID
r.fieldMap["type"] = r.Type
r.fieldMap["live"] = r.Live
r.fieldMap["expire"] = r.Expire
r.fieldMap["type"] = r.Type
r.fieldMap["quota"] = r.Quota
r.fieldMap["expire_at"] = r.ExpireAt
r.fieldMap["used"] = r.Used
r.fieldMap["daily_limit"] = r.DailyLimit
r.fieldMap["daily_used"] = r.DailyUsed
r.fieldMap["daily_last"] = r.DailyLast
r.fieldMap["daily"] = r.Daily
r.fieldMap["last_at"] = r.LastAt
}
func (r resourceLong) clone(db *gorm.DB) resourceLong {

View File

@@ -29,14 +29,13 @@ func newResourceShort(db *gorm.DB, opts ...gen.DOOption) resourceShort {
_resourceShort.ALL = field.NewAsterisk(tableName)
_resourceShort.ID = field.NewInt32(tableName, "id")
_resourceShort.ResourceID = field.NewInt32(tableName, "resource_id")
_resourceShort.Type = field.NewInt(tableName, "type")
_resourceShort.Live = field.NewInt32(tableName, "live")
_resourceShort.Expire = field.NewTime(tableName, "expire")
_resourceShort.Type = field.NewInt(tableName, "type")
_resourceShort.Quota = field.NewInt32(tableName, "quota")
_resourceShort.ExpireAt = field.NewTime(tableName, "expire_at")
_resourceShort.Used = field.NewInt32(tableName, "used")
_resourceShort.DailyLimit = field.NewInt32(tableName, "daily_limit")
_resourceShort.DailyUsed = field.NewInt32(tableName, "daily_used")
_resourceShort.DailyLast = field.NewTime(tableName, "daily_last")
_resourceShort.Daily = field.NewInt32(tableName, "daily")
_resourceShort.LastAt = field.NewTime(tableName, "last_at")
_resourceShort.fillFieldMap()
@@ -49,14 +48,13 @@ type resourceShort struct {
ALL field.Asterisk
ID field.Int32
ResourceID field.Int32
Type field.Int
Live field.Int32
Expire field.Time
Type field.Int
Quota field.Int32
ExpireAt field.Time
Used field.Int32
DailyLimit field.Int32
DailyUsed field.Int32
DailyLast field.Time
Daily field.Int32
LastAt field.Time
fieldMap map[string]field.Expr
}
@@ -75,14 +73,13 @@ func (r *resourceShort) updateTableName(table string) *resourceShort {
r.ALL = field.NewAsterisk(table)
r.ID = field.NewInt32(table, "id")
r.ResourceID = field.NewInt32(table, "resource_id")
r.Type = field.NewInt(table, "type")
r.Live = field.NewInt32(table, "live")
r.Expire = field.NewTime(table, "expire")
r.Type = field.NewInt(table, "type")
r.Quota = field.NewInt32(table, "quota")
r.ExpireAt = field.NewTime(table, "expire_at")
r.Used = field.NewInt32(table, "used")
r.DailyLimit = field.NewInt32(table, "daily_limit")
r.DailyUsed = field.NewInt32(table, "daily_used")
r.DailyLast = field.NewTime(table, "daily_last")
r.Daily = field.NewInt32(table, "daily")
r.LastAt = field.NewTime(table, "last_at")
r.fillFieldMap()
@@ -99,17 +96,16 @@ func (r *resourceShort) GetFieldByName(fieldName string) (field.OrderExpr, bool)
}
func (r *resourceShort) fillFieldMap() {
r.fieldMap = make(map[string]field.Expr, 10)
r.fieldMap = make(map[string]field.Expr, 9)
r.fieldMap["id"] = r.ID
r.fieldMap["resource_id"] = r.ResourceID
r.fieldMap["type"] = r.Type
r.fieldMap["live"] = r.Live
r.fieldMap["expire"] = r.Expire
r.fieldMap["type"] = r.Type
r.fieldMap["quota"] = r.Quota
r.fieldMap["expire_at"] = r.ExpireAt
r.fieldMap["used"] = r.Used
r.fieldMap["daily_limit"] = r.DailyLimit
r.fieldMap["daily_used"] = r.DailyUsed
r.fieldMap["daily_last"] = r.DailyLast
r.fieldMap["daily"] = r.Daily
r.fieldMap["last_at"] = r.LastAt
}
func (r resourceShort) clone(db *gorm.DB) resourceShort {

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
@@ -141,17 +129,19 @@ func findResource(resourceId int32) (*ResourceView, error) {
// ResourceView 套餐数据的简化视图,便于直接获取主要数据
type ResourceView struct {
Id int32
User m.User
Active bool
Type m.ResourceType
Mode m.ResourceMode
ShortId *int32
LongId *int32
Live time.Duration
DailyLimit int32
DailyUsed int32
DailyLast time.Time
Mode m.ResourceMode
Quota int32
ExpireAt *time.Time
Used int32
Expire time.Time
User m.User
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,
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,
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("不支持的套餐类型")
@@ -139,20 +171,18 @@ 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"`
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))
data.price = &dec
}
return *data.price
if dec.IsZero() {
return decimal.Zero, errors.New("计算金额错误")
}
func (data *CreateLongResourceData) GetSubject() string {
data.price = &dec
}
return *data.price, nil
}
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) {