Files
NianAIGC/backend/internal/billing/http_service.go

157 lines
5.5 KiB
Go

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)},
}
}