Files
sub2api/backend/internal/service/user_platform_quota_port.go
T
feeeei 4a4b50d807 fix(quota): 平台限额表只保存已配置限额的记录,仪表盘按用量与限额展示平台
user_platform_quotas 只保存至少配置了一档限额的记录;不存在的行等价于不限额。

- 注册播种只为配置了限额的平台建行,一个都没有时不写入
- IncrementUsageWithReset 无行时不再 fail-open 建行
- BatchSnapshotUsage 改为只更新既有活跃行,不再插入无限额的新行
- 仓储 BulkInsertInitial/UpsertForUser 把三档全空的记录视为未配置;
  管理员全量替换接口同样剔除,清空限额即软删并放弃累计用量
- 迁移 238 删除三档全 NULL 的历史行
- 用户仪表盘平台卡片 = 有用量的平台 ∪ 配置了限额的平台,计数改为卡片数
- 管理员限额弹窗对未保存限额的平台禁用"重置窗口"
2026-09-10 16:41:58 +08:00

77 lines
4.1 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 service
import (
"context"
"errors"
"time"
)
// ErrUserPlatformQuotaNotFound service 层 sentinel:quota 记录不存在。
// adapter 将 repository.ErrUserPlatformQuotaNotFound 包装为此错误,
// handler 只需引用 service 包,无需直接依赖 repository 包。
var ErrUserPlatformQuotaNotFound = errors.New("user platform quota not found")
// ErrUserPlatformQuotaFKViolation service 层 sentinel:批量快照写入命中了 user_id 不在 users 表的记录。
// adapter 负责将 repository 层同名 sentinel 包装为此错误。
var ErrUserPlatformQuotaFKViolation = errors.New("user platform quota snapshot FK violation")
// UserPlatformQuotaSnapshot 是 service 层 flusher 向 DB 写入快照时使用的传输结构。
// 字段语义与 repository.UserPlatformQuotaSnapshot 完全对应,由 adapter 负责转换。
type UserPlatformQuotaSnapshot struct {
UserID int64
Platform string
DailyUsageUSD float64
WeeklyUsageUSD float64
MonthlyUsageUSD float64
DailyWindowStart time.Time
WeeklyWindowStart time.Time
MonthlyWindowStart time.Time
}
// UserPlatformQuotaRecord service 层传输结构体(与 repository 层解耦)。
type UserPlatformQuotaRecord struct {
UserID int64
Platform string
DailyLimitUSD *float64
WeeklyLimitUSD *float64
MonthlyLimitUSD *float64
DailyUsageUSD float64
WeeklyUsageUSD float64
MonthlyUsageUSD float64
// 窗口起始时间(可选,用于未来 reset 校验)
DailyWindowStart *time.Time
WeeklyWindowStart *time.Time
MonthlyWindowStart *time.Time
}
// HasAnyLimit 报告该记录是否至少配置了一档限额(0 也算配置)。
// 三档全空的记录视为未配置:不写入 user_platform_quotas,等价于不限额。
func (r UserPlatformQuotaRecord) HasAnyLimit() bool {
return r.DailyLimitUSD != nil || r.WeeklyLimitUSD != nil || r.MonthlyLimitUSD != nil
}
// UserPlatformQuotaRepository 定义 service 层所需的 user × platform quota 数据访问端口。
// repository 包的 userPlatformQuotaRepository 实现此接口。
type UserPlatformQuotaRepository interface {
// GetByUserPlatform 查询单条配额记录,未找到时返回 (nil, nil)。
GetByUserPlatform(ctx context.Context, userID int64, platform string) (*UserPlatformQuotaRecord, error)
// BulkInsertInitial 幂等批量插入初始配额记录:冲突时只补既有行为 NULL 的档位;三档全空的记录跳过。
BulkInsertInitial(ctx context.Context, records []UserPlatformQuotaRecord) error
// IncrementUsageWithReset 原子地累加用量,若窗口已过期则先重置再累加;记录不存在时不建行、直接返回 nil。
IncrementUsageWithReset(ctx context.Context, userID int64, platform string, cost float64, now time.Time) error
// ListByUser 查询用户的所有平台配额记录。
ListByUser(ctx context.Context, userID int64) ([]UserPlatformQuotaRecord, error)
// UpsertForUser 全量替换该用户所有平台限额配置(事务内):
// 1. 软删除未在 records 中出现、或在 records 中但三档全空的所有 active 行
// 2. 对每条至少配置了一档限额的 record:UPDATE 已存在的 active 行;未命中时 INSERT
// 仅改 *_limit_usd + updated_at,保留 *_usage_usd / *_window_start。
// 清空某平台的限额即软删该行并放弃其累计用量;之后再次配置限额会新建行、从新窗口起算。
// records 为空时仅执行步骤 1。
UpsertForUser(ctx context.Context, userID int64, records []UserPlatformQuotaRecord) error
// ResetExpiredWindow 重置指定窗口("daily"|"weekly"|"monthly")的用量与起始时间。
// 未命中活跃记录时返回(service-side wrapper of repository.ErrUserPlatformQuotaNotFound)。
ResetExpiredWindow(ctx context.Context, userID int64, platform string, window string, newStart time.Time) error
// BatchSnapshotUsage 绝对值覆盖写入整批 usage 快照到既有活跃行;行不存在或已软删的 (user,platform) 跳过。
BatchSnapshotUsage(ctx context.Context, snapshots []UserPlatformQuotaSnapshot, now time.Time) error
}