package billing import ( "context" "errors" "fmt" "math" "strings" "time" ) type Wallet struct { OrganizationID string `json:"organizationId"` BalanceFen int64 `json:"balanceFen"` TotalRechargedFen int64 `json:"totalRechargedFen"` TotalChargedFen int64 `json:"totalChargedFen"` Currency string `json:"currency,omitempty"` UpdatedAt string `json:"updatedAt"` } type LedgerEntry struct { ID string `json:"id"` OrganizationID string `json:"organizationId"` AccountID string `json:"accountId,omitempty"` JobID string `json:"jobId,omitempty"` Kind string `json:"kind"` DeltaFen int64 `json:"deltaFen"` BalanceAfterFen int64 `json:"balanceAfterFen"` Currency string `json:"currency"` IdempotencyKey string `json:"idempotencyKey"` Description string `json:"description"` Metadata map[string]any `json:"metadata"` CreatedAt string `json:"createdAt"` } type Summary struct { RechargeFen int64 `json:"rechargeFen"` ChargedFen int64 `json:"chargedFen"` RefundedFen int64 `json:"refundedFen"` NetConsumedFen int64 `json:"netConsumedFen"` } type AccountConfig struct { AccountName string `json:"accountName,omitempty"` BankName string `json:"bankName,omitempty"` AccountNumber string `json:"accountNumber,omitempty"` Contact string `json:"contact,omitempty"` } type Organization struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` ArchiveOwnerID string `json:"archiveOwnerId"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Wallet Wallet `json:"wallet"` } type Member struct { ID string `json:"id"` DisplayName string `json:"displayName"` Phone string `json:"phone"` Role string `json:"role"` OrganizationID string `json:"organizationId,omitempty"` Status string `json:"status"` } type Overview struct { Wallet Wallet `json:"wallet"` Ledger []LedgerEntry `json:"ledger"` BillingAccount AccountConfig `json:"billingAccount"` Summary Summary `json:"summary"` Personal Summary `json:"personal"` } type AdminOverview struct { BillingAccount AccountConfig `json:"billingAccount"` Organizations []Organization `json:"organizations"` Members []Member `json:"members"` Ledger []LedgerEntry `json:"ledger"` PriceRules []PriceRule `json:"priceRules"` } type QuoteCommand struct { AccountID, OrganizationID, OrganizationName, Role string Provider, Capability, ReqKey string Parameters Parameters Payload map[string]any } type PricePatch struct { MarkupMultiplier float64 DimensionKey, TierValue string } type AdjustmentCommand struct { OrganizationID, OperatorID, Direction, Note string AmountFen, DeltaFen int64 } type AdjustmentResult struct { Wallet Wallet Entry LedgerEntry } type AccountConfigStore interface { Load(context.Context) (AccountConfig, error) Save(context.Context, AccountConfig) error } // ReadService is the deliberately cohesive persistence seam required by the // Billing HTTP module. Implementations can execute overview reads concurrently. type ReadService interface { Overview(context.Context, string, string) (Overview, error) AdminOverview(context.Context) (AdminOverview, error) ListPrices(context.Context) ([]PriceRule, error) GetPrice(context.Context, string) (*PriceRule, error) } type HTTPService interface { ReadService Quote(context.Context, QuoteCommand) (*Quote, error) UpdatePrice(context.Context, string, PricePatch) (*PriceRule, error) Adjust(context.Context, AdjustmentCommand) (AdjustmentResult, error) } var ErrPriceNotFound = errors.New("billing price not found") func ValidatePricePatch(rule *PriceRule, patch PricePatch) error { if patch.MarkupMultiplier < 1 || patch.MarkupMultiplier > 1000 || math.IsNaN(patch.MarkupMultiplier) || math.IsInf(patch.MarkupMultiplier, 0) { return errors.New("上浮倍率必须在 1.00 至 1000.00 之间。") } if (patch.DimensionKey == "") != (patch.TierValue == "") { return errors.New("参数档位倍率更新必须同时提供参数维度和档位。") } if rule == nil { return ErrPriceNotFound } if patch.DimensionKey == "" && len(rule.Dimensions) > 0 { return errors.New("当前服务包含参数档位,请指定要调整的参数档位倍率。") } if patch.DimensionKey != "" { for _, dimension := range rule.Dimensions { if dimension.Key != patch.DimensionKey { continue } for _, tier := range dimension.Tiers { if strings.TrimSpace(fmt.Sprint(tier.Value)) == patch.TierValue { return nil } } return errors.New("计费参数档位不存在。") } return errors.New("计费参数维度不存在。") } return nil } func PostingResult(post WalletPosting, organizationID string) AdjustmentResult { return AdjustmentResult{ Wallet: Wallet{OrganizationID: organizationID, BalanceFen: post.BalanceFen, TotalRechargedFen: post.TotalRechargedFen, TotalChargedFen: post.TotalChargedFen, Currency: CurrencyCNY, UpdatedAt: post.UpdatedAt.Format(time.RFC3339Nano)}, Entry: LedgerEntry{ID: post.LedgerID, OrganizationID: organizationID, DeltaFen: post.DeltaFen, BalanceAfterFen: post.BalanceAfterFen, Currency: CurrencyCNY, CreatedAt: post.CreatedAt.Format(time.RFC3339Nano)}, } }