Files
ttstd-infinite-canvas/backend/internal/app/task_billing.go

141 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"errors"
"fmt"
"strings"
"yingce/backend/internal/model"
)
// taskBillingCoordinator 只负责任务生命周期中的计费状态迁移和核对策略。
//
// 兑换码、充值和管理员人工核对仍属于 finance service;这里收敛的是任务
// 从排队、上游调用到终态这一条链路,避免 worker、provider 和终态协调器
// 各自决定如何退款或进入待核对。
type taskBillingCoordinator struct {
repo taskBillingRepository
}
type taskBillingRepository interface {
MarkBillingRunning(orderID string) error
SettleBillingOrder(orderID string, providerRequestID string) error
RestoreRefundedBillingOrder(orderID string, providerRequestID string) error
RefundBillingOrder(orderID string, errorText string) error
MarkBillingUncertain(orderID string, errorText string) error
BillingOrder(orderID string) (*model.BillingOrder, error)
TaskHasSuccessfulBillableCall(taskID string) (bool, error)
}
var (
// errTaskBillingReview 表示订单状态已经不能由重试流程安全推断,必须先完成费用核对。
errTaskBillingReview = errors.New("task billing order requires review")
)
func newTaskBillingCoordinator(repo taskBillingRepository) *taskBillingCoordinator {
return &taskBillingCoordinator{repo: repo}
}
func (c *taskBillingCoordinator) MarkBillingRunning(orderID string) error {
if orderID == "" {
return nil
}
return c.repo.MarkBillingRunning(orderID)
}
func (c *taskBillingCoordinator) SettleBilling(orderID string, providerRequestID string) error {
if orderID == "" {
return nil
}
return c.repo.SettleBillingOrder(orderID, providerRequestID)
}
func (c *taskBillingCoordinator) BillingOrder(orderID string) (*model.BillingOrder, error) {
if orderID == "" {
return nil, nil
}
return c.repo.BillingOrder(orderID)
}
func (c *taskBillingCoordinator) SettleBillingWithAudioDuration(orderID string, providerRequestID string, durationMs int64) error {
if orderID == "" {
return nil
}
if repo, ok := c.repo.(interface {
SettleBillingOrderWithAudioDuration(string, string, int64) error
}); ok {
return repo.SettleBillingOrderWithAudioDuration(orderID, providerRequestID, durationMs)
}
return c.repo.SettleBillingOrder(orderID, providerRequestID)
}
func (c *taskBillingCoordinator) RestoreRefundedBilling(orderID string, providerRequestID string) error {
if orderID == "" {
return nil
}
return c.repo.RestoreRefundedBillingOrder(orderID, providerRequestID)
}
func (c *taskBillingCoordinator) RefundBilling(orderID string, errorText string) error {
if orderID == "" {
return nil
}
return c.repo.RefundBillingOrder(orderID, truncateRunes(errorText, 1000))
}
func (c *taskBillingCoordinator) MarkBillingUncertain(orderID string, errorText string) error {
if orderID == "" {
return nil
}
return c.repo.MarkBillingUncertain(orderID, truncateRunes(errorText, 1000))
}
// CheckRetryEligibility 收敛失败/取消任务重试前的计费状态校验。
//
// 计费订单读取失败不能被当作“没有待核对订单”继续放行,否则重试可能重复扣费。
// 只有明确读到订单且订单不是 uncertain 时,才允许进入后续的重新计费流程。
func (c *taskBillingCoordinator) CheckRetryEligibility(orderID string) error {
if strings.TrimSpace(orderID) == "" {
return nil
}
order, err := c.repo.BillingOrder(orderID)
if err != nil {
return fmt.Errorf("读取任务计费订单失败:%w", err)
}
if order == nil {
return fmt.Errorf("任务计费订单不存在:%s", orderID)
}
if order.Status == model.BillingStatusUncertain {
return errTaskBillingReview
}
return nil
}
func (c *taskBillingCoordinator) BillingFailureRequiresReview(orderID string, taskID string, err error) bool {
if orderID == "" {
return false
}
if billingFailureUncertain(err) {
return true
}
order, orderErr := c.repo.BillingOrder(orderID)
if orderErr != nil || order == nil || order.Status == model.BillingStatusUncertain {
return true
}
hasSuccessfulCall, logErr := c.repo.TaskHasSuccessfulBillableCall(taskID)
return logErr != nil || hasSuccessfulCall
}
func billingFailureUncertain(err error) bool {
if err == nil {
return false
}
message := strings.ToLower(err.Error())
for _, marker := range []string{"524", "timeout", "超时", "deadline exceeded", "context canceled", "connection reset", "unexpected eof", "broken pipe"} {
if strings.Contains(message, marker) {
return true
}
}
return false
}