完善生图5.0的功能

This commit is contained in:
andy committed 2026-08-25 14:06:04 +08:00
1 parent 88a4268d40
commit 678d213f40
31 files changed
+3281 -107

No files matched your search

+76 -7
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"log"
"strings"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
@@ -161,8 +162,15 @@ func (c *CreationCoordinator) charge(ctx context.Context, job jobs.Job, reused b
return c.writeCharge(ctx, job, reused, charge.raw)
}
var use usageContext
if json.Unmarshal(job.UsageContext, &use) != nil || use.OrganizationID == "" || charge.AmountFen <= 0 {
return c.failCharge(ctx, job, reused, errors.New("charge generation job"), charge.raw)
if json.Unmarshal(job.UsageContext, &use) != nil || charge.AmountFen <= 0 {
cause := billing.ErrProviderUnavailable
logGenerationBillingFailure(job.ID, "validate", cause, cause)
return c.failCharge(ctx, job, reused, cause, charge.raw)
}
if use.OrganizationID == "" {
cause := &billing.StatusError{Status: 409, Err: billing.ErrOrganizationUnavailable}
logGenerationBillingFailure(job.ID, "validate", cause, cause)
return c.failCharge(ctx, job, reused, cause, charge.raw)
}
request := billing.ChargeRequest{OrganizationID: use.OrganizationID, AccountID: use.AccountID, JobID: job.ID, AmountFen: charge.AmountFen, Description: capabilityLabel(job.Capability) + " · " + job.ReqKey, Metadata: map[string]any{"quote": cloneMap(charge.raw), "accountName": use.DisplayName, "organizationName": use.OrganizationName}}
if atomic, ok := c.state.(AtomicCreationCharger); ok {
@@ -173,16 +181,22 @@ func (c *CreationCoordinator) charge(ctx context.Context, job jobs.Job, reused b
encoded, atomicErr := atomic.ChargeAndActivateCreation(ctx, request, pending)
if atomicErr != nil {
if errors.Is(atomicErr, billing.ErrCommitOutcomeUnknown) {
return jobs.Job{}, reused, errors.New("charge generation job")
publicErr := errors.New("charge generation job")
logGenerationBillingFailure(job.ID, "commit", atomicErr, publicErr)
return jobs.Job{}, reused, publicErr
}
return c.failCharge(ctx, job, reused, safeBillingError(atomicErr), charge.raw)
safeErr := safeBillingError(atomicErr)
logGenerationBillingFailure(job.ID, "charge", atomicErr, safeErr)
return c.failCharge(ctx, job, reused, safeErr, charge.raw)
}
job.Billing = encoded
return job, reused, nil
}
posting, err := c.charges.Charge(ctx, request)
if err != nil {
return c.failCharge(ctx, job, reused, safeBillingError(err), charge.raw)
safeErr := safeBillingError(err)
logGenerationBillingFailure(job.ID, "charge", err, safeErr)
return c.failCharge(ctx, job, reused, safeErr, charge.raw)
}
charge.raw["status"] = "charged"
charge.raw["ledgerEntryId"] = posting.LedgerID
@@ -205,7 +219,8 @@ func (c *CreationCoordinator) failCharge(ctx context.Context, job jobs.Job, reus
job.Status = jobs.StatusFailed
job.Error = &jobs.JobError{Message: "generation charge failed", Retryable: false}
if err := c.state.FailCreation(ctx, job); err != nil {
return jobs.Job{}, reused, errors.New("persist failed generation charge")
logGenerationBillingFailure(job.ID, "persist_failure", err, billing.ErrProviderUnavailable)
return jobs.Job{}, reused, billing.ErrProviderUnavailable
}
return job, reused, cause
}
@@ -231,10 +246,64 @@ func safeBillingError(err error) error {
if strings.Contains(message, "BILLING_IDEMPOTENCY_PAYLOAD_MISMATCH") {
return &billing.StatusError{Status: 409, Err: billing.ErrIdempotencyConflict}
}
return errors.New("charge generation job")
return billing.ErrProviderUnavailable
}
}
func logGenerationBillingFailure(jobID, phase string, rawErr, publicErr error) {
log.Printf(
"zhinian-api generation billing failed jobId=%s phase=%s status=%d code=%q errorClass=%s",
jobID,
phase,
billing.HTTPStatus(publicErr),
billingSQLState(rawErr),
generationBillingErrorClass(rawErr, publicErr),
)
}
func generationBillingErrorClass(rawErr, publicErr error) string {
switch {
case errors.Is(publicErr, billing.ErrInsufficientBalance):
return "insufficient_balance"
case errors.Is(publicErr, billing.ErrIdempotencyConflict):
return "idempotency_conflict"
case errors.Is(publicErr, billing.ErrOrganizationUnavailable):
return "organization_unavailable"
case errors.Is(rawErr, billing.ErrCommitOutcomeUnknown):
return "commit_outcome_unknown"
case errors.Is(rawErr, context.DeadlineExceeded):
return "timeout"
case errors.Is(rawErr, context.Canceled):
return "canceled"
case billingSQLState(rawErr) != "":
return "database"
case errors.Is(publicErr, billing.ErrProviderUnavailable):
return "unavailable"
default:
return "unknown"
}
}
func billingSQLState(err error) string {
type sqlStateCarrier interface{ SQLState() string }
var carrier sqlStateCarrier
if !errors.As(err, &carrier) {
return ""
}
code := strings.TrimSpace(carrier.SQLState())
if len(code) != 5 {
return ""
}
for _, char := range code {
isDigit := char >= '0' && char <= '9'
isUppercase := char >= 'A' && char <= 'Z'
if !isDigit && !isUppercase {
return ""
}
}
return code
}
func retryEngine(provider, capability string) string {
if capability == "video.generate" {
if provider == "seedance" || provider == "bailian" {