feat: complete remaining Go backend modules
This commit is contained in:
1 parent
cea2751dc5
commit
aef5a97165
145 files changed
+18376
-199
No files matched your search
@@ -0,0 +1,201 @@
|
||||
package billing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
BillingWallet(context.Context, string) (Wallet, error)
|
||||
BillingWallets(context.Context) ([]Wallet, error)
|
||||
BillingLedger(context.Context, string, string, int) ([]LedgerEntry, error)
|
||||
BillingOrganizations(context.Context) ([]Organization, error)
|
||||
BillingMembers(context.Context) ([]Member, error)
|
||||
BillingOrganizationExists(context.Context, string) (bool, error)
|
||||
ListBillingPriceRules(context.Context, bool) ([]PriceRule, error)
|
||||
GetBillingPriceRule(context.Context, string) (*PriceRule, error)
|
||||
UpdateBillingPriceRule(context.Context, string, PricePatch) (*PriceRule, error)
|
||||
PostBillingWalletEntry(context.Context, WalletPostParams) (WalletPosting, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
store Store
|
||||
newID func() string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func NewService(store Store, newID func() string) *Service {
|
||||
if newID == nil {
|
||||
newID = billingID
|
||||
}
|
||||
return &Service{store: store, newID: newID, enabled: true}
|
||||
}
|
||||
|
||||
// SetEnabled configures whether generation quotes are required. The default is
|
||||
// enabled; disabling preserves the historical optional-billing behavior and
|
||||
// does not consult or seed the price catalog.
|
||||
func (s *Service) SetEnabled(enabled bool) *Service {
|
||||
s.enabled = enabled
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Service) Overview(ctx context.Context, organizationID, accountID string) (Overview, error) {
|
||||
wallet, err := s.store.BillingWallet(ctx, organizationID)
|
||||
if err != nil {
|
||||
return Overview{}, err
|
||||
}
|
||||
ledger, err := s.store.BillingLedger(ctx, organizationID, "", 500)
|
||||
if err != nil {
|
||||
return Overview{}, err
|
||||
}
|
||||
personal, err := s.store.BillingLedger(ctx, organizationID, accountID, 500)
|
||||
if err != nil {
|
||||
return Overview{}, err
|
||||
}
|
||||
return Overview{Wallet: wallet, Ledger: ledger, Summary: Summarize(ledger), Personal: Summarize(personal)}, nil
|
||||
}
|
||||
func (s *Service) Quote(ctx context.Context, command QuoteCommand) (*Quote, error) {
|
||||
if !s.enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if seeder, ok := s.store.(PriceRuleSeeder); ok {
|
||||
if err := seeder.SeedBillingPriceRules(ctx, DefaultBillingPriceRules()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
rules, err := s.store.ListBillingPriceRules(ctx, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parameters := NormalizeBillingParameters(command.Payload, command.Parameters)
|
||||
quote, err := (Catalog{Rules: rules}).Quote(QuoteInput{Provider: command.Provider, Capability: command.Capability, ReqKey: command.ReqKey, Source: "platform", Role: command.Role, Parameters: parameters})
|
||||
if err != nil || quote == nil || command.Provider != "seedance" || command.ReqKey != "doubao-seedance-2-0-260128" {
|
||||
return quote, err
|
||||
}
|
||||
inputVideo, inputDuration := seedanceInputVideo(command.Payload)
|
||||
estimated, err := EstimateSeedanceAmountFen(SeedanceEstimateInput{
|
||||
Resolution: fmt.Sprint(parameters["resolution"]), AspectRatio: fmt.Sprint(parameters["aspectRatio"]),
|
||||
OutputDurationSeconds: quote.Quantity, InputVideo: inputVideo, InputVideoDurationSeconds: inputDuration,
|
||||
MarkupMultiplier: quote.MarkupMultiplier,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if estimated > quote.AmountFen {
|
||||
quote.AmountFen = estimated
|
||||
}
|
||||
quote.ReservedAmountFen = quote.AmountFen
|
||||
quote.SettlementStatus = "pending"
|
||||
if quote.Parameters == nil {
|
||||
quote.Parameters = Parameters{}
|
||||
}
|
||||
quote.Parameters["inputVideo"] = inputVideo
|
||||
return quote, nil
|
||||
}
|
||||
func (s *Service) AdminOverview(ctx context.Context) (AdminOverview, error) {
|
||||
organizations, err := s.store.BillingOrganizations(ctx)
|
||||
if err != nil {
|
||||
return AdminOverview{}, err
|
||||
}
|
||||
wallets, err := s.store.BillingWallets(ctx)
|
||||
if err != nil {
|
||||
return AdminOverview{}, err
|
||||
}
|
||||
members, err := s.store.BillingMembers(ctx)
|
||||
if err != nil {
|
||||
return AdminOverview{}, err
|
||||
}
|
||||
ledger, err := s.store.BillingLedger(ctx, "", "", 500)
|
||||
if err != nil {
|
||||
return AdminOverview{}, err
|
||||
}
|
||||
rules, err := s.store.ListBillingPriceRules(ctx, true)
|
||||
if err != nil {
|
||||
return AdminOverview{}, err
|
||||
}
|
||||
byOrganization := map[string]Wallet{}
|
||||
for _, wallet := range wallets {
|
||||
byOrganization[wallet.OrganizationID] = wallet
|
||||
}
|
||||
for i := range organizations {
|
||||
if wallet, ok := byOrganization[organizations[i].ID]; ok {
|
||||
organizations[i].Wallet = wallet
|
||||
} else {
|
||||
organizations[i].Wallet = Wallet{OrganizationID: organizations[i].ID, Currency: CurrencyCNY, UpdatedAt: organizations[i].UpdatedAt}
|
||||
}
|
||||
}
|
||||
return AdminOverview{Organizations: organizations, Members: members, Ledger: ledger, PriceRules: rules}, nil
|
||||
}
|
||||
func (s *Service) ListPrices(ctx context.Context) ([]PriceRule, error) {
|
||||
return s.store.ListBillingPriceRules(ctx, true)
|
||||
}
|
||||
func (s *Service) GetPrice(ctx context.Context, id string) (*PriceRule, error) {
|
||||
return s.store.GetBillingPriceRule(ctx, id)
|
||||
}
|
||||
func (s *Service) UpdatePrice(ctx context.Context, id string, patch PricePatch) (*PriceRule, error) {
|
||||
return s.store.UpdateBillingPriceRule(ctx, id, patch)
|
||||
}
|
||||
func (s *Service) Adjust(ctx context.Context, command AdjustmentCommand) (AdjustmentResult, error) {
|
||||
exists, err := s.store.BillingOrganizationExists(ctx, command.OrganizationID)
|
||||
if err != nil {
|
||||
return AdjustmentResult{}, err
|
||||
}
|
||||
if !exists {
|
||||
return AdjustmentResult{}, &StatusError{400, errors.New("组织不存在。")}
|
||||
}
|
||||
kind, description := "adjustment", "管理员扣减 · "+command.Note
|
||||
if command.Direction == "credit" {
|
||||
kind, description = "recharge", "管理员上账 · "+command.Note
|
||||
}
|
||||
post, err := s.store.PostBillingWalletEntry(ctx, WalletPostParams{LedgerID: s.newID(), OrganizationID: command.OrganizationID, Kind: kind, DeltaFen: command.DeltaFen, Currency: CurrencyCNY, IdempotencyKey: "manual-adjustment:" + s.newID(), Description: description, Metadata: map[string]any{"operation": map[bool]string{true: "admin_top_up", false: "manual_adjustment"}[command.Direction == "credit"], "direction": command.Direction, "note": command.Note, "operatorId": command.OperatorID, "amountYuan": fmt.Sprintf("%.2f", float64(command.AmountFen)/100)}})
|
||||
if err != nil {
|
||||
message := err.Error()
|
||||
if strings.Contains(message, "BILLING_INSUFFICIENT_BALANCE") {
|
||||
return AdjustmentResult{}, &StatusError{402, ErrInsufficientBalance}
|
||||
}
|
||||
if strings.Contains(message, "BILLING_IDEMPOTENCY_PAYLOAD_MISMATCH") {
|
||||
return AdjustmentResult{}, &StatusError{409, ErrIdempotencyConflict}
|
||||
}
|
||||
return AdjustmentResult{}, err
|
||||
}
|
||||
return PostingResult(post, command.OrganizationID), nil
|
||||
}
|
||||
|
||||
func Summarize(entries []LedgerEntry) Summary {
|
||||
var out Summary
|
||||
for _, entry := range entries {
|
||||
switch {
|
||||
case entry.Kind == "recharge", entry.Kind == "adjustment" && entry.DeltaFen > 0:
|
||||
if entry.DeltaFen > 0 {
|
||||
out.RechargeFen += entry.DeltaFen
|
||||
}
|
||||
case entry.Kind == "charge":
|
||||
if entry.DeltaFen < 0 {
|
||||
out.ChargedFen -= entry.DeltaFen
|
||||
}
|
||||
case entry.Kind == "refund":
|
||||
if entry.DeltaFen > 0 {
|
||||
out.RefundedFen += entry.DeltaFen
|
||||
}
|
||||
}
|
||||
}
|
||||
out.NetConsumedFen = out.ChargedFen - out.RefundedFen
|
||||
if out.NetConsumedFen < 0 {
|
||||
out.NetConsumedFen = 0
|
||||
}
|
||||
return out
|
||||
}
|
||||
func billingID() string {
|
||||
raw := make([]byte, 12)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return fmt.Sprintf("entry-%d", time.Now().UnixNano())
|
||||
}
|
||||
return "entry-" + hex.EncodeToString(raw)
|
||||
}
|
||||
|
||||
var _ HTTPService = (*Service)(nil)
|
||||
Reference in new issue
Block a user