609 lines
18 KiB
Go
609 lines
18 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
"strconv"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/errors/gcode"
|
||
"github.com/gogf/gf/v2/errors/gerror"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gtime"
|
||
|
||
v1 "tool-api/api/user/v1"
|
||
"tool-api/internal/consts"
|
||
"tool-api/internal/model/entity"
|
||
)
|
||
|
||
// ============================================================================
|
||
// 额度引擎
|
||
//
|
||
// 三种额度,按「免费 → 会员 → 付费」的顺序扣减,先扣对用户最有利的池:
|
||
// 1. 免费额度 free_quota —— 每个周期(默认 7 天)重置,所有用户都有
|
||
// 2. 会员额度 member_quota —— 每周期重置,仅会员可用(在免费额度用尽后启用)
|
||
// 3. 付费额度 次数包 —— 买断制、按工具绑定、在有效期内累计使用,不随周期重置
|
||
//
|
||
// 周期是「滚动窗口」而非自然周:首次使用时把周期起点定为当时时间,
|
||
// 起点 + 周期天数 = 终点;到点归零。这样做的好处是后台改周期天数后
|
||
// 立刻按新长度重算,不用等下一个自然周。
|
||
//
|
||
// 周期的取值优先级:用户单独设置 > 会员套餐(发货时写入用户)> 全局设置 > 默认 7 天。
|
||
// ============================================================================
|
||
|
||
const (
|
||
quotaSettingCacheTTL = 30 * time.Second
|
||
quotaMaxPeriodDays = 3650
|
||
)
|
||
|
||
// 扣减时返回的额度判定结果,附带「还能不能用」与失败原因。
|
||
type quotaVerdict struct {
|
||
Source string // 命中/将命中的池:free / member / paid / none
|
||
Message string // source == none 时的可读原因
|
||
}
|
||
|
||
// ===== 全局设置读取(带短缓存,避免每次扣减都查库)=====
|
||
|
||
type settingCacheItem struct {
|
||
value string
|
||
at time.Time
|
||
}
|
||
|
||
var (
|
||
settingCacheMu sync.RWMutex
|
||
settingCache = map[string]settingCacheItem{}
|
||
)
|
||
|
||
// SettingValue 读取 settings 表中的配置值,带 30 秒进程内缓存
|
||
func SettingValue(ctx context.Context, key string) string {
|
||
settingCacheMu.RLock()
|
||
item, ok := settingCache[key]
|
||
settingCacheMu.RUnlock()
|
||
if ok && time.Since(item.at) < quotaSettingCacheTTL {
|
||
return item.value
|
||
}
|
||
value, err := g.Model(consts.TableSettings).Where("k", key).Value("v")
|
||
if err != nil || value == nil {
|
||
return ""
|
||
}
|
||
settingCacheMu.Lock()
|
||
settingCache[key] = settingCacheItem{value: value.String(), at: time.Now()}
|
||
settingCacheMu.Unlock()
|
||
return value.String()
|
||
}
|
||
|
||
// settingSave 写入配置并立即失效缓存(管理端保存后马上生效)
|
||
func settingSave(ctx context.Context, key, value, remark string) error {
|
||
if _, err := g.Model(consts.TableSettings).Data(g.Map{
|
||
"k": key,
|
||
"v": value,
|
||
"remark": remark,
|
||
"updated_at": gtime.Now(),
|
||
}).Save(); err != nil {
|
||
return err
|
||
}
|
||
settingCacheMu.Lock()
|
||
delete(settingCache, key)
|
||
settingCacheMu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
// GlobalQuotaPeriodDays 全局默认额度周期(天)
|
||
func GlobalQuotaPeriodDays(ctx context.Context) int {
|
||
raw := SettingValue(ctx, consts.SettingQuotaPeriodDays)
|
||
days, err := strconv.Atoi(raw)
|
||
if err != nil || days <= 0 || days > quotaMaxPeriodDays {
|
||
return consts.DefaultQuotaPeriodDays
|
||
}
|
||
return days
|
||
}
|
||
|
||
// EffectiveQuotaPeriodDays 用户实际生效的额度周期:
|
||
// 用户单独设置(后台可改)优先,其次全局设置。
|
||
func EffectiveQuotaPeriodDays(ctx context.Context, user *entity.Users) int {
|
||
if user != nil && user.QuotaPeriodDays > 0 && user.QuotaPeriodDays <= quotaMaxPeriodDays {
|
||
return user.QuotaPeriodDays
|
||
}
|
||
return GlobalQuotaPeriodDays(ctx)
|
||
}
|
||
|
||
// ===== 工具读取 =====
|
||
|
||
func toolRow(ctx context.Context, toolKey string) (*entity.Tools, error) {
|
||
record, err := g.Model(consts.TableTools).Where("tool_key", toolKey).One()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if record.IsEmpty() {
|
||
return nil, gerror.New("工具不存在")
|
||
}
|
||
tool := &entity.Tools{}
|
||
if err = record.Struct(tool); err != nil {
|
||
return nil, err
|
||
}
|
||
return tool, nil
|
||
}
|
||
|
||
// ===== 周期用量 =====
|
||
|
||
type quotaUsageState struct {
|
||
Id int64
|
||
FreeUsed int
|
||
MemberUsed int
|
||
PeriodStart *gtime.Time
|
||
PeriodEnd *gtime.Time
|
||
Version int // 供构建输出用(本周期是否被重置过)
|
||
ResetOccurred bool
|
||
PeriodDaysSnapshot int
|
||
}
|
||
|
||
// lockOrInitUsage 行锁读取(不存在则创建)某用户某工具的周期用量;
|
||
// 周期到期或后台改短周期导致已过期时,就地归零并开启新周期。
|
||
func lockOrInitUsage(ctx context.Context, tx gdb.TX, userId int64, toolKey string, periodDays int) (*quotaUsageState, error) {
|
||
now := gtime.Now()
|
||
record, err := tx.Model(consts.TableQuotaUsage).
|
||
Where("user_id", userId).Where("tool_key", toolKey).LockUpdate().One()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if record.IsEmpty() {
|
||
data := g.Map{
|
||
"user_id": userId,
|
||
"tool_key": toolKey,
|
||
"period_start": now,
|
||
"period_end": now.AddDate(0, 0, periodDays),
|
||
"free_used": 0,
|
||
"member_used": 0,
|
||
"updated_at": now,
|
||
}
|
||
lastId, insertErr := tx.Model(consts.TableQuotaUsage).Data(data).InsertAndGetId()
|
||
if insertErr != nil {
|
||
// 并发下可能已被其它请求插入:重新加锁读取
|
||
record, err = tx.Model(consts.TableQuotaUsage).
|
||
Where("user_id", userId).Where("tool_key", toolKey).LockUpdate().One()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if record.IsEmpty() {
|
||
return nil, insertErr
|
||
}
|
||
} else {
|
||
return "aUsageState{
|
||
Id: lastId,
|
||
PeriodStart: now,
|
||
PeriodEnd: now.AddDate(0, 0, periodDays),
|
||
PeriodDaysSnapshot: periodDays,
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
state := "aUsageState{
|
||
Id: record["id"].Int64(),
|
||
FreeUsed: record["free_used"].Int(),
|
||
MemberUsed: record["member_used"].Int(),
|
||
PeriodStart: record["period_start"].GTime(),
|
||
PeriodEnd: record["period_end"].GTime(),
|
||
}
|
||
if state.PeriodStart == nil || state.PeriodStart.IsZero() {
|
||
return state.reset(ctx, tx, now, periodDays)
|
||
}
|
||
|
||
// 按「当前配置的周期天数」重算终点:后台改长/改短都能立刻对齐
|
||
expectedEnd := state.PeriodStart.AddDate(0, 0, periodDays)
|
||
if !now.Before(expectedEnd) {
|
||
// 周期已结束 → 归零开新周期
|
||
return state.reset(ctx, tx, now, periodDays)
|
||
}
|
||
if state.PeriodEnd == nil || !state.PeriodEnd.Equal(expectedEnd) {
|
||
if _, err = tx.Model(consts.TableQuotaUsage).Where("id", state.Id).Data(g.Map{
|
||
"period_end": expectedEnd,
|
||
"updated_at": now,
|
||
}).Update(); err != nil {
|
||
return nil, err
|
||
}
|
||
state.PeriodEnd = expectedEnd
|
||
}
|
||
return state, nil
|
||
}
|
||
|
||
func (s *quotaUsageState) reset(ctx context.Context, tx gdb.TX, now *gtime.Time, periodDays int) (*quotaUsageState, error) {
|
||
end := now.AddDate(0, 0, periodDays)
|
||
if _, err := tx.Model(consts.TableQuotaUsage).Where("id", s.Id).Data(g.Map{
|
||
"period_start": now,
|
||
"period_end": end,
|
||
"free_used": 0,
|
||
"member_used": 0,
|
||
"updated_at": now,
|
||
}).Update(); err != nil {
|
||
return nil, err
|
||
}
|
||
s.FreeUsed = 0
|
||
s.MemberUsed = 0
|
||
s.PeriodStart = now
|
||
s.PeriodEnd = end
|
||
s.ResetOccurred = true
|
||
s.PeriodDaysSnapshot = periodDays
|
||
return s, nil
|
||
}
|
||
|
||
// ===== 付费额度余额 =====
|
||
|
||
type paidBalance struct {
|
||
Id int64
|
||
TimesLeft int
|
||
ExpireAt *gtime.Time
|
||
Exists bool
|
||
}
|
||
|
||
func paidExpired(b *paidBalance, now *gtime.Time) bool {
|
||
if b == nil || !b.Exists {
|
||
return true
|
||
}
|
||
if b.ExpireAt == nil || b.ExpireAt.IsZero() {
|
||
return true
|
||
}
|
||
return !now.Before(b.ExpireAt)
|
||
}
|
||
|
||
// lockPaidBalance 行锁读取用户在某工具上的付费额度余额;为空时返回不存在(不创建)
|
||
func lockPaidBalance(ctx context.Context, tx gdb.TX, userId int64, toolKey string) (*paidBalance, error) {
|
||
record, err := tx.Model(consts.TableUserToolQuota).
|
||
Where("user_id", userId).Where("tool_key", toolKey).LockUpdate().One()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if record.IsEmpty() {
|
||
return &paidBalance{Exists: false}, nil
|
||
}
|
||
return &paidBalance{
|
||
Id: record["id"].Int64(),
|
||
TimesLeft: record["times_left"].Int(),
|
||
ExpireAt: record["expire_at"].GTime(),
|
||
Exists: true,
|
||
}, nil
|
||
}
|
||
|
||
// grantToolQuota 发货:给用户增加某工具的付费额度。
|
||
// 同工具重复购买 → 次数累加、有效期顺延(base = max(now, 当前到期时间))。
|
||
func grantToolQuota(
|
||
ctx context.Context, tx gdb.TX, userId int64, toolKey string, times, validDays int,
|
||
) (*gtime.Time, error) {
|
||
if times <= 0 {
|
||
return nil, gerror.New("次数包次数异常")
|
||
}
|
||
if validDays <= 0 {
|
||
validDays = 365
|
||
}
|
||
now := gtime.Now()
|
||
record, err := tx.Model(consts.TableUserToolQuota).
|
||
Where("user_id", userId).Where("tool_key", toolKey).LockUpdate().One()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
base := now
|
||
curLeft := 0
|
||
if !record.IsEmpty() {
|
||
curExpire := record["expire_at"].GTime()
|
||
curLeft = record["times_left"].Int()
|
||
if curExpire != nil && curExpire.After(now) {
|
||
base = curExpire
|
||
} else {
|
||
curLeft = 0 // 已过期:旧余额作废,重新计
|
||
}
|
||
}
|
||
expireAt := base.AddDate(0, 0, validDays)
|
||
|
||
if record.IsEmpty() {
|
||
if _, err = tx.Model(consts.TableUserToolQuota).Data(g.Map{
|
||
"user_id": userId,
|
||
"tool_key": toolKey,
|
||
"times_left": times,
|
||
"total_bought": times,
|
||
"expire_at": expireAt,
|
||
"created_at": now,
|
||
"updated_at": now,
|
||
}).Insert(); err != nil {
|
||
return nil, err
|
||
}
|
||
return expireAt, nil
|
||
}
|
||
if _, err = tx.Model(consts.TableUserToolQuota).Where("id", record["id"].Int64()).Data(g.Map{
|
||
"times_left": curLeft + times,
|
||
"total_bought": record["total_bought"].Int() + times,
|
||
"expire_at": expireAt,
|
||
"updated_at": now,
|
||
}).Update(); err != nil {
|
||
return nil, err
|
||
}
|
||
return expireAt, nil
|
||
}
|
||
|
||
// ===== 额度判定 =====
|
||
|
||
// judgeQuota 计算当前会命中哪个池,以及还有没有额度
|
||
func judgeQuota(tool *entity.Tools, usage *quotaUsageState, paid *paidBalance, isMember bool, now *gtime.Time) quotaVerdict {
|
||
payCost := tool.PayCost
|
||
if payCost <= 0 {
|
||
payCost = 1
|
||
}
|
||
if usage == nil || usage.FreeUsed < tool.FreeQuota {
|
||
return quotaVerdict{Source: consts.QuotaSourceFree}
|
||
}
|
||
if isMember && usage.MemberUsed < tool.MemberQuota {
|
||
return quotaVerdict{Source: consts.QuotaSourceMember}
|
||
}
|
||
if paid != nil && paid.Exists && !paidExpired(paid, now) && paid.TimesLeft >= payCost {
|
||
return quotaVerdict{Source: consts.QuotaSourcePaid}
|
||
}
|
||
return quotaVerdict{Source: consts.QuotaSourceNone, Message: quotaExhaustedMessage(tool)}
|
||
}
|
||
|
||
func quotaExhaustedMessage(tool *entity.Tools) string {
|
||
return "「" + tool.Name + "」本周期额度已用完,开通会员可获得更多额度,或购买次数包继续使用"
|
||
}
|
||
|
||
// ConsumeToolQuota 扣减一次额度(产出结果时调用)。
|
||
//
|
||
// 事务内按 out_trade_no 之外的另一把锁保证并发安全:行锁落在 user_quota_usage /
|
||
// user_tool_quota 上,两个请求同时扣同一个人同一工具时串行执行,不会超发。
|
||
func ConsumeToolQuota(ctx context.Context, userId int64, toolKey string) (*v1.QuotaToolOut, error) {
|
||
user, err := getUserById(ctx, userId)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
tool, err := toolRow(ctx, toolKey)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if tool.IsEnabled != 1 {
|
||
return nil, gerror.New("工具不存在或已下架")
|
||
}
|
||
isMember := EffectiveLevelKey(user) != consts.DefaultLevelKey
|
||
periodDays := EffectiveQuotaPeriodDays(ctx, user)
|
||
now := gtime.Now()
|
||
|
||
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||
usage, err := lockOrInitUsage(ctx, tx, userId, toolKey, periodDays)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
paid, err := lockPaidBalance(ctx, tx, userId, toolKey)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
verdict := judgeQuota(tool, usage, paid, isMember, now)
|
||
switch verdict.Source {
|
||
case consts.QuotaSourceFree:
|
||
if _, err = tx.Model(consts.TableQuotaUsage).Where("id", usage.Id).Data(g.Map{
|
||
"free_used": usage.FreeUsed + 1,
|
||
"updated_at": now,
|
||
}).Update(); err != nil {
|
||
return err
|
||
}
|
||
case consts.QuotaSourceMember:
|
||
if _, err = tx.Model(consts.TableQuotaUsage).Where("id", usage.Id).Data(g.Map{
|
||
"member_used": usage.MemberUsed + 1,
|
||
"updated_at": now,
|
||
}).Update(); err != nil {
|
||
return err
|
||
}
|
||
case consts.QuotaSourcePaid:
|
||
payCost := tool.PayCost
|
||
if payCost <= 0 {
|
||
payCost = 1
|
||
}
|
||
if _, err = tx.Model(consts.TableUserToolQuota).Where("id", paid.Id).Data(g.Map{
|
||
"times_left": paid.TimesLeft - payCost,
|
||
"updated_at": now,
|
||
}).Update(); err != nil {
|
||
return err
|
||
}
|
||
default:
|
||
return gerror.NewCode(gcodeQuotaExhausted(), verdict.Message)
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 扣减后回读一次真实状态(含周期已滚动的情形)
|
||
status, err := ToolQuotaStatus(ctx, user, tool)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return status, nil
|
||
}
|
||
|
||
// ===== 状态查询 =====
|
||
|
||
// ToolQuotaStatus 单个工具的额度状态
|
||
func ToolQuotaStatus(ctx context.Context, user *entity.Users, tool *entity.Tools) (*v1.QuotaToolOut, error) {
|
||
isMember := EffectiveLevelKey(user) != consts.DefaultLevelKey
|
||
periodDays := EffectiveQuotaPeriodDays(ctx, user)
|
||
now := gtime.Now()
|
||
|
||
usage := "aUsageState{}
|
||
record, err := g.Model(consts.TableQuotaUsage).
|
||
Where("user_id", user.Id).Where("tool_key", tool.ToolKey).One()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !record.IsEmpty() {
|
||
usage.Id = record["id"].Int64()
|
||
usage.FreeUsed = record["free_used"].Int()
|
||
usage.MemberUsed = record["member_used"].Int()
|
||
usage.PeriodStart = record["period_start"].GTime()
|
||
usage.PeriodEnd = record["period_end"].GTime()
|
||
// 只读场景不写库:过期即视为用量归零
|
||
if usage.PeriodEnd == nil || !now.Before(usage.PeriodEnd) {
|
||
usage.FreeUsed = 0
|
||
usage.MemberUsed = 0
|
||
usage.PeriodStart = now
|
||
usage.PeriodEnd = now.AddDate(0, 0, periodDays)
|
||
} else if usage.PeriodStart != nil {
|
||
expectedEnd := usage.PeriodStart.AddDate(0, 0, periodDays)
|
||
if expectedEnd.After(now) {
|
||
usage.PeriodEnd = expectedEnd
|
||
}
|
||
}
|
||
} else {
|
||
usage.PeriodStart = now
|
||
usage.PeriodEnd = now.AddDate(0, 0, periodDays)
|
||
}
|
||
|
||
paid := &paidBalance{}
|
||
paidRecord, err := g.Model(consts.TableUserToolQuota).
|
||
Where("user_id", user.Id).Where("tool_key", tool.ToolKey).One()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !paidRecord.IsEmpty() {
|
||
paid.Exists = true
|
||
paid.Id = paidRecord["id"].Int64()
|
||
paid.TimesLeft = paidRecord["times_left"].Int()
|
||
paid.ExpireAt = paidRecord["expire_at"].GTime()
|
||
}
|
||
|
||
return buildQuotaOut(tool, usage, paid, isMember, periodDays, now), nil
|
||
}
|
||
|
||
// QuotaSnapshotForTools 批量计算多个工具的额度状态(2 次查询搞定,避免 11 个工具 11 次查库)
|
||
func QuotaSnapshotForTools(
|
||
ctx context.Context, user *entity.Users, tools []*entity.Tools,
|
||
) (map[string]*v1.QuotaToolOut, error) {
|
||
out := make(map[string]*v1.QuotaToolOut, len(tools))
|
||
if len(tools) == 0 {
|
||
return out, nil
|
||
}
|
||
isMember := EffectiveLevelKey(user) != consts.DefaultLevelKey
|
||
periodDays := EffectiveQuotaPeriodDays(ctx, user)
|
||
now := gtime.Now()
|
||
|
||
usageRecords, err := g.Model(consts.TableQuotaUsage).Where("user_id", user.Id).All()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
usageMap := map[string]*quotaUsageState{}
|
||
for _, r := range usageRecords {
|
||
usageMap[r["tool_key"].String()] = "aUsageState{
|
||
Id: r["id"].Int64(),
|
||
FreeUsed: r["free_used"].Int(),
|
||
MemberUsed: r["member_used"].Int(),
|
||
PeriodStart: r["period_start"].GTime(),
|
||
PeriodEnd: r["period_end"].GTime(),
|
||
}
|
||
}
|
||
|
||
paidRecords, err := g.Model(consts.TableUserToolQuota).Where("user_id", user.Id).All()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
paidMap := map[string]*paidBalance{}
|
||
for _, r := range paidRecords {
|
||
paidMap[r["tool_key"].String()] = &paidBalance{
|
||
Id: r["id"].Int64(),
|
||
TimesLeft: r["times_left"].Int(),
|
||
ExpireAt: r["expire_at"].GTime(),
|
||
Exists: true,
|
||
}
|
||
}
|
||
|
||
for _, tool := range tools {
|
||
usage := usageMap[tool.ToolKey]
|
||
if usage == nil {
|
||
usage = "aUsageState{}
|
||
}
|
||
// 只读路径不写库:过期即按归零展示
|
||
if usage.PeriodEnd == nil || !now.Before(usage.PeriodEnd) {
|
||
usage.FreeUsed = 0
|
||
usage.MemberUsed = 0
|
||
usage.PeriodStart = now
|
||
usage.PeriodEnd = now.AddDate(0, 0, periodDays)
|
||
} else if usage.PeriodStart != nil {
|
||
if expectedEnd := usage.PeriodStart.AddDate(0, 0, periodDays); expectedEnd.After(now) {
|
||
usage.PeriodEnd = expectedEnd
|
||
}
|
||
}
|
||
paid := paidMap[tool.ToolKey]
|
||
if paid == nil {
|
||
paid = &paidBalance{}
|
||
}
|
||
out[tool.ToolKey] = buildQuotaOut(tool, usage, paid, isMember, periodDays, now)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func buildQuotaOut(
|
||
tool *entity.Tools, usage *quotaUsageState, paid *paidBalance,
|
||
isMember bool, periodDays int, now *gtime.Time,
|
||
) *v1.QuotaToolOut {
|
||
payCost := tool.PayCost
|
||
if payCost <= 0 {
|
||
payCost = 1
|
||
}
|
||
freeLeft := clampMin(tool.FreeQuota-usage.FreeUsed, 0)
|
||
memberLeft := 0
|
||
if isMember {
|
||
memberLeft = clampMin(tool.MemberQuota-usage.MemberUsed, 0)
|
||
}
|
||
paidLeft := 0
|
||
paidExpireAt := ""
|
||
if paid != nil && paid.Exists && !paidExpired(paid, now) {
|
||
paidLeft = clampMax(paid.TimesLeft, 0)
|
||
if paid.ExpireAt != nil && !paid.ExpireAt.IsZero() {
|
||
paidExpireAt = paid.ExpireAt.Format("Y-m-d")
|
||
}
|
||
}
|
||
|
||
verdict := judgeQuota(tool, usage, paid, isMember, now)
|
||
totalLeft := freeLeft + memberLeft + paidLeft/payCost
|
||
|
||
out := &v1.QuotaToolOut{
|
||
ToolKey: tool.ToolKey,
|
||
Name: tool.Name,
|
||
Icon: tool.Icon,
|
||
FreeTotal: tool.FreeQuota,
|
||
FreeUsed: usage.FreeUsed,
|
||
FreeLeft: freeLeft,
|
||
MemberTotal: tool.MemberQuota,
|
||
MemberUsed: usage.MemberUsed,
|
||
MemberLeft: memberLeft,
|
||
PaidLeft: paidLeft,
|
||
PaidExpireAt: paidExpireAt,
|
||
PayCost: payCost,
|
||
IsMember: isMember,
|
||
PeriodDays: periodDays,
|
||
Source: verdict.Source,
|
||
CanUse: verdict.Source != consts.QuotaSourceNone,
|
||
TotalLeft: totalLeft,
|
||
}
|
||
if usage.PeriodEnd != nil && !usage.PeriodEnd.IsZero() {
|
||
out.PeriodEnd = usage.PeriodEnd.Format("Y-m-d H:i:s")
|
||
}
|
||
if verdict.Source == consts.QuotaSourceNone {
|
||
out.Message = verdict.Message
|
||
}
|
||
return out
|
||
}
|
||
|
||
func clampMin(v, min int) int {
|
||
if v < min {
|
||
return min
|
||
}
|
||
return v
|
||
}
|
||
|
||
func clampMax(v, max int) int {
|
||
if v > max {
|
||
return max
|
||
}
|
||
return v
|
||
}
|
||
|
||
func gcodeQuotaExhausted() gcode.Code {
|
||
return gcode.New(consts.CodeQuotaExhausted, "", nil)
|
||
}
|