Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
eaa8116
update
CSCITech Sep 10, 2026
fae2680
fix: harden task usage producer and wrapped payloads
CSCITech Sep 10, 2026
1f69397
fix: enforce polling usage stage
CSCITech Sep 10, 2026
84e57b2
fix: reject inconsistent task token usage
CSCITech Sep 10, 2026
32dc897
fix: harden task usage polling provenance
CSCITech Sep 10, 2026
ee998dd
test: pin task plugin producer fixture
CSCITech Sep 10, 2026
d6ea95d
fix: stabilize task usage evidence recovery
CSCITech Sep 10, 2026
8f9af2d
fix: reject invalid first task usage envelope
CSCITech Sep 10, 2026
4e0bfbe
fix: quarantine invalid task usage tokens
CSCITech Sep 10, 2026
b97d8ca
feat: streamline MiniMax task settlement operations
CSCITech Sep 10, 2026
3524180
fix: streamline H3 smart ops settlement
CSCITech Sep 10, 2026
ccf1dd7
fix: harden task settlement review follow-up
CSCITech Sep 10, 2026
78bde4d
fix: streamline H3 SmartOps settlement review
CSCITech Sep 11, 2026
96da958
refactor: share task settlement eligibility checks
CSCITech Sep 11, 2026
651526c
fix: align smart ops settlement selection states
CSCITech Sep 11, 2026
ad6a4e3
fix: expose exact H3 settlement alongside zero shortcut
CSCITech Sep 11, 2026
6a31b57
fix: clarify exact task settlement entry
CSCITech Sep 11, 2026
ddfdf85
fix: block batch review during manual settlement
CSCITech Sep 11, 2026
68df5dc
test: cover non-zero manual task settlement
CSCITech Sep 11, 2026
59b3d6f
feat: support exact batch task settlement quotas
CSCITech Sep 11, 2026
58e420e
fix: close batch settlement review findings
CSCITech Sep 11, 2026
36ecdf6
fix: harden manual task settlement review
CSCITech Sep 11, 2026
61fea49
fix: harden task usage and settlement diagnostics
CSCITech Sep 11, 2026
e0f37ef
fix: close latest task settlement review findings
CSCITech Sep 11, 2026
8b9869b
fix: keep exact task quota settlement discoverable
CSCITech Sep 11, 2026
1948212
test: strengthen task billing contract coverage
CSCITech Sep 11, 2026
ee19395
refactor: isolate manual task batch settlement hook
CSCITech Sep 11, 2026
e3f575f
fix: clarify manual task quota settlement
CSCITech Sep 11, 2026
daf6893
test: cover batch settlement audit rendering
CSCITech Sep 11, 2026
4cf18a1
fix: complete manual task settlement entry flow
CSCITech Sep 11, 2026
7530e16
fix: harden wrapped task result and settlement confirmation
CSCITech Sep 11, 2026
7ad38d2
fix: clarify batch settlement actions
CSCITech Sep 11, 2026
969a6e5
fix: preserve invalid H3 task results
CSCITech Sep 11, 2026
f89146b
test: align task settlement review fixtures
CSCITech Sep 11, 2026
4c3dd3f
fix: use stable settlement failure codes
CSCITech Sep 11, 2026
87c5ddc
fix: serialize sqlite settlement writes
CSCITech Sep 12, 2026
7c80f03
fix: clarify manual task quota settlement
CSCITech Sep 12, 2026
564076f
fix: clarify exact task quota entry
CSCITech Sep 12, 2026
2c3958a
fix: translate French settlement message
CSCITech Sep 12, 2026
6974f90
fix: preserve exact task settlement paths
CSCITech Sep 12, 2026
442ea92
fix: preserve task settlement recovery and quota entry
CSCITech Sep 12, 2026
ea6dcf3
fix: preserve manual task settlement results
CSCITech Sep 12, 2026
194dd4b
fix: harden task usage and settlement recovery
CSCITech Sep 12, 2026
2543390
fix: default manual task quota to zero
CSCITech Sep 12, 2026
19dbecb
fix: close latest smart ops review findings
CSCITech Sep 12, 2026
67ee472
fix: preserve locale plural variants during i18n sync
CSCITech Sep 12, 2026
369e634
fix: restore batch manual quota input
CSCITech Sep 12, 2026
bc86854
fix: close latest task settlement review findings
CSCITech Sep 12, 2026
70b913c
fix: harden manual task settlement review paths
CSCITech Sep 13, 2026
4a1fa60
fix: require explicit task settlement quotas
CSCITech Sep 13, 2026
edbd16f
test: distinguish explicit zero task usage
CSCITech Sep 13, 2026
ad20d13
fix: serialize sqlite task settlement intents
CSCITech Sep 13, 2026
c643c41
fix: avoid duplicate batch settlement validation
CSCITech Sep 13, 2026
4804903
test: verify zero quota count interpolation
CSCITech Sep 13, 2026
4e0cb45
fix: improve Japanese settlement failure wording
CSCITech Sep 13, 2026
1518687
fix: clarify French quota units
CSCITech Sep 13, 2026
6f11ca8
fix: support standard batch quota input events
CSCITech Sep 13, 2026
08d9456
fix: tighten task usage and settlement response contracts
CSCITech Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions controller/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ var auditContentTemplates = map[string]string{
"billing.reconciliation_review": "Reviewed billing settlement ${settlement_id} (block user: ${block_user})",
"billing.reconciliation_review_batch": "Reviewed and closed ${count} billing reconciliation alerts",
"billing.manual_task_settlement_complete": "Completed manual task billing settlement ${settlement_id} with exact quota ${actual_quota}",
"billing.manual_task_settlement_batch_complete": "Completed ${completed_count} manual task billing settlements (${failed_count} failed)",
"billing.manual_task_settlement_batch_zero": "Completed ${completed_count} manual task settlements with zero quota (${failed_count} failed)",

"channel.create": "Created channel ${name} (type ${type}, count ${count})",
"channel.update": "Updated channel ${name} (ID: ${id})",
Expand Down
81 changes: 81 additions & 0 deletions controller/smart_ops_alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ type manualTaskBillingCompletionRequest struct {
Note string `json:"note"`
}

type manualTaskBillingBatchCompletionRequest struct {
Items []manualTaskBillingBatchCompletionItem `json:"items"`
}

type manualTaskBillingBatchCompletionItem struct {
ID int64 `json:"id"`
Revision int64 `json:"revision"`
ActualQuota *int64 `json:"actual_quota"`
}

// ReviewBillingSettlements closes a bounded set of current reconciliation
// alerts after the administrator acknowledges them.
func ReviewBillingSettlements(c *gin.Context) {
Expand Down Expand Up @@ -236,6 +246,77 @@ func CompleteManualTaskBillingSettlement(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": result})
}

// CompleteManualTaskBillingSettlementsZero performs an explicit root-admin
// zero-quota settlement for selected MiniMax-H3 task alerts. Ordinary
// reconciliation alerts are never accepted by this endpoint.
func CompleteManualTaskBillingSettlementsZero(c *gin.Context) {
var request billingSettlementBatchReviewRequest
if err := common.DecodeJson(c.Request.Body, &request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid manual task billing batch completion request"})
return
}
result, err := service.CompleteManualTaskBillingSettlementsZero(request.Items, c.GetInt("id"))
if err != nil {
status := http.StatusInternalServerError
message := "failed to complete manual task billing settlements"
if errors.Is(err, service.ErrInvalidBillingSettlementReconciliationReview) {
status = http.StatusBadRequest
message = err.Error()
} else {
common.SysError(fmt.Sprintf("failed to complete manual task billing settlements: %v", err))
}
c.JSON(status, gin.H{"success": false, "message": message})
return
}
recordManageAudit(c, "billing.manual_task_settlement_batch_zero", map[string]interface{}{
"completed_count": result.CompletedCount,
"failed_count": result.FailedCount,
"settlement_ids": result.SettlementIDs,
"failed": result.Failed,
})
c.JSON(http.StatusOK, gin.H{"success": true, "data": result})
}

// CompleteManualTaskBillingSettlements applies administrator-supplied exact
// final quotas to a bounded set of task-finalization alerts. Each item carries
// its own amount so reservations from different tasks cannot be conflated.
func CompleteManualTaskBillingSettlements(c *gin.Context) {
var request manualTaskBillingBatchCompletionRequest
if err := common.DecodeJson(c.Request.Body, &request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "invalid manual task billing batch completion request"})
return
}
targets := make([]service.ManualTaskBillingCompletionTarget, len(request.Items))
for index, item := range request.Items {
targets[index] = service.ManualTaskBillingCompletionTarget{
ID: item.ID,
Revision: item.Revision,
ActualQuota: item.ActualQuota,
}
}
result, err := service.CompleteManualTaskBillingSettlements(targets, c.GetInt("id"))
if err != nil {
status := http.StatusInternalServerError
message := "failed to complete manual task billing settlements"
if errors.Is(err, service.ErrInvalidBillingSettlementReconciliationReview) {
status = http.StatusBadRequest
message = err.Error()
}
if status == http.StatusInternalServerError {
common.SysError(fmt.Sprintf("failed to complete manual task billing settlements: %v", err))
}
c.JSON(status, gin.H{"success": false, "message": message})
return
}
recordManageAudit(c, "billing.manual_task_settlement_batch_complete", map[string]interface{}{
"completed_count": result.CompletedCount,
"failed_count": result.FailedCount,
"settlement_ids": result.SettlementIDs,
"failed": result.Failed,
})
c.JSON(http.StatusOK, gin.H{"success": true, "data": result})
}

// GetBillingSettlementReconciliation exposes bounded, read-only evidence for
// open billing reconciliation records. It does not retry or mutate records.
func GetBillingSettlementReconciliation(c *gin.Context) {
Expand Down
119 changes: 119 additions & 0 deletions controller/smart_ops_alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/MAX-API-Next/MAX-API/common"
"github.com/MAX-API-Next/MAX-API/constant"
"github.com/MAX-API-Next/MAX-API/model"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
Expand Down Expand Up @@ -39,6 +40,124 @@ func TestBillingSettlementMutationRequestsPreserveExplicitFalse(t *testing.T) {
assert.Nil(t, omittedCompletion.ActualQuota)
}

func TestCompleteManualTaskBillingSettlementsZeroDecodesEmptyBatch(t *testing.T) {
var request billingSettlementBatchReviewRequest
require.NoError(t, common.Unmarshal([]byte(`{"items":[]}`), &request))
assert.Empty(t, request.Items)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func TestCompleteManualTaskBillingSettlementsDecodesPerItemExplicitZero(t *testing.T) {
var request manualTaskBillingBatchCompletionRequest
require.NoError(t, common.Unmarshal([]byte(`{"items":[{"id":9,"revision":2,"actual_quota":0},{"id":10,"revision":3,"actual_quota":40}]}`), &request))
require.Len(t, request.Items, 2)
require.NotNil(t, request.Items[0].ActualQuota)
assert.Zero(t, *request.Items[0].ActualQuota)
require.NotNil(t, request.Items[1].ActualQuota)
assert.EqualValues(t, 40, *request.Items[1].ActualQuota)

var omitted manualTaskBillingBatchCompletionRequest
require.NoError(t, common.Unmarshal([]byte(`{"items":[{"id":9,"revision":2}]}`), &omitted))
require.Len(t, omitted.Items, 1)
assert.Nil(t, omitted.Items[0].ActualQuota)
}

func TestCompleteManualTaskBillingSettlementsZeroBlocksBatchWithIneligibleTarget(t *testing.T) {
gin.SetMode(gin.TestMode)
oldDB := model.DB
oldLogDB := model.LOG_DB
oldRedisEnabled := common.RedisEnabled
oldMemoryCacheEnabled := common.MemoryCacheEnabled
oldLogConsumeEnabled := common.LogConsumeEnabled
common.RedisEnabled = false
common.MemoryCacheEnabled = false
common.LogConsumeEnabled = false

dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(
&model.User{},
&model.Token{},
&model.Task{},
&model.BillingSettlement{},
&model.CacheInvalidationTask{},
&model.Log{},
))
model.DB = db
model.LOG_DB = db
t.Cleanup(func() {
if sqlDB, dbErr := db.DB(); dbErr == nil {
_ = sqlDB.Close()
}
model.DB = oldDB
model.LOG_DB = oldLogDB
common.RedisEnabled = oldRedisEnabled
common.MemoryCacheEnabled = oldMemoryCacheEnabled
common.LogConsumeEnabled = oldLogConsumeEnabled
})

admin := model.User{Id: 7041, Username: "smart-ops-zero-root", AffCode: "smart-ops-zero-root-aff", Role: common.RoleRootUser, Status: common.UserStatusEnabled, Group: "default"}
owner := model.User{Id: 7042, Username: "smart-ops-zero-owner", AffCode: "smart-ops-zero-owner-aff", Role: common.RoleCommonUser, Status: common.UserStatusEnabled, Group: "default", Quota: 900}
token := model.Token{Id: 7043, UserId: owner.Id, Key: "smart-ops-zero-token", Name: "smart-ops-zero-token", Status: common.TokenStatusEnabled, RemainQuota: 100, UsedQuota: 100, Group: "default"}
require.NoError(t, db.Create(&admin).Error)
require.NoError(t, db.Create(&owner).Error)
require.NoError(t, db.Create(&token).Error)

makeManualTask := func(taskID string, modelName string) (model.Task, model.BillingSettlement) {
task := model.Task{
TaskID: taskID, UserId: owner.Id, Group: "default", ChannelId: 35, Quota: 100,
Status: model.TaskStatusInProgress, CreatedAt: 1, UpdatedAt: 1,
Properties: model.Properties{OriginModelName: modelName, UpstreamModelName: modelName},
PrivateData: model.TaskPrivateData{
TokenId: token.Id, BillingSource: "wallet",
BillingContext: &model.TaskBillingContext{OriginModelName: modelName},
},
}
task.SetData(map[string]any{"provider_status": "complete"})
require.NoError(t, db.Create(&task).Error)
settlement := model.BillingSettlement{
OperationKey: model.BillingTaskFinalizeOperationKey(task.ID), Source: model.BillingSettlementSourceWallet,
UserID: owner.Id, TokenID: token.Id, TaskID: task.ID, TaskQuota: 100, TaskQuotaTarget: 100,
Status: model.BillingSettlementStatusManual, LastError: "provider usage requires an exact quota",
CreatedAt: 1, UpdatedAt: 1, Revision: 1,
}
require.NoError(t, db.Create(&settlement).Error)
return task, settlement
}

_, h3Settlement := makeManualTask("smart-ops-zero-h3", constant.TaskModelMiniMaxH3)
_, ordinarySettlement := makeManualTask("smart-ops-zero-ordinary", "ordinary-task-model")
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
ctx.Request = httptest.NewRequest(
http.MethodPost,
"/api/smart-ops/billing-settlements/complete-tasks-zero",
bytes.NewBufferString(fmt.Sprintf(`{"items":[{"id":%d,"revision":1},{"id":%d,"revision":1}]}`, h3Settlement.ID, ordinarySettlement.ID)),
)
ctx.Set("id", admin.Id)
ctx.Set("username", admin.Username)
ctx.Set("role", admin.Role)

CompleteManualTaskBillingSettlementsZero(ctx)

require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
assert.Contains(t, recorder.Body.String(), `"completed_count":0`)
assert.Contains(t, recorder.Body.String(), `"failed_count":2`)
assert.Contains(t, recorder.Body.String(), fmt.Sprintf(`"settlement_id":%d,"code":"not_attempted"`, h3Settlement.ID))
assert.Contains(t, recorder.Body.String(), fmt.Sprintf(`"settlement_id":%d`, ordinarySettlement.ID))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert.Contains(t, recorder.Body.String(), `"code":"minimax_h3_required"`)
var settled model.BillingSettlement
require.NoError(t, db.First(&settled, h3Settlement.ID).Error)
assert.Equal(t, model.BillingSettlementStatusManual, settled.Status)
assert.Zero(t, settled.ReconciliationReviewedBy)
var rejected model.BillingSettlement
require.NoError(t, db.First(&rejected, ordinarySettlement.ID).Error)
assert.Equal(t, model.BillingSettlementStatusManual, rejected.Status)
var storedOwner model.User
require.NoError(t, db.First(&storedOwner, owner.Id).Error)
assert.EqualValues(t, 900, storedOwner.Quota)
}

func TestCompleteManualTaskBillingSettlementAuditsExactCompletion(t *testing.T) {
gin.SetMode(gin.TestMode)
oldDB := model.DB
Expand Down
44 changes: 44 additions & 0 deletions model/billing_settlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ var (
ErrSubscriptionRefundClamped = errors.New("subscription refund was clamped")
ErrSubscriptionSettlementUnbound = errors.New("subscription settlement is not bound to its pre-consume request")
ErrSubscriptionSettlementPeriodChanged = errors.New("subscription settlement crossed a quota reset period")

// SQLite permits only one writer and does not support the row-level lock
// clause used by the other supported databases. Serializing the complete
// settlement lifecycle prevents two local connections from both reading a
// pending record and then racing while upgrading their deferred transaction
// to a write transaction. The operation key remains the durable idempotency
// boundary for retries from other processes or after a restart.
billingSettlementSQLiteWriteMu sync.Mutex
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)

type permanentBillingSettlementError struct {
Expand Down Expand Up @@ -202,6 +210,7 @@ type BillingSettlementReconciliationItem struct {
TaskQuota int64 `json:"task_quota"`
TaskQuotaTarget int64 `json:"task_quota_target"`
RequiresManualCompletion bool `json:"requires_manual_completion"`
ZeroQuotaEligible bool `json:"zero_quota_eligible"`
FundingDelta int64 `json:"funding_delta"`
AppliedFundingDelta int64 `json:"applied_funding_delta"`
TokenDelta int64 `json:"token_delta"`
Expand Down Expand Up @@ -460,6 +469,22 @@ func GetUnresolvedPositiveFinalizeSettlements(limit int) (BillingSettlementRecon
data.Truncated = true
items = items[:limit]
}
taskIDs := make([]int64, 0, len(items))
for _, item := range items {
if item.TaskID > 0 {
taskIDs = append(taskIDs, item.TaskID)
}
}
tasksByID := make(map[int64]*Task, len(taskIDs))
if len(taskIDs) > 0 {
var tasks []Task
if err := tx.Select("id", "properties").Where("id IN ?", taskIDs).Find(&tasks).Error; err != nil {
return err
}
for index := range tasks {
tasksByID[tasks[index].ID] = &tasks[index]
}
}
displayedUserIDs := make([]int, 0, len(items))
for index := range items {
items[index].LastError = common.SanitizePersistedLogContent(
Expand All @@ -473,6 +498,9 @@ func GetUnresolvedPositiveFinalizeSettlements(limit int) (BillingSettlementRecon
FundingDelta: items[index].FundingDelta, TokenDelta: items[index].TokenDelta,
TaskQuota: items[index].TaskQuota, TaskQuotaTarget: items[index].TaskQuotaTarget,
})
if items[index].RequiresManualCompletion {
items[index].ZeroQuotaEligible = IsMiniMaxH3Task(tasksByID[items[index].TaskID])
}
items[index].RecordBlocksUser = items[index].FundingDelta > 0 &&
strings.HasPrefix(items[index].OperationKey, billingRequestOperationPrefix) &&
strings.HasSuffix(items[index].OperationKey, billingRequestFinalizeSuffix) &&
Expand Down Expand Up @@ -766,6 +794,10 @@ func EnsureManualTaskBillingCompletion(input BillingSettlementInput, reviewerID
if reviewerID <= 0 || strings.TrimSpace(note) == "" || !billingSettlementIsManualTaskCompletion(input.OperationKey, input.TaskID) {
return BillingSettlement{}, ErrBillingSettlementReviewConflict
}
if common.UsingSQLite {
billingSettlementSQLiteWriteMu.Lock()
defer billingSettlementSQLiteWriteMu.Unlock()
}
var approved BillingSettlement
err := DB.Transaction(func(tx *gorm.DB) error {
record, _, err := ensureBillingSettlementRecordDB(tx, input)
Expand Down Expand Up @@ -953,6 +985,10 @@ func ResolveManualTaskBillingSettlement(id int64, expectedRevision int64, review
if id <= 0 || expectedRevision <= 0 || reviewerID <= 0 || strings.TrimSpace(note) == "" {
return BillingSettlement{}, false, ErrBillingSettlementReviewConflict
}
if common.UsingSQLite {
billingSettlementSQLiteWriteMu.Lock()
defer billingSettlementSQLiteWriteMu.Unlock()
}
var resolved BillingSettlement
alreadyResolved := false
err := DB.Transaction(func(tx *gorm.DB) error {
Expand Down Expand Up @@ -1054,6 +1090,10 @@ func ApplyBillingSettlementOnce(input BillingSettlementInput) (appliedFundingDel
if input.OperationKey == "" {
return 0, false, errors.New("billing settlement operation key is required")
}
if common.UsingSQLite {
billingSettlementSQLiteWriteMu.Lock()
defer billingSettlementSQLiteWriteMu.Unlock()
}

record, alreadyApplied, err := ensureBillingSettlementRecord(input)
if err != nil {
Expand Down Expand Up @@ -1253,6 +1293,10 @@ func PromoteManualTaskBillingSettlement(input BillingSettlementInput, reasonPref
if err := validateBillingSettlementInput(input); err != nil {
return false, err
}
if common.UsingSQLite {
billingSettlementSQLiteWriteMu.Lock()
defer billingSettlementSQLiteWriteMu.Unlock()
}
effectPayload, err := billingSettlementEffectPayload(input.Effect)
if err != nil {
return false, fmt.Errorf("%w: %v", ErrBillingSettlementRecordNotDurable, err)
Expand Down
23 changes: 23 additions & 0 deletions model/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"

"github.com/MAX-API-Next/MAX-API/common"
Expand Down Expand Up @@ -72,6 +73,19 @@ type Task struct {
includePrivateDataInUpdate bool `gorm:"-"`
}

// IsMiniMaxH3Task reports whether the persisted task has an explicit,
// trusted MiniMax-H3 upstream binding. Client-side origin aliases and legacy
// billing-context names are intentionally not sufficient for H3-only actions.
func IsMiniMaxH3Task(task *Task) bool {
if task == nil {
return false
}
if upstream := strings.TrimSpace(task.Properties.UpstreamModelName); upstream != "" {
return strings.EqualFold(upstream, constant.TaskModelMiniMaxH3)
}
return false
}

func (t *Task) BeforeSave(*gorm.DB) error {
if t != nil {
t.FailReason = common.SanitizePersistedLogContent(t.FailReason)
Expand Down Expand Up @@ -157,6 +171,7 @@ type TaskBillingContext struct {
TaskBilling *types.TaskBillingResult `json:"task_billing,omitempty"`
TaskBillingPlan *types.TaskBillingPlan `json:"task_billing_plan,omitempty"`
TaskUsage *types.TaskUsage `json:"task_usage,omitempty"`
TaskUsageEnvelope *types.TaskUsageEnvelope `json:"task_usage_envelope,omitempty"`
OriginModelName string `json:"origin_model_name,omitempty"` // 模型名称,必须为OriginModelName
PerCallBilling bool `json:"per_call_billing,omitempty"` // 按次计费:跳过轮询阶段的差额结算
DeltaSettlementDisabled *bool `json:"delta_settlement_disabled,omitempty"` // 渠道关闭完成态差额结算时按提交快照跳过
Expand Down Expand Up @@ -722,6 +737,10 @@ func (t *Task) UpdateWithStatusAndSettlementIntent(fromStatus TaskStatus, expect
updatedAt = expectedUpdatedAt + 1
}

if common.UsingSQLite {
billingSettlementSQLiteWriteMu.Lock()
defer billingSettlementSQLiteWriteMu.Unlock()
}
err := DB.Transaction(func(tx *gorm.DB) error {
if _, _, err := ensureBillingSettlementRecordDB(tx, input); err != nil {
return err
Expand Down Expand Up @@ -779,6 +798,10 @@ func (t *Task) UpdateWithStatusAndManualSettlement(fromStatus TaskStatus, expect
updatedAt = expectedUpdatedAt + 1
}

if common.UsingSQLite {
billingSettlementSQLiteWriteMu.Lock()
defer billingSettlementSQLiteWriteMu.Unlock()
}
err := DB.Transaction(func(tx *gorm.DB) error {
if _, err := ensureManualBillingSettlementRecordDB(tx, input, reason); err != nil {
return err
Expand Down
Loading
Loading