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,220 @@
|
||||
package orchestration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
|
||||
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs"
|
||||
)
|
||||
|
||||
type SettlementLedger interface {
|
||||
Settle(context.Context, billing.SettlementRequest) (billing.WalletPosting, error)
|
||||
}
|
||||
|
||||
type SettlementStateWriter interface {
|
||||
WriteBilling(context.Context, string, json.RawMessage) error
|
||||
}
|
||||
|
||||
type FencedSettlementStateWriter interface {
|
||||
WriteBillingFenced(context.Context, string, json.RawMessage, jobs.Status, string) error
|
||||
}
|
||||
|
||||
// SettlementProcessor adds Seedance actual-usage settlement to a jobs
|
||||
// processor. Other providers and non-successful advances pass through.
|
||||
type SettlementProcessor struct {
|
||||
inner jobs.Processor
|
||||
ledger SettlementLedger
|
||||
state SettlementStateWriter
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewSettlementProcessor(inner jobs.Processor, ledger SettlementLedger, state SettlementStateWriter, now func() time.Time) *SettlementProcessor {
|
||||
if now == nil {
|
||||
now = time.Now
|
||||
}
|
||||
return &SettlementProcessor{inner: inner, ledger: ledger, state: state, now: now}
|
||||
}
|
||||
|
||||
func (p *SettlementProcessor) Advance(ctx context.Context, job jobs.Job) (jobs.Job, error) {
|
||||
if p == nil || p.inner == nil {
|
||||
return jobs.Job{}, errors.New("advance generation job")
|
||||
}
|
||||
advanced, err := p.inner.Advance(ctx, job)
|
||||
if err != nil || advanced.Provider != "seedance" || advanced.Status != jobs.StatusSucceeded || len(advanced.Billing) == 0 {
|
||||
return advanced, err
|
||||
}
|
||||
|
||||
var charge billingState
|
||||
if json.Unmarshal(advanced.Billing, &charge) != nil {
|
||||
return jobs.Job{}, errors.New("settle seedance generation charge")
|
||||
}
|
||||
status, _ := charge.raw["settlementStatus"].(string)
|
||||
if status == "settled" || status == "estimated" {
|
||||
return advanced, nil
|
||||
}
|
||||
var use usageContext
|
||||
if len(advanced.UsageContext) != 0 && json.Unmarshal(advanced.UsageContext, &use) != nil {
|
||||
return jobs.Job{}, errors.New("settle seedance generation charge")
|
||||
}
|
||||
quotaExempt := charge.QuotaExempt || use.Source == "platform" && use.Role == "super_admin"
|
||||
chargeReady := charge.Status == "charged" || quotaExempt && charge.Status == "not_charged"
|
||||
if !chargeReady || !quotaExempt && use.OrganizationID == "" {
|
||||
return advanced, nil
|
||||
}
|
||||
|
||||
completionTokens := seedanceCompletionTokens(advanced.ResponsePayload)
|
||||
if completionTokens <= 0 {
|
||||
charge.raw["settlementStatus"] = "estimated"
|
||||
charge.raw["settlementReason"] = "provider_usage_unavailable"
|
||||
charge.raw["settledAt"] = p.now().UTC().Format(time.RFC3339Nano)
|
||||
return p.write(ctx, advanced, charge.raw)
|
||||
}
|
||||
|
||||
resolution := seedanceResolution(charge.raw)
|
||||
inputVideo := seedanceRequestHasInputVideo(advanced.RequestPayload)
|
||||
actualAmount, err := billing.CalculateSeedanceActualAmountFen(billing.SeedanceActualAmountInput{
|
||||
Resolution: resolution, InputVideo: inputVideo, CompletionTokens: completionTokens,
|
||||
MarkupMultiplier: numberOrZero(charge.raw["markupMultiplier"]),
|
||||
})
|
||||
if err != nil {
|
||||
return jobs.Job{}, errors.New("settle seedance generation charge")
|
||||
}
|
||||
delta := actualAmount - charge.AmountFen
|
||||
settledAt := p.now().UTC()
|
||||
var settlementLedgerID string
|
||||
if delta != 0 && !quotaExempt {
|
||||
if p.ledger == nil {
|
||||
return jobs.Job{}, errors.New("settle seedance generation charge")
|
||||
}
|
||||
description := capabilityLabel(advanced.Capability) + "实际用量差额退回"
|
||||
if delta > 0 {
|
||||
description = capabilityLabel(advanced.Capability) + "实际用量补扣"
|
||||
}
|
||||
reservedAmount := charge.AmountFen
|
||||
if value, ok := integer(charge.raw["reservedAmountFen"]); ok {
|
||||
reservedAmount = value
|
||||
}
|
||||
posting, postErr := p.ledger.Settle(ctx, billing.SettlementRequest{
|
||||
OrganizationID: use.OrganizationID, AccountID: use.AccountID, JobID: advanced.ID,
|
||||
DeltaFen: delta, Description: description,
|
||||
Metadata: map[string]any{
|
||||
"operation": "seedance_actual_settlement", "reservedAmountFen": reservedAmount,
|
||||
"chargedAmountFen": charge.AmountFen, "actualAmountFen": actualAmount,
|
||||
"completionTokens": completionTokens, "inputVideo": inputVideo, "resolution": resolution,
|
||||
},
|
||||
})
|
||||
if postErr != nil {
|
||||
return jobs.Job{}, errors.New("settle seedance generation charge")
|
||||
}
|
||||
settlementLedgerID = posting.LedgerID
|
||||
if !posting.CreatedAt.IsZero() {
|
||||
settledAt = posting.CreatedAt.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
charge.raw["amountFen"] = actualAmount
|
||||
charge.raw["settlementStatus"] = "settled"
|
||||
charge.raw["settledAt"] = settledAt.Format(time.RFC3339Nano)
|
||||
if settlementLedgerID != "" {
|
||||
charge.raw["settlementLedgerEntryId"] = settlementLedgerID
|
||||
} else {
|
||||
delete(charge.raw, "settlementLedgerEntryId")
|
||||
}
|
||||
charge.raw["providerUsage"] = map[string]any{
|
||||
"completionTokens": completionTokens, "resolution": resolution, "inputVideo": inputVideo,
|
||||
"tokenPriceFenPerMillion": billing.SeedanceTokenPriceFenPerMillion(resolution, inputVideo),
|
||||
}
|
||||
return p.write(ctx, advanced, charge.raw)
|
||||
}
|
||||
|
||||
func (p *SettlementProcessor) write(ctx context.Context, job jobs.Job, snapshot map[string]any) (jobs.Job, error) {
|
||||
if p.state == nil {
|
||||
return jobs.Job{}, errors.New("persist seedance generation settlement")
|
||||
}
|
||||
encoded, err := json.Marshal(snapshot)
|
||||
if err != nil {
|
||||
return jobs.Job{}, errors.New("persist seedance generation settlement")
|
||||
}
|
||||
var writeErr error
|
||||
if fenced, ok := p.state.(FencedSettlementStateWriter); ok && job.LockedBy != "" {
|
||||
writeErr = fenced.WriteBillingFenced(ctx, job.ID, encoded, job.Status, job.LockedBy)
|
||||
} else {
|
||||
writeErr = p.state.WriteBilling(ctx, job.ID, encoded)
|
||||
}
|
||||
if writeErr != nil {
|
||||
return jobs.Job{}, errors.New("persist seedance generation settlement")
|
||||
}
|
||||
job.Billing = encoded
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func seedanceCompletionTokens(payload json.RawMessage) int64 {
|
||||
var response struct {
|
||||
Usage map[string]any `json:"usage"`
|
||||
}
|
||||
if json.Unmarshal(payload, &response) != nil {
|
||||
return 0
|
||||
}
|
||||
value, _ := integer(response.Usage["completionTokens"])
|
||||
return value
|
||||
}
|
||||
|
||||
func seedanceResolution(snapshot map[string]any) string {
|
||||
parameters, _ := snapshot["parameters"].(map[string]any)
|
||||
resolution, _ := parameters["resolution"].(string)
|
||||
resolution = strings.ToLower(strings.TrimSpace(resolution))
|
||||
if resolution == "480p" || resolution == "1080p" || resolution == "4k" {
|
||||
return resolution
|
||||
}
|
||||
return "720p"
|
||||
}
|
||||
|
||||
func seedanceRequestHasInputVideo(payload json.RawMessage) bool {
|
||||
var value any
|
||||
if json.Unmarshal(payload, &value) != nil {
|
||||
return false
|
||||
}
|
||||
return hasVideoMaterial(value)
|
||||
}
|
||||
|
||||
func hasVideoMaterial(value any) bool {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
if materialType, _ := typed["type"].(string); strings.EqualFold(strings.TrimSpace(materialType), "video") {
|
||||
return true
|
||||
}
|
||||
for _, child := range typed {
|
||||
if hasVideoMaterial(child) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, child := range typed {
|
||||
if hasVideoMaterial(child) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func numberOrZero(value any) float64 {
|
||||
if number, ok := value.(float64); ok {
|
||||
return number
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func integer(value any) (int64, bool) {
|
||||
number, ok := value.(float64)
|
||||
if !ok || number <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
return int64(number), true
|
||||
}
|
||||
|
||||
var _ jobs.Processor = (*SettlementProcessor)(nil)
|
||||
Reference in new issue
Block a user