141 lines
4.4 KiB
Go
141 lines
4.4 KiB
Go
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
|
||
}
|