thread-master/apps/backend/internal/module/usage/domain/usage.go

168 lines
5.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 domain
import (
"errors"
"strings"
"time"
)
const (
KeyModePlatform = "platform"
KeyModeByok = "byok"
MeterAICopy = "ai_copy"
MeterAIResearch = "ai_research"
MeterWebSearch = "web_search"
MeterAIImage = "ai_image"
PlanFree = "free"
PlanStarter = "starter"
PlanPro = "pro"
)
var (
ErrNotFound = errors.New("usage not found")
ErrForbidden = errors.New("usage forbidden")
ErrNoKey = errors.New("no api key configured (platform or byok)")
ErrQuotaExceeded = errors.New("platform credits insufficient")
// 分項點數上限(與 FE soft_caps 對齊platform only
ErrMeterCapExceeded = errors.New("usage.err.meterCap")
// 平台 key 無容量/限流:僅 BYOK 可繼續(不扣平台點)
ErrPlatformCapacity = errors.New("usage.err.platformCapacity")
ErrInvalidPlan = errors.New("invalid plan")
)
type Event struct {
ID string `bson:"_id" json:"id"`
UID int64 `bson:"uid" json:"uid"`
Meter string `bson:"meter" json:"meter"`
Credits int `bson:"credits" json:"credits"` // platform only; byok always 0
KeyMode string `bson:"key_mode" json:"key_mode"`
Label string `bson:"label" json:"label"`
Source string `bson:"source" json:"source"`
MonthKey string `bson:"month_key" json:"month_key"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}
type MemberPrefs struct {
UID int64 `bson:"uid" json:"uid"`
PlanID string `bson:"plan_id" json:"plan_id"`
Unlimited bool `bson:"unlimited" json:"unlimited"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
}
type Purchase struct {
ID string `bson:"_id" json:"id"`
UID int64 `bson:"uid" json:"uid"`
PlanID string `bson:"plan_id" json:"plan_id"`
MockRef string `bson:"mock_ref,omitempty" json:"mock_ref,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}
type PlanDef struct {
ID string
MonthlyCredits int
// SoftCaps各 meter 點數硬上限platform加總 = MonthlyCredits
SoftCaps map[string]int
}
// Plans — 與 FE PLANS 同步2026-07 · 組合毛利 ≥50%
// 安全單價約 $0.012Starter NT$590≈$18.4 / Pro NT$1990≈$62.2
// Free 120 點:夠用試用(可虧 ~$1.44 滿用);付費扛回組合毛利。
var Plans = map[string]PlanDef{
PlanFree: {
ID: PlanFree, MonthlyCredits: 120,
SoftCaps: map[string]int{
MeterAICopy: 60, MeterAIResearch: 15, MeterWebSearch: 30, MeterAIImage: 15,
},
},
PlanStarter: {
ID: PlanStarter, MonthlyCredits: 600,
SoftCaps: map[string]int{
MeterAICopy: 300, MeterAIResearch: 90, MeterWebSearch: 150, MeterAIImage: 60,
},
},
PlanPro: {
ID: PlanPro, MonthlyCredits: 2000,
SoftCaps: map[string]int{
MeterAICopy: 1000, MeterAIResearch: 300, MeterWebSearch: 500, MeterAIImage: 200,
},
},
}
var DefaultCreditCost = map[string]int{
MeterAICopy: 1, MeterAIResearch: 3, MeterWebSearch: 1, MeterAIImage: 5,
}
// ResolvePlan — 正規化 plan_id小寫並回傳方案未知 → Free
func ResolvePlan(planID string) PlanDef {
id := strings.ToLower(strings.TrimSpace(planID))
if p, ok := Plans[id]; ok && p.ID != "" {
return p
}
return Plans[PlanFree]
}
// MeterCost returns platform credit cost for one call.
func MeterCost(meter string) int {
if c := DefaultCreditCost[meter]; c > 0 {
return c
}
return 1
}
func NowNano() int64 { return time.Now().UTC().UnixNano() }
func MonthKey(nano int64) string {
t := time.Unix(0, nano).UTC()
return t.Format("2006-01")
}
func CurrentMonthKey() string { return MonthKey(NowNano()) }
// Summary split — never merge platform+byok into one total_calls only.
type MeterCount struct {
Count int `json:"count"`
Credits int `json:"credits"`
}
type PlatformBlock struct {
CreditsUsed int `json:"credits_used"`
CreditsRemaining int `json:"credits_remaining"`
CreditsTotal int `json:"credits_total"`
CallCount int `json:"call_count"`
ByMeter map[string]MeterCount `json:"by_meter"`
}
type ByokBlock struct {
CallCount int `json:"call_count"`
ByMeter map[string]MeterCount `json:"by_meter"`
}
type MonthSummary struct {
MonthKey string `json:"month_key"`
UID int64 `json:"uid"`
PlanID string `json:"plan_id"`
Unlimited bool `json:"unlimited"`
Platform PlatformBlock `json:"platform"`
Byok ByokBlock `json:"byok"`
// Legacy FE bars: only platform credits (not mixed)
TotalCredits int `json:"total_credits"`
RemainingCredits int `json:"remaining_credits"`
}
type TenantRow struct {
UID int64 `json:"uid"`
PlanID string `json:"plan_id"`
Unlimited bool `json:"unlimited"`
PlatformCreditsUsed int `json:"platform_credits_used"`
ByokCallCount int `json:"byok_call_count"`
}
type TenantSummary struct {
MonthKey string `json:"month_key"`
PlatformCreditsUsed int `json:"platform_credits_used"`
ByokCallCount int `json:"byok_call_count"`
Members []TenantRow `json:"members"`
}