| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007 |
- // Package dto provides data transfer objects for HTTP handlers.
- package dto
- import (
- "strconv"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/service"
- )
- func UserFromServiceShallow(u *service.User) *User {
- if u == nil {
- return nil
- }
- return &User{
- ID: u.ID,
- Email: u.Email,
- Username: u.Username,
- Role: u.Role,
- Balance: u.Balance,
- FrozenBalance: u.FrozenBalance,
- Concurrency: u.Concurrency,
- Status: u.Status,
- AllowedGroups: u.AllowedGroups,
- LastActiveAt: u.LastActiveAt,
- CreatedAt: u.CreatedAt,
- UpdatedAt: u.UpdatedAt,
- BalanceNotifyEnabled: u.BalanceNotifyEnabled,
- BalanceNotifyThresholdType: u.BalanceNotifyThresholdType,
- BalanceNotifyThreshold: u.BalanceNotifyThreshold,
- BalanceNotifyExtraEmails: NotifyEmailEntriesFromService(u.BalanceNotifyExtraEmails),
- TotalRecharged: u.TotalRecharged,
- RPMLimit: u.RPMLimit,
- DeletedAt: u.DeletedAt,
- }
- }
- func UserFromService(u *service.User) *User {
- if u == nil {
- return nil
- }
- out := UserFromServiceShallow(u)
- if len(u.APIKeys) > 0 {
- out.APIKeys = make([]APIKey, 0, len(u.APIKeys))
- for i := range u.APIKeys {
- k := u.APIKeys[i]
- out.APIKeys = append(out.APIKeys, *APIKeyFromService(&k))
- }
- }
- if len(u.Subscriptions) > 0 {
- out.Subscriptions = make([]UserSubscription, 0, len(u.Subscriptions))
- for i := range u.Subscriptions {
- s := u.Subscriptions[i]
- out.Subscriptions = append(out.Subscriptions, *UserSubscriptionFromService(&s))
- }
- }
- return out
- }
- // UserFromServiceAdmin converts a service User to DTO for admin users.
- // It includes notes - user-facing endpoints must not use this.
- func UserFromServiceAdmin(u *service.User) *AdminUser {
- if u == nil {
- return nil
- }
- base := UserFromService(u)
- if base == nil {
- return nil
- }
- return &AdminUser{
- User: *base,
- Notes: u.Notes,
- LastUsedAt: u.LastUsedAt,
- GroupRates: u.GroupRates,
- RestrictPublicGroups: u.RestrictPublicGroups,
- }
- }
- func APIKeyFromService(k *service.APIKey) *APIKey {
- if k == nil {
- return nil
- }
- out := &APIKey{
- ID: k.ID,
- UserID: k.UserID,
- Key: k.Key,
- Name: k.Name,
- GroupID: k.GroupID,
- Status: k.Status,
- IPWhitelist: k.IPWhitelist,
- IPBlacklist: k.IPBlacklist,
- LastUsedAt: k.LastUsedAt,
- LastUsedIP: k.LastUsedIP,
- Quota: k.Quota,
- QuotaUsed: k.QuotaUsed,
- ExpiresAt: k.ExpiresAt,
- CreatedAt: k.CreatedAt,
- UpdatedAt: k.UpdatedAt,
- CurrentConcurrency: k.CurrentConcurrency,
- RateLimit5h: k.RateLimit5h,
- RateLimit1d: k.RateLimit1d,
- RateLimit7d: k.RateLimit7d,
- Usage5h: k.EffectiveUsage5h(),
- Usage1d: k.EffectiveUsage1d(),
- Usage7d: k.EffectiveUsage7d(),
- Window5hStart: k.Window5hStart,
- Window1dStart: k.Window1dStart,
- Window7dStart: k.Window7dStart,
- User: UserFromServiceShallow(k.User),
- Group: GroupFromServiceShallow(k.Group),
- }
- if k.Window5hStart != nil && !service.IsWindowExpired(k.Window5hStart, service.RateLimitWindow5h) {
- t := k.Window5hStart.Add(service.RateLimitWindow5h)
- out.Reset5hAt = &t
- }
- if k.Window1dStart != nil && !service.IsWindowExpired(k.Window1dStart, service.RateLimitWindow1d) {
- t := k.Window1dStart.Add(service.RateLimitWindow1d)
- out.Reset1dAt = &t
- }
- if k.Window7dStart != nil && !service.IsWindowExpired(k.Window7dStart, service.RateLimitWindow7d) {
- t := k.Window7dStart.Add(service.RateLimitWindow7d)
- out.Reset7dAt = &t
- }
- if k.Key == "" && k.KeyHash != "" {
- out.KeyDisplay = k.KeyPrefix + "********"
- }
- return out
- }
- func GroupFromServiceShallow(g *service.Group) *Group {
- if g == nil {
- return nil
- }
- out := groupFromServiceBase(g)
- return &out
- }
- func GroupFromService(g *service.Group) *Group {
- if g == nil {
- return nil
- }
- return GroupFromServiceShallow(g)
- }
- // GroupFromServiceAdmin converts a service Group to DTO for admin users.
- // It includes internal fields like model_routing and account_count.
- func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
- if g == nil {
- return nil
- }
- out := &AdminGroup{
- Group: groupFromServiceBase(g),
- ForceOpenAIFast: g.ForceOpenAIFast,
- FreeOpenAIFast: g.FreeOpenAIFast,
- ProfitControlEnabled: g.ProfitControlEnabled,
- ProfitMinMargin: g.ProfitMinMargin,
- ProfitSafetyBuffer: g.ProfitSafetyBuffer,
- ModelPricing: g.ModelPricing,
- ModelRouting: g.ModelRouting,
- ModelRoutingEnabled: g.ModelRoutingEnabled,
- MCPXMLInject: g.MCPXMLInject,
- DefaultMappedModel: g.DefaultMappedModel,
- MessagesDispatchModelConfig: g.MessagesDispatchModelConfig,
- ModelAllowlist: g.ModelAllowlist,
- CodexModelsManifestConfig: g.CodexModelsManifestConfig,
- SupportedModelScopes: g.SupportedModelScopes,
- AccountCount: g.AccountCount,
- ActiveAccountCount: g.ActiveAccountCount,
- RateLimitedAccountCount: g.RateLimitedAccountCount,
- SortOrder: g.SortOrder,
- }
- if len(g.AccountGroups) > 0 {
- out.AccountGroups = make([]AccountGroup, 0, len(g.AccountGroups))
- for i := range g.AccountGroups {
- ag := g.AccountGroups[i]
- out.AccountGroups = append(out.AccountGroups, *AccountGroupFromService(&ag))
- }
- }
- return out
- }
- func groupFromServiceBase(g *service.Group) Group {
- return Group{
- ID: g.ID,
- Name: g.Name,
- Description: g.Description,
- Platform: g.Platform,
- RateMultiplier: g.RateMultiplier,
- IsExclusive: g.IsExclusive,
- SecurityPolicyEnabled: g.SecurityPolicyEnabled,
- SecurityPolicyMode: g.SecurityPolicyMode,
- SecurityPolicyEmailEnabled: g.SecurityPolicyEmailEnabled,
- Status: g.Status,
- SubscriptionType: g.SubscriptionType,
- DailyLimitUSD: g.DailyLimitUSD,
- WeeklyLimitUSD: g.WeeklyLimitUSD,
- MonthlyLimitUSD: g.MonthlyLimitUSD,
- LongContextPricingEnabled: g.LongContextPricingEnabled,
- AllowImageGeneration: g.AllowImageGeneration,
- AllowBatchImageGeneration: g.AllowBatchImageGeneration,
- ImageRateIndependent: g.ImageRateIndependent,
- ImageRateMultiplier: g.ImageRateMultiplier,
- BatchImageDiscountMultiplier: g.BatchImageDiscountMultiplier,
- BatchImageHoldMultiplier: g.BatchImageHoldMultiplier,
- VideoRateIndependent: g.VideoRateIndependent,
- VideoRateMultiplier: g.VideoRateMultiplier,
- PeakRateEnabled: g.PeakRateEnabled,
- PeakStart: g.PeakStart,
- PeakEnd: g.PeakEnd,
- PeakRateMultiplier: g.PeakRateMultiplier,
- ImagePrice1K: g.ImagePrice1K,
- ImagePrice2K: g.ImagePrice2K,
- ImagePrice4K: g.ImagePrice4K,
- VideoPrice480P: g.VideoPrice480P,
- VideoPrice720P: g.VideoPrice720P,
- VideoPrice1080P: g.VideoPrice1080P,
- VideoModelPrices: g.VideoModelPrices,
- WebSearchPricePerCall: g.WebSearchPricePerCall,
- SearchPricePer1k: g.SearchPricePer1k,
- AudioRealtimePricePerMin: g.AudioRealtimePricePerMin,
- AudioTtsPricePerMillionChars: g.AudioTTSPricePerMillionChars,
- AudioSttPricePerHour: g.AudioSTTPricePerHour,
- ClaudeCodeOnly: g.ClaudeCodeOnly,
- FallbackGroupID: g.FallbackGroupID,
- FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest,
- AllowMessagesDispatch: g.AllowMessagesDispatch,
- AllowLive: g.AllowLive,
- RequireOAuthOnly: g.RequireOAuthOnly,
- RequirePrivacySet: g.RequirePrivacySet,
- RPMLimit: g.RPMLimit,
- MaxReasoningEffort: g.MaxReasoningEffort,
- MaxReasoningEffortOverLimit: g.MaxReasoningEffortOverLimit,
- ReasoningEffortMappings: g.ReasoningEffortMappings,
- CreatedAt: g.CreatedAt,
- UpdatedAt: g.UpdatedAt,
- }
- }
- func AccountFromServiceShallow(a *service.Account) *Account {
- if a == nil {
- return nil
- }
- redactedCreds, credsStatus := RedactCredentials(a.Credentials)
- extra := redactAccountManagedExtra(a.Extra)
- var ollamaCloudUsage *service.OllamaCloudUsageState
- if state := service.OllamaCloudUsageStateFromAccount(a); state.Eligible {
- ollamaCloudUsage = state
- }
- out := &Account{
- AntiDegradation: a.AntiDegradationEnabled(),
- ProtectionScope: a.ProtectionScope(),
- ProtectionMode: a.ProtectionMode(),
- ID: a.ID,
- Name: a.Name,
- Notes: a.Notes,
- Platform: a.Platform,
- Type: a.Type,
- Credentials: redactedCreds,
- CredentialsStatus: credsStatus,
- Extra: extra,
- OllamaCloudUsage: ollamaCloudUsage,
- ProxyID: a.ProxyID,
- ProxyFallbackOriginID: a.ProxyFallbackOriginID,
- ProxyFallbackOriginName: a.ProxyFallbackOriginName,
- Concurrency: a.Concurrency,
- LoadFactor: a.LoadFactor,
- Priority: a.Priority,
- RateMultiplier: a.BillingRateMultiplier(),
- Status: a.Status,
- ErrorMessage: a.ErrorMessage,
- LastUsedAt: a.LastUsedAt,
- ExpiresAt: timeToUnixSeconds(a.ExpiresAt),
- AutoPauseOnExpired: a.AutoPauseOnExpired,
- CreatedAt: a.CreatedAt,
- UpdatedAt: a.UpdatedAt,
- Schedulable: a.Schedulable,
- RateLimitedAt: a.RateLimitedAt,
- RateLimitResetAt: a.RateLimitResetAt,
- OverloadUntil: a.OverloadUntil,
- TempUnschedulableUntil: a.TempUnschedulableUntil,
- TempUnschedulableReason: a.TempUnschedulableReason,
- SessionWindowStart: a.SessionWindowStart,
- SessionWindowEnd: a.SessionWindowEnd,
- SessionWindowStatus: a.SessionWindowStatus,
- GroupIDs: a.GroupIDs,
- ParentAccountID: a.ParentAccountID,
- QuotaDimension: a.QuotaDimension,
- }
- // 提取 5h 窗口费用控制和会话数量控制配置(仅 Anthropic OAuth/SetupToken 账号有效)
- if a.IsAnthropicOAuthOrSetupToken() {
- if limit := a.GetWindowCostLimit(); limit > 0 {
- out.WindowCostLimit = &limit
- }
- if reserve := a.GetWindowCostStickyReserve(); reserve > 0 {
- out.WindowCostStickyReserve = &reserve
- }
- if maxSessions := a.GetMaxSessions(); maxSessions > 0 {
- out.MaxSessions = &maxSessions
- }
- if idleTimeout := a.GetSessionIdleTimeoutMinutes(); idleTimeout > 0 {
- out.SessionIdleTimeoutMin = &idleTimeout
- }
- if rpm := a.GetBaseRPM(); rpm > 0 {
- out.BaseRPM = &rpm
- strategy := a.GetRPMStrategy()
- out.RPMStrategy = &strategy
- buffer := a.GetRPMStickyBuffer()
- out.RPMStickyBuffer = &buffer
- }
- // 用户消息队列模式
- if mode := a.GetUserMsgQueueMode(); mode != "" {
- out.UserMsgQueueMode = &mode
- }
- // TLS指纹伪装开关
- if a.IsTLSFingerprintEnabled() {
- enabled := true
- out.EnableTLSFingerprint = &enabled
- }
- // TLS指纹模板ID
- if profileID := a.GetTLSFingerprintProfileID(); profileID > 0 {
- out.TLSFingerprintProfileID = &profileID
- }
- // 会话ID伪装开关
- if a.IsSessionIDMaskingEnabled() {
- enabled := true
- out.EnableSessionIDMasking = &enabled
- }
- // 缓存 TTL 强制替换
- if a.IsCacheTTLOverrideEnabled() {
- enabled := true
- out.CacheTTLOverrideEnabled = &enabled
- target := a.GetCacheTTLOverrideTarget()
- out.CacheTTLOverrideTarget = &target
- }
- // 自定义 Base URL 中继转发
- if a.IsCustomBaseURLEnabled() {
- enabled := true
- out.CustomBaseURLEnabled = &enabled
- if customURL := a.GetCustomBaseURL(); customURL != "" {
- out.CustomBaseURL = &customURL
- }
- }
- }
- // 提取账号配额限制(apikey / bedrock 类型有效)
- if a.IsAPIKeyOrBedrock() {
- if limit := a.GetQuotaLimit(); limit > 0 {
- out.QuotaLimit = &limit
- used := a.GetQuotaUsed()
- out.QuotaUsed = &used
- }
- if limit := a.GetQuotaDailyLimit(); limit > 0 {
- out.QuotaDailyLimit = &limit
- used := a.GetQuotaDailyUsed()
- if a.IsDailyQuotaPeriodExpired() {
- used = 0
- }
- out.QuotaDailyUsed = &used
- }
- if limit := a.GetQuotaWeeklyLimit(); limit > 0 {
- out.QuotaWeeklyLimit = &limit
- used := a.GetQuotaWeeklyUsed()
- if a.IsWeeklyQuotaPeriodExpired() {
- used = 0
- }
- out.QuotaWeeklyUsed = &used
- }
- // 固定时间重置配置
- if mode := a.GetQuotaDailyResetMode(); mode == "fixed" {
- out.QuotaDailyResetMode = &mode
- hour := a.GetQuotaDailyResetHour()
- out.QuotaDailyResetHour = &hour
- }
- if mode := a.GetQuotaWeeklyResetMode(); mode == "fixed" {
- out.QuotaWeeklyResetMode = &mode
- day := a.GetQuotaWeeklyResetDay()
- out.QuotaWeeklyResetDay = &day
- hour := a.GetQuotaWeeklyResetHour()
- out.QuotaWeeklyResetHour = &hour
- }
- if a.GetQuotaDailyResetMode() == "fixed" || a.GetQuotaWeeklyResetMode() == "fixed" {
- tz := a.GetQuotaResetTimezone()
- out.QuotaResetTimezone = &tz
- }
- if a.Extra != nil {
- if v, ok := a.Extra["quota_daily_reset_at"].(string); ok && v != "" {
- out.QuotaDailyResetAt = &v
- }
- if v, ok := a.Extra["quota_weekly_reset_at"].(string); ok && v != "" {
- out.QuotaWeeklyResetAt = &v
- }
- }
- // 配额通知配置
- if enabled := a.GetQuotaNotifyDailyEnabled(); enabled {
- out.QuotaNotifyDailyEnabled = &enabled
- }
- if threshold := a.GetQuotaNotifyDailyThreshold(); threshold > 0 {
- out.QuotaNotifyDailyThreshold = &threshold
- }
- if enabled := a.GetQuotaNotifyWeeklyEnabled(); enabled {
- out.QuotaNotifyWeeklyEnabled = &enabled
- }
- if threshold := a.GetQuotaNotifyWeeklyThreshold(); threshold > 0 {
- out.QuotaNotifyWeeklyThreshold = &threshold
- }
- if enabled := a.GetQuotaNotifyTotalEnabled(); enabled {
- out.QuotaNotifyTotalEnabled = &enabled
- }
- if threshold := a.GetQuotaNotifyTotalThreshold(); threshold > 0 {
- out.QuotaNotifyTotalThreshold = &threshold
- }
- }
- return out
- }
- func redactAccountManagedExtra(extra map[string]any) map[string]any {
- if extra == nil {
- return nil
- }
- redacted := make(map[string]any, len(extra))
- for key, value := range extra {
- switch key {
- case "codex_fingerprint_seed":
- continue
- case service.OllamaCloudUsageSessionExtraKey,
- service.OllamaCloudUsageAutoRefreshExtraKey,
- service.OllamaCloudUsageSnapshotExtraKey:
- continue
- default:
- redacted[key] = value
- }
- }
- return redacted
- }
- func AccountFromService(a *service.Account) *Account {
- if a == nil {
- return nil
- }
- out := AccountFromServiceShallow(a)
- out.Proxy = ProxyFromService(a.Proxy)
- if len(a.AccountGroups) > 0 {
- out.AccountGroups = make([]AccountGroup, 0, len(a.AccountGroups))
- for i := range a.AccountGroups {
- ag := a.AccountGroups[i]
- out.AccountGroups = append(out.AccountGroups, *AccountGroupFromService(&ag))
- }
- }
- if len(a.Groups) > 0 {
- out.Groups = make([]*Group, 0, len(a.Groups))
- for _, g := range a.Groups {
- out.Groups = append(out.Groups, GroupFromServiceShallow(g))
- }
- }
- return out
- }
- // AccountListItemFromAccount projects a full account response into the
- // compact shape used by the paginated admin account list. Keeping this
- // projection separate from Account preserves the existing detail API.
- func AccountListItemFromAccount(a *Account) *AccountListItem {
- if a == nil {
- return nil
- }
- return &AccountListItem{
- AntiDegradation: a.AntiDegradation,
- ProtectionScope: a.ProtectionScope,
- ProtectionMode: a.ProtectionMode,
- ID: a.ID, Name: a.Name, Notes: a.Notes, Platform: a.Platform, Type: a.Type,
- Credentials: a.Credentials, CredentialsStatus: a.CredentialsStatus, Extra: a.Extra,
- OllamaCloudUsage: a.OllamaCloudUsage,
- ProxyID: a.ProxyID, ProxyFallbackOriginID: a.ProxyFallbackOriginID, ProxyFallbackOriginName: a.ProxyFallbackOriginName,
- Concurrency: a.Concurrency, LoadFactor: a.LoadFactor, Priority: a.Priority, RateMultiplier: a.RateMultiplier,
- Status: a.Status, ErrorMessage: a.ErrorMessage, LastUsedAt: a.LastUsedAt, ExpiresAt: a.ExpiresAt,
- AutoPauseOnExpired: a.AutoPauseOnExpired, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt,
- Schedulable: a.Schedulable, RateLimitedAt: a.RateLimitedAt, RateLimitResetAt: a.RateLimitResetAt,
- OverloadUntil: a.OverloadUntil, TempUnschedulableUntil: a.TempUnschedulableUntil,
- TempUnschedulableReason: a.TempUnschedulableReason, SessionWindowStart: a.SessionWindowStart,
- SessionWindowEnd: a.SessionWindowEnd, SessionWindowStatus: a.SessionWindowStatus,
- WindowCostLimit: a.WindowCostLimit, WindowCostStickyReserve: a.WindowCostStickyReserve,
- MaxSessions: a.MaxSessions, SessionIdleTimeoutMin: a.SessionIdleTimeoutMin, BaseRPM: a.BaseRPM,
- RPMStrategy: a.RPMStrategy, RPMStickyBuffer: a.RPMStickyBuffer, UserMsgQueueMode: a.UserMsgQueueMode,
- EnableTLSFingerprint: a.EnableTLSFingerprint, TLSFingerprintProfileID: a.TLSFingerprintProfileID,
- EnableSessionIDMasking: a.EnableSessionIDMasking, CacheTTLOverrideEnabled: a.CacheTTLOverrideEnabled,
- CacheTTLOverrideTarget: a.CacheTTLOverrideTarget, CustomBaseURLEnabled: a.CustomBaseURLEnabled,
- CustomBaseURL: a.CustomBaseURL, QuotaLimit: a.QuotaLimit, QuotaUsed: a.QuotaUsed,
- QuotaDailyLimit: a.QuotaDailyLimit, QuotaDailyUsed: a.QuotaDailyUsed, QuotaWeeklyLimit: a.QuotaWeeklyLimit,
- QuotaWeeklyUsed: a.QuotaWeeklyUsed, QuotaDailyResetMode: a.QuotaDailyResetMode,
- QuotaDailyResetHour: a.QuotaDailyResetHour, QuotaWeeklyResetMode: a.QuotaWeeklyResetMode,
- QuotaWeeklyResetDay: a.QuotaWeeklyResetDay, QuotaWeeklyResetHour: a.QuotaWeeklyResetHour,
- QuotaResetTimezone: a.QuotaResetTimezone, QuotaDailyResetAt: a.QuotaDailyResetAt,
- QuotaWeeklyResetAt: a.QuotaWeeklyResetAt, QuotaNotifyDailyEnabled: a.QuotaNotifyDailyEnabled,
- QuotaNotifyDailyThreshold: a.QuotaNotifyDailyThreshold, QuotaNotifyWeeklyEnabled: a.QuotaNotifyWeeklyEnabled,
- QuotaNotifyWeeklyThreshold: a.QuotaNotifyWeeklyThreshold, QuotaNotifyTotalEnabled: a.QuotaNotifyTotalEnabled,
- QuotaNotifyTotalThreshold: a.QuotaNotifyTotalThreshold, ParentAccountID: a.ParentAccountID,
- QuotaDimension: a.QuotaDimension, ParentEmail: a.ParentEmail, ParentPlanType: a.ParentPlanType,
- ParentPrivacyMode: a.ParentPrivacyMode, ParentSubscriptionExpiresAt: a.ParentSubscriptionExpiresAt,
- ParentChatGPTAccountID: a.ParentChatGPTAccountID, Proxy: a.Proxy, GroupIDs: a.GroupIDs,
- }
- }
- func timeToUnixSeconds(value *time.Time) *int64 {
- if value == nil {
- return nil
- }
- ts := value.Unix()
- return &ts
- }
- func AccountGroupFromService(ag *service.AccountGroup) *AccountGroup {
- if ag == nil {
- return nil
- }
- return &AccountGroup{
- AccountID: ag.AccountID,
- GroupID: ag.GroupID,
- Priority: ag.Priority,
- CreatedAt: ag.CreatedAt,
- Account: AccountFromServiceShallow(ag.Account),
- Group: GroupFromServiceShallow(ag.Group),
- }
- }
- func ProxyFromService(p *service.Proxy) *Proxy {
- if p == nil {
- return nil
- }
- return &Proxy{
- ID: p.ID,
- Name: p.Name,
- Protocol: p.Protocol,
- Host: p.Host,
- Port: p.Port,
- Username: p.Username,
- Status: p.Status,
- CreatedAt: p.CreatedAt,
- UpdatedAt: p.UpdatedAt,
- ExpiresAt: p.ExpiresAt,
- FallbackMode: p.FallbackMode,
- BackupProxyID: p.BackupProxyID,
- ExpiryWarnDays: p.ExpiryWarnDays,
- }
- }
- func ProxyWithAccountCountFromService(p *service.ProxyWithAccountCount) *ProxyWithAccountCount {
- if p == nil {
- return nil
- }
- return &ProxyWithAccountCount{
- Proxy: *ProxyFromService(&p.Proxy),
- AccountCount: p.AccountCount,
- LatencyMs: p.LatencyMs,
- LatencyStatus: p.LatencyStatus,
- LatencyMessage: p.LatencyMessage,
- IPAddress: p.IPAddress,
- Country: p.Country,
- CountryCode: p.CountryCode,
- Region: p.Region,
- City: p.City,
- QualityStatus: p.QualityStatus,
- QualityScore: p.QualityScore,
- QualityGrade: p.QualityGrade,
- QualitySummary: p.QualitySummary,
- QualityChecked: p.QualityChecked,
- }
- }
- // ProxyFromServiceAdmin converts a service Proxy to AdminProxy DTO for admin users.
- // It includes the password field - user-facing endpoints must not use this.
- func ProxyFromServiceAdmin(p *service.Proxy) *AdminProxy {
- if p == nil {
- return nil
- }
- base := ProxyFromService(p)
- if base == nil {
- return nil
- }
- return &AdminProxy{
- Proxy: *base,
- Password: p.Password,
- }
- }
- // ProxyWithAccountCountFromServiceAdmin converts a service ProxyWithAccountCount to AdminProxyWithAccountCount DTO.
- // It includes the password field - user-facing endpoints must not use this.
- func ProxyWithAccountCountFromServiceAdmin(p *service.ProxyWithAccountCount) *AdminProxyWithAccountCount {
- if p == nil {
- return nil
- }
- admin := ProxyFromServiceAdmin(&p.Proxy)
- if admin == nil {
- return nil
- }
- return &AdminProxyWithAccountCount{
- AdminProxy: *admin,
- AccountCount: p.AccountCount,
- LatencyMs: p.LatencyMs,
- LatencyStatus: p.LatencyStatus,
- LatencyMessage: p.LatencyMessage,
- IPAddress: p.IPAddress,
- Country: p.Country,
- CountryCode: p.CountryCode,
- Region: p.Region,
- City: p.City,
- QualityStatus: p.QualityStatus,
- QualityScore: p.QualityScore,
- QualityGrade: p.QualityGrade,
- QualitySummary: p.QualitySummary,
- QualityChecked: p.QualityChecked,
- }
- }
- func ProxyAccountSummaryFromService(a *service.ProxyAccountSummary) *ProxyAccountSummary {
- if a == nil {
- return nil
- }
- return &ProxyAccountSummary{
- ID: a.ID,
- Name: a.Name,
- Platform: a.Platform,
- Type: a.Type,
- Notes: a.Notes,
- }
- }
- func RedeemCodeFromService(rc *service.RedeemCode) *RedeemCode {
- if rc == nil {
- return nil
- }
- out := redeemCodeFromServiceBase(rc)
- return &out
- }
- // RedeemCodeFromServiceAdmin converts a service RedeemCode to DTO for admin users.
- // It includes notes - user-facing endpoints must not use this.
- func RedeemCodeFromServiceAdmin(rc *service.RedeemCode) *AdminRedeemCode {
- if rc == nil {
- return nil
- }
- return &AdminRedeemCode{
- RedeemCode: redeemCodeFromServiceBase(rc),
- Notes: rc.Notes,
- }
- }
- func redeemCodeFromServiceBase(rc *service.RedeemCode) RedeemCode {
- out := RedeemCode{
- ID: rc.ID,
- Code: rc.Code,
- Type: rc.Type,
- Value: rc.Value,
- Status: rc.Status,
- UsedBy: rc.UsedBy,
- UsedAt: rc.UsedAt,
- CreatedAt: rc.CreatedAt,
- ExpiresAt: rc.ExpiresAt,
- GroupID: rc.GroupID,
- ValidityDays: rc.ValidityDays,
- User: UserFromServiceShallow(rc.User),
- Group: GroupFromServiceShallow(rc.Group),
- }
- if rc.IsExpired() {
- out.Status = service.StatusExpired
- }
- // For admin_balance/admin_concurrency types, include notes so users can see
- // why they were charged or credited by admin
- if (rc.Type == "admin_balance" || rc.Type == "admin_concurrency") && rc.Notes != "" {
- out.Notes = &rc.Notes
- }
- return out
- }
- // AccountSummaryFromService returns a minimal AccountSummary for usage log display.
- // Only includes ID and Name - no sensitive fields like Credentials, Proxy, etc.
- func AccountSummaryFromService(a *service.Account) *AccountSummary {
- if a == nil {
- return nil
- }
- return &AccountSummary{
- ID: a.ID,
- Name: a.Name,
- }
- }
- func usageLogFromServiceUser(l *service.UsageLog) UsageLog {
- // 普通用户 DTO:严禁包含管理员字段(例如 account_rate_multiplier、account、upstream_model)。
- requestType := l.EffectiveRequestType()
- stream, openAIWSMode := service.ApplyLegacyRequestFields(requestType, l.Stream, l.OpenAIWSMode)
- requestedModel := l.RequestedModel
- if requestedModel == "" {
- requestedModel = l.Model
- }
- return UsageLog{
- ID: l.ID,
- UserID: l.UserID,
- APIKeyID: l.APIKeyID,
- AccountID: l.AccountID,
- RequestID: l.RequestID,
- Model: requestedModel,
- ServiceTier: l.ServiceTier,
- ReasoningEffort: userFacingReasoningEffort(l),
- InboundEndpoint: l.InboundEndpoint,
- GroupID: l.GroupID,
- SubscriptionID: l.SubscriptionID,
- InputTokens: l.InputTokens,
- OutputTokens: l.OutputTokens,
- CacheCreationTokens: l.CacheCreationTokens,
- CacheReadTokens: l.CacheReadTokens,
- CacheCreation5mTokens: l.CacheCreation5mTokens,
- CacheCreation1hTokens: l.CacheCreation1hTokens,
- InputCost: l.InputCost,
- OutputCost: l.OutputCost,
- CacheCreationCost: l.CacheCreationCost,
- CacheReadCost: l.CacheReadCost,
- TotalCost: l.TotalCost,
- ActualCost: l.ActualCost,
- RateMultiplier: l.RateMultiplier,
- LongContextBillingApplied: l.LongContextBillingApplied,
- BillingType: l.BillingType,
- RequestType: requestType.String(),
- Stream: stream,
- OpenAIWSMode: openAIWSMode,
- NativeCompactionV2: l.NativeCompactionV2,
- DurationMs: l.DurationMs,
- FirstTokenMs: l.FirstTokenMs,
- ImageCount: l.ImageCount,
- ImageSize: l.ImageSize,
- ImageInputSize: l.ImageInputSize,
- ImageOutputSize: l.ImageOutputSize,
- ImageInputTokens: l.ImageInputTokens,
- ImageInputCost: l.ImageInputCost,
- ImageOutputTokens: l.ImageOutputTokens,
- ImageOutputCost: l.ImageOutputCost,
- ImageSizeSource: l.ImageSizeSource,
- ImageSizeBreakdown: l.ImageSizeBreakdown,
- MediaType: l.MediaType,
- UserAgent: l.UserAgent,
- IPAddress: l.IPAddress,
- SessionID: l.SessionID,
- CacheTTLOverridden: l.CacheTTLOverridden,
- BillingMode: l.BillingMode,
- CreatedAt: l.CreatedAt,
- User: UserFromServiceShallow(l.User),
- APIKey: APIKeyFromService(l.APIKey),
- Group: GroupFromServiceShallow(l.Group),
- Subscription: UserSubscriptionFromService(l.Subscription),
- }
- }
- // UsageLogFromService converts a service UsageLog to DTO for regular users.
- // It excludes admin-only account/upstream internals while keeping user billing and request metadata.
- func UsageLogFromService(l *service.UsageLog) *UsageLog {
- if l == nil {
- return nil
- }
- u := usageLogFromServiceUser(l)
- return &u
- }
- // UsageLogFromServiceAdmin converts a service UsageLog to DTO for admin users.
- // It includes minimal Account info (ID, Name only) and IP address.
- func UsageLogFromServiceAdmin(l *service.UsageLog) *AdminUsageLog {
- if l == nil {
- return nil
- }
- usageLog := usageLogFromServiceUser(l)
- usageLog.UpstreamEndpoint = l.UpstreamEndpoint
- return &AdminUsageLog{
- UsageLog: usageLog,
- UpstreamModel: l.UpstreamModel,
- UpstreamReasoningEffort: adminUpstreamReasoningEffort(l),
- UpstreamResponseModel: l.UpstreamResponseModel,
- UpstreamModelMismatch: l.UpstreamModelMismatch,
- ChannelID: l.ChannelID,
- ModelMappingChain: l.ModelMappingChain,
- UpstreamRequestID: l.UpstreamRequestID,
- BillingTier: l.BillingTier,
- AccountRateMultiplier: l.AccountRateMultiplier,
- AccountStatsCost: l.AccountStatsCost,
- IPAddress: l.IPAddress,
- Account: AccountSummaryFromService(l.Account),
- }
- }
- func userFacingReasoningEffort(l *service.UsageLog) *string {
- if l == nil {
- return nil
- }
- if requested := strings.TrimSpace(derefString(l.RequestedReasoningEffort)); requested != "" {
- return &requested
- }
- return l.ReasoningEffort
- }
- func adminUpstreamReasoningEffort(l *service.UsageLog) *string {
- if l == nil {
- return nil
- }
- forwarded := strings.TrimSpace(derefString(l.ReasoningEffort))
- if forwarded == "" {
- return nil
- }
- requested := userFacingReasoningEffort(l)
- if requested != nil && service.NormalizeMaxReasoningEffort(*requested) == service.NormalizeMaxReasoningEffort(forwarded) {
- return nil
- }
- return &forwarded
- }
- func derefString(value *string) string {
- if value == nil {
- return ""
- }
- return *value
- }
- func UsageCleanupTaskFromService(task *service.UsageCleanupTask) *UsageCleanupTask {
- if task == nil {
- return nil
- }
- return &UsageCleanupTask{
- ID: task.ID,
- Status: task.Status,
- Filters: UsageCleanupFilters{
- StartTime: task.Filters.StartTime,
- EndTime: task.Filters.EndTime,
- UserID: task.Filters.UserID,
- APIKeyID: task.Filters.APIKeyID,
- AccountID: task.Filters.AccountID,
- GroupID: task.Filters.GroupID,
- Model: task.Filters.Model,
- RequestType: requestTypeStringPtr(task.Filters.RequestType),
- Stream: task.Filters.Stream,
- BillingType: task.Filters.BillingType,
- },
- CreatedBy: task.CreatedBy,
- DeletedRows: task.DeletedRows,
- ErrorMessage: task.ErrorMsg,
- CanceledBy: task.CanceledBy,
- CanceledAt: task.CanceledAt,
- StartedAt: task.StartedAt,
- FinishedAt: task.FinishedAt,
- CreatedAt: task.CreatedAt,
- UpdatedAt: task.UpdatedAt,
- }
- }
- func requestTypeStringPtr(requestType *int16) *string {
- if requestType == nil {
- return nil
- }
- value := service.RequestTypeFromInt16(*requestType).String()
- return &value
- }
- func SettingFromService(s *service.Setting) *Setting {
- if s == nil {
- return nil
- }
- return &Setting{
- ID: s.ID,
- Key: s.Key,
- Value: s.Value,
- UpdatedAt: s.UpdatedAt,
- }
- }
- func UserSubscriptionFromService(sub *service.UserSubscription) *UserSubscription {
- if sub == nil {
- return nil
- }
- out := userSubscriptionFromServiceBase(sub)
- return &out
- }
- // UserSubscriptionFromServiceAdmin converts a service UserSubscription to DTO for admin users.
- // It includes assignment metadata and notes.
- func UserSubscriptionFromServiceAdmin(sub *service.UserSubscription) *AdminUserSubscription {
- if sub == nil {
- return nil
- }
- return &AdminUserSubscription{
- UserSubscription: userSubscriptionFromServiceBase(sub),
- AssignedBy: sub.AssignedBy,
- AssignedAt: sub.AssignedAt,
- Notes: sub.Notes,
- AssignedByUser: UserFromServiceShallow(sub.AssignedByUser),
- }
- }
- func userSubscriptionFromServiceBase(sub *service.UserSubscription) UserSubscription {
- return UserSubscription{
- ID: sub.ID,
- UserID: sub.UserID,
- GroupID: sub.GroupID,
- StartsAt: sub.StartsAt,
- ExpiresAt: sub.ExpiresAt,
- Status: sub.Status,
- DailyWindowStart: sub.DailyWindowStart,
- WeeklyWindowStart: sub.WeeklyWindowStart,
- MonthlyWindowStart: sub.MonthlyWindowStart,
- DailyUsageUSD: sub.DailyUsageUSD,
- WeeklyUsageUSD: sub.WeeklyUsageUSD,
- MonthlyUsageUSD: sub.MonthlyUsageUSD,
- CreatedAt: sub.CreatedAt,
- UpdatedAt: sub.UpdatedAt,
- RevokedAt: sub.DeletedAt,
- User: UserFromServiceShallow(sub.User),
- Group: GroupFromServiceShallow(sub.Group),
- }
- }
- func BulkAssignResultFromService(r *service.BulkAssignResult) *BulkAssignResult {
- if r == nil {
- return nil
- }
- subs := make([]AdminUserSubscription, 0, len(r.Subscriptions))
- for i := range r.Subscriptions {
- subs = append(subs, *UserSubscriptionFromServiceAdmin(&r.Subscriptions[i]))
- }
- statuses := make(map[string]string, len(r.Statuses))
- for userID, status := range r.Statuses {
- statuses[strconv.FormatInt(userID, 10)] = status
- }
- return &BulkAssignResult{
- SuccessCount: r.SuccessCount,
- CreatedCount: r.CreatedCount,
- ReusedCount: r.ReusedCount,
- FailedCount: r.FailedCount,
- Subscriptions: subs,
- Errors: r.Errors,
- Statuses: statuses,
- }
- }
- func PromoCodeFromService(pc *service.PromoCode) *PromoCode {
- if pc == nil {
- return nil
- }
- return &PromoCode{
- ID: pc.ID,
- Code: pc.Code,
- BonusAmount: pc.BonusAmount,
- MaxUses: pc.MaxUses,
- UsedCount: pc.UsedCount,
- Status: pc.Status,
- ExpiresAt: pc.ExpiresAt,
- Notes: pc.Notes,
- CreatedAt: pc.CreatedAt,
- UpdatedAt: pc.UpdatedAt,
- }
- }
- func PromoCodeUsageFromService(u *service.PromoCodeUsage) *PromoCodeUsage {
- if u == nil {
- return nil
- }
- return &PromoCodeUsage{
- ID: u.ID,
- PromoCodeID: u.PromoCodeID,
- UserID: u.UserID,
- BonusAmount: u.BonusAmount,
- UsedAt: u.UsedAt,
- User: UserFromServiceShallow(u.User),
- }
- }
- // SecurityPolicyKeywordFromService converts a service keyword to admin DTO.
- func SecurityPolicyKeywordFromService(w *service.SecurityPolicyKeyword) *SecurityPolicyKeyword {
- if w == nil {
- return nil
- }
- return &SecurityPolicyKeyword{
- ID: w.ID,
- GroupID: w.GroupID,
- Keyword: w.Keyword,
- Category: w.Category,
- Enabled: w.Enabled,
- CreatedAt: w.CreatedAt,
- UpdatedAt: w.UpdatedAt,
- }
- }
- // SecurityPolicyKeywordsFromService converts a service keyword list to admin DTOs.
- func SecurityPolicyKeywordsFromService(words []service.SecurityPolicyKeyword) []SecurityPolicyKeyword {
- out := make([]SecurityPolicyKeyword, 0, len(words))
- for i := range words {
- if dto := SecurityPolicyKeywordFromService(&words[i]); dto != nil {
- out = append(out, *dto)
- }
- }
- return out
- }
- // SecurityPolicyKeywordSeedFromService converts a builtin seed to admin DTO.
- func SecurityPolicyKeywordSeedFromService(seed service.SecurityPolicyKeywordSeed) SecurityPolicyKeywordSeed {
- return SecurityPolicyKeywordSeed{Keyword: seed.Keyword, Category: seed.Category}
- }
- // ProxyWithAccountCountFromServiceAdmin converts a service ProxyWithAccountCount to AdminProxyWithAccountCount DTO.
- // It includes the password field - user-facing endpoints must not use this.
|