fix(*): 支付与Agent运行 - 修复商品策略并保证审批恢复稳定
This commit is contained in:
1 parent
37bb4d7242
commit
3dd84ff25b
17 files changed
+1836
-226
No files matched your search
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -52,7 +53,13 @@ func (s *Service) startCloudAgentPi(userID, runID string) {
|
||||
defer func() {
|
||||
s.piRunnerMu.Lock()
|
||||
delete(s.piRunners, runID)
|
||||
_, restart := s.piRunnerRestarts[runID]
|
||||
delete(s.piRunnerRestarts, runID)
|
||||
s.piRunnerMu.Unlock()
|
||||
// 先登记新会话再 Done,关闭流程等待时不会漏掉刚恢复的会话。
|
||||
if restart {
|
||||
s.startCloudAgentPi(userID, runID)
|
||||
}
|
||||
s.piRunnerWg.Done()
|
||||
}()
|
||||
|
||||
@@ -63,6 +70,26 @@ func (s *Service) startCloudAgentPi(userID, runID string) {
|
||||
}()
|
||||
}
|
||||
|
||||
// resumeCloudAgentPi 用于审批后恢复运行。审批暂停的会话在中止后仍要刷新事件和
|
||||
// 会话快照,用户很快批准时它可能还没退出;这时不能像普通重复启动那样丢弃,
|
||||
// 而是登记一次重启,等旧会话退出后用已保存的恢复提示词继续。
|
||||
func (s *Service) resumeCloudAgentPi(userID, runID string) {
|
||||
if s == nil || s.disablePiRuntime || runID == "" {
|
||||
return
|
||||
}
|
||||
s.piRunnerMu.Lock()
|
||||
if _, exists := s.piRunners[runID]; exists && !s.piRunnersClosed {
|
||||
if s.piRunnerRestarts == nil {
|
||||
s.piRunnerRestarts = make(map[string]struct{})
|
||||
}
|
||||
s.piRunnerRestarts[runID] = struct{}{}
|
||||
s.piRunnerMu.Unlock()
|
||||
return
|
||||
}
|
||||
s.piRunnerMu.Unlock()
|
||||
s.startCloudAgentPi(userID, runID)
|
||||
}
|
||||
|
||||
// closeCloudAgentPiRunners cancels and joins every Pi session before the
|
||||
// service's database and other dependencies are closed. Without the join, a
|
||||
// test or graceful shutdown can close the repository while a child runtime is
|
||||
@@ -308,12 +335,17 @@ func (s *Service) runCloudAgentPiSession(ctx context.Context, userID, runID stri
|
||||
}
|
||||
|
||||
// 7. 运行 Pi 会话
|
||||
var pausedForApproval atomic.Bool
|
||||
err = runCloudAgentPi(ctx, request, cloudAgentPiBridge{
|
||||
Model: func(callCtx context.Context, payload map[string]json.RawMessage) (any, error) {
|
||||
return s.cloudAgentPiModel(callCtx, userID, runID, payload)
|
||||
},
|
||||
Tool: func(callCtx context.Context, payload map[string]json.RawMessage) (any, error) {
|
||||
return s.cloudAgentPiTool(callCtx, userID, runID, payload)
|
||||
result, err := s.cloudAgentPiTool(callCtx, userID, runID, payload)
|
||||
if paused, ok := result.(map[string]any); ok && paused["pause"] == true {
|
||||
pausedForApproval.Store(true)
|
||||
}
|
||||
return result, err
|
||||
},
|
||||
Event: func(callCtx context.Context, payload map[string]json.RawMessage) (any, error) {
|
||||
return s.cloudAgentPiEvent(callCtx, userID, runID, payload)
|
||||
@@ -323,6 +355,12 @@ func (s *Service) runCloudAgentPiSession(ctx context.Context, userID, runID stri
|
||||
return err
|
||||
}
|
||||
|
||||
// 本轮因审批暂停:运行由审批决定接管,不能在这里完成。用户可能在会话退出前
|
||||
// 就已批准,此时状态已回到 running,若继续会把未恢复的运行误标为完成。
|
||||
if pausedForApproval.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 8. 清理恢复提示词
|
||||
if runtimeState.PiResumePrompt != "" {
|
||||
if err := s.saveCloudAgentPiResumePrompt(userID, runID, ""); err != nil {
|
||||
|
||||
@@ -16,3 +16,19 @@ func TestStartCloudAgentPiDoesNotRestartAnActiveRunner(t *testing.T) {
|
||||
t.Fatalf("duplicate start cancelled the active Pi runner: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 审批恢复时旧会话可能还在收尾:不能取消它,也不能丢弃恢复请求。
|
||||
func TestResumeCloudAgentPiQueuesRestartWhileRunnerIsExiting(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
s := &Service{piRunners: map[string]context.CancelFunc{"run-a": cancel}}
|
||||
s.resumeCloudAgentPi("user-a", "run-a")
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
t.Fatalf("resume cancelled the exiting Pi runner: %v", err)
|
||||
}
|
||||
if _, queued := s.piRunnerRestarts["run-a"]; !queued {
|
||||
t.Fatal("resume request was dropped while the previous runner was still exiting")
|
||||
}
|
||||
}
|
||||
@@ -2454,80 +2454,119 @@ func (s *Service) DecideCloudAgentApproval(userID, id, approvalID, decision, rea
|
||||
return creationConflict("审批不存在或已过期")
|
||||
}
|
||||
// 只在记录审批决定的这次写入期间持锁:随后的工具执行(advanceCloudAgentTool)
|
||||
// 会自己获取 storageMu,持锁到函数返回会造成自锁。
|
||||
s.storageMu.Lock()
|
||||
err = s.repo.MutateCloudAgent(userID, id, run.Revision, func(current *model.CloudAgentExecution, repo *repository.Repository) error {
|
||||
if settings != nil {
|
||||
if err := s.updateCloudAgentMediaApproval(repo, run, &state, *settings); err != nil {
|
||||
// 会自己获取 storageMu,持锁到函数返回会造成自锁。旧 Pi 会话退出前仍可能
|
||||
// 写入最后一个事件,因此审批决定采用重读后 CAS 重试。
|
||||
for attempt := 0; attempt < 8; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(time.Duration(attempt) * 20 * time.Millisecond)
|
||||
run, err = s.repo.CloudAgent(userID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
state.Approval.Decision = decision
|
||||
state.Approval.Reason = reason
|
||||
state.Decisions[approvalID] = decision
|
||||
if settings != nil {
|
||||
if state.DecisionSettings == nil {
|
||||
state.DecisionSettings = map[string]string{}
|
||||
}
|
||||
state.DecisionSettings[approvalID] = creationHash(settings)
|
||||
}
|
||||
if decision == "approve" && state.Approval.Prepared != nil {
|
||||
if state.DecisionPreparedHashes == nil {
|
||||
state.DecisionPreparedHashes = map[string]string{}
|
||||
}
|
||||
state.DecisionPreparedHashes[approvalID] = state.Approval.Prepared.Hash
|
||||
}
|
||||
if decision == "reject" {
|
||||
// Rejection is a user control-plane decision, not a failed tool
|
||||
// invocation. Make it terminal before the scheduler can advance the
|
||||
// pending call; no tool result, canvas mutation, generation task or
|
||||
// follow-up model request may be produced from this decision.
|
||||
current.Status = "rejected"
|
||||
current.FailureMessage = ""
|
||||
state.Approval = nil
|
||||
state.event(id, "approval_decided", map[string]any{
|
||||
"approvalId": approvalID,
|
||||
"decision": decision,
|
||||
"reason": reason,
|
||||
"text": "已拒绝本次生成,草稿节点仍保留在画布中;未提交任务、未产生扣费。你可以继续编辑后重新申请。",
|
||||
})
|
||||
if err := repo.ReleaseCloudAgentResourceLeases(userID, approvalID); err != nil {
|
||||
state, err = cloudAgentDecodeForExecution(run)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if previous, ok := state.Decisions[approvalID]; ok {
|
||||
if previous == decision && (settings == nil || state.DecisionSettings[approvalID] == creationHash(settings)) {
|
||||
return nil
|
||||
}
|
||||
return creationConflict("该审批已有不同决定")
|
||||
}
|
||||
if run.Status != "waiting_approval" || state.Approval == nil || state.Approval.ID != approvalID {
|
||||
return creationConflict("审批不存在或已过期")
|
||||
}
|
||||
}
|
||||
s.storageMu.Lock()
|
||||
err = s.repo.MutateCloudAgent(userID, id, run.Revision, func(current *model.CloudAgentExecution, repo *repository.Repository) error {
|
||||
if settings != nil {
|
||||
if err := s.updateCloudAgentMediaApproval(repo, run, &state, *settings); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
state.Approval.Decision = decision
|
||||
state.Approval.Reason = reason
|
||||
state.Decisions[approvalID] = decision
|
||||
if settings != nil {
|
||||
if state.DecisionSettings == nil {
|
||||
state.DecisionSettings = map[string]string{}
|
||||
}
|
||||
state.DecisionSettings[approvalID] = creationHash(settings)
|
||||
}
|
||||
if decision == "approve" && state.Approval.Prepared != nil {
|
||||
if state.DecisionPreparedHashes == nil {
|
||||
state.DecisionPreparedHashes = map[string]string{}
|
||||
}
|
||||
state.DecisionPreparedHashes[approvalID] = state.Approval.Prepared.Hash
|
||||
}
|
||||
if decision == "reject" {
|
||||
// Rejection is a user control-plane decision, not a failed tool
|
||||
// invocation. Make it terminal before the scheduler can advance the
|
||||
// pending call; no tool result, canvas mutation, generation task or
|
||||
// follow-up model request may be produced from this decision.
|
||||
current.Status = "rejected"
|
||||
current.FailureMessage = ""
|
||||
state.Approval = nil
|
||||
state.event(id, "approval_decided", map[string]any{
|
||||
"approvalId": approvalID,
|
||||
"decision": decision,
|
||||
"reason": reason,
|
||||
"text": "已拒绝本次生成,草稿节点仍保留在画布中;未提交任务、未产生扣费。你可以继续编辑后重新申请。",
|
||||
})
|
||||
if err := repo.ReleaseCloudAgentResourceLeases(userID, approvalID); err != nil {
|
||||
return err
|
||||
}
|
||||
return cloudAgentSave(current, &state)
|
||||
}
|
||||
current.Status = "running"
|
||||
payload := map[string]any{"approvalId": approvalID, "decision": decision, "arguments": json.RawMessage(state.Approval.Call.Function.Arguments), "preview": state.Approval.Preview, "modelName": state.Approval.ModelName}
|
||||
if state.Approval.Prepared != nil {
|
||||
payload["preparedHash"] = state.Approval.Prepared.Hash
|
||||
payload["generationId"] = state.Approval.Prepared.GenerationID
|
||||
}
|
||||
state.event(id, "approval_decided", payload)
|
||||
return cloudAgentSave(current, &state)
|
||||
})
|
||||
s.storageMu.Unlock()
|
||||
if !errors.Is(err, repository.ErrCreationConflict) {
|
||||
break
|
||||
}
|
||||
current.Status = "running"
|
||||
payload := map[string]any{"approvalId": approvalID, "decision": decision, "arguments": json.RawMessage(state.Approval.Call.Function.Arguments), "preview": state.Approval.Preview, "modelName": state.Approval.ModelName}
|
||||
if state.Approval.Prepared != nil {
|
||||
payload["preparedHash"] = state.Approval.Prepared.Hash
|
||||
payload["generationId"] = state.Approval.Prepared.GenerationID
|
||||
}
|
||||
state.event(id, "approval_decided", payload)
|
||||
return cloudAgentSave(current, &state)
|
||||
})
|
||||
s.storageMu.Unlock()
|
||||
}
|
||||
if err != nil || decision != "approve" {
|
||||
return err
|
||||
}
|
||||
// Approval only releases the paused tool. Execute it once in the Go business
|
||||
// executor, then give the durable result back to Pi; Go never asks a model
|
||||
// what to do next.
|
||||
latest, err := s.repo.CloudAgent(userID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
var mediaTaskID string
|
||||
for attempt := 0; attempt < 8; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(time.Duration(attempt) * 20 * time.Millisecond)
|
||||
}
|
||||
latest, err := s.repo.CloudAgent(userID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err = cloudAgentDecode(latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.advanceCloudAgentTool(latest, &state)
|
||||
if !errors.Is(err, repository.ErrCreationConflict) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mediaTaskID = state.MediaTaskID
|
||||
break
|
||||
}
|
||||
if attempt == 7 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
state, err = cloudAgentDecode(latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = s.advanceCloudAgentTool(latest, &state); err != nil {
|
||||
return err
|
||||
}
|
||||
if state.MediaTaskID == "" {
|
||||
if mediaTaskID == "" {
|
||||
return s.resumeCloudAgentAfterApproval(userID, id, &state)
|
||||
}
|
||||
// 媒体生成可能要几分钟:审批请求立即返回,等待与回写在后台完成后再恢复运行。
|
||||
mediaTaskID := state.MediaTaskID
|
||||
s.startApprovedCloudAgentMediaWaiter(userID, id, mediaTaskID)
|
||||
return nil
|
||||
}
|
||||
@@ -2564,7 +2603,7 @@ func (s *Service) resumeCloudAgentAfterApproval(userID, id string, state *cloudA
|
||||
if err := s.saveCloudAgentPiResumePrompt(userID, id, state.PiResumePrompt); err != nil {
|
||||
return err
|
||||
}
|
||||
s.startCloudAgentPi(userID, id)
|
||||
s.resumeCloudAgentPi(userID, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -57,12 +57,18 @@ type UpdatePaymentProviderConfigRequest struct {
|
||||
}
|
||||
|
||||
type TopupProductRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
AmountFen int64 `json:"amountFen"`
|
||||
CreditsMicrocredits int64 `json:"creditsMicrocredits"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
AmountFen int64 `json:"amountFen"`
|
||||
CreditsMicrocredits int64 `json:"creditsMicrocredits"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
SaleStrategy model.TopupSaleStrategy `json:"saleStrategy"`
|
||||
PeriodDays int `json:"periodDays"`
|
||||
PeriodPurchaseLimit int `json:"periodPurchaseLimit"`
|
||||
StockTotal int64 `json:"stockTotal"`
|
||||
SaleStartAt *time.Time `json:"saleStartAt"`
|
||||
SaleEndAt *time.Time `json:"saleEndAt"`
|
||||
}
|
||||
|
||||
type CreatePaymentOrderRequest struct {
|
||||
@@ -430,28 +436,63 @@ func (s *Service) TopupProducts(actor *model.User) ([]model.TopupProduct, error)
|
||||
if err := s.RequireFeature(FeatureCredits); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.TopupProducts(false)
|
||||
products, err := s.repo.TopupProducts(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decorateTopupProducts(products, time.Now()), nil
|
||||
}
|
||||
|
||||
func (s *Service) AdminTopupProducts(actor *model.User) ([]model.TopupProduct, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.TopupProducts(true)
|
||||
products, err := s.repo.TopupProducts(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return decorateTopupProducts(products, time.Now()), nil
|
||||
}
|
||||
|
||||
func decorateTopupProducts(products []model.TopupProduct, now time.Time) []model.TopupProduct {
|
||||
for index := range products {
|
||||
product := &products[index]
|
||||
if product.SaleStrategy == "" {
|
||||
product.SaleStrategy = model.TopupSaleStrategyUnlimited
|
||||
}
|
||||
product.CanPurchase = product.Enabled
|
||||
product.SaleStatus = "on_sale"
|
||||
if !product.Enabled {
|
||||
product.CanPurchase = false
|
||||
product.SaleStatus = "disabled"
|
||||
} else if product.SaleStrategy == model.TopupSaleStrategyInventory && product.StockRemaining <= 0 {
|
||||
product.CanPurchase = false
|
||||
product.SaleStatus = "sold_out"
|
||||
} else if product.SaleStrategy == model.TopupSaleStrategyTimed {
|
||||
if product.SaleStartAt != nil && now.Before(*product.SaleStartAt) {
|
||||
product.CanPurchase = false
|
||||
product.SaleStatus = "upcoming"
|
||||
} else if product.SaleEndAt != nil && !now.Before(*product.SaleEndAt) {
|
||||
product.CanPurchase = false
|
||||
product.SaleStatus = "ended"
|
||||
}
|
||||
}
|
||||
}
|
||||
return products
|
||||
}
|
||||
|
||||
func (s *Service) CreateTopupProduct(actor *model.User, request TopupProductRequest) (*model.TopupProduct, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
product, err := topupProductFromRequest(newID(), actor.ID, request)
|
||||
product, err := topupProductFromRequest(newID(), actor.ID, request, 0, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.repo.CreateTopupProduct(product); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.appendAdminAudit(actor, "topup_product.create", "topup_product", product.ID, "创建积分充值商品", map[string]any{"amountFen": product.AmountFen, "creditsMicrocredits": product.CreditsMicrocredits}); err != nil {
|
||||
if err := s.appendAdminAudit(actor, "topup_product.create", "topup_product", product.ID, "创建积分充值商品", map[string]any{"amountFen": product.AmountFen, "creditsMicrocredits": product.CreditsMicrocredits, "saleStrategy": product.SaleStrategy}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return product, nil
|
||||
@@ -461,23 +502,32 @@ func (s *Service) UpdateTopupProduct(actor *model.User, id string, request Topup
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.repo.TopupProduct(id); err != nil {
|
||||
existing, err := s.repo.TopupProduct(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
product, err := topupProductFromRequest(strings.TrimSpace(id), actor.ID, request)
|
||||
consumed := existing.StockTotal - existing.StockRemaining
|
||||
if existing.SaleStrategy != model.TopupSaleStrategyInventory {
|
||||
consumed = 0
|
||||
}
|
||||
product, err := topupProductFromRequest(strings.TrimSpace(id), actor.ID, request, existing.StockTotal, consumed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.repo.UpdateTopupProduct(product); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.appendAdminAudit(actor, "topup_product.update", "topup_product", product.ID, "更新积分充值商品", map[string]any{"enabled": product.Enabled}); err != nil {
|
||||
if err := s.appendAdminAudit(actor, "topup_product.update", "topup_product", product.ID, "更新积分充值商品", map[string]any{"enabled": product.Enabled, "saleStrategy": product.SaleStrategy}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.TopupProduct(product.ID)
|
||||
}
|
||||
|
||||
func topupProductFromRequest(id, actorID string, request TopupProductRequest) (*model.TopupProduct, error) {
|
||||
func topupProductFromRequest(id, actorID string, request TopupProductRequest, stockArgs ...int64) (*model.TopupProduct, error) {
|
||||
var consumed int64
|
||||
if len(stockArgs) > 1 {
|
||||
consumed = stockArgs[1]
|
||||
}
|
||||
name := strings.TrimSpace(request.Name)
|
||||
if name == "" || len([]rune(name)) > 120 {
|
||||
return nil, BadAuthRequest("充值商品名称不能为空且不能超过 120 个字符")
|
||||
@@ -488,10 +538,40 @@ func topupProductFromRequest(id, actorID string, request TopupProductRequest) (*
|
||||
if request.CreditsMicrocredits <= 0 || request.CreditsMicrocredits > maxTopupCreditsMicrocredits {
|
||||
return nil, BadAuthRequest("充值积分必须为 0.000001 至 10 亿积分")
|
||||
}
|
||||
strategy := request.SaleStrategy
|
||||
if strategy == "" {
|
||||
strategy = model.TopupSaleStrategyUnlimited
|
||||
}
|
||||
if strategy != model.TopupSaleStrategyUnlimited && strategy != model.TopupSaleStrategyPeriodic && strategy != model.TopupSaleStrategyInventory && strategy != model.TopupSaleStrategyTimed {
|
||||
return nil, BadAuthRequest("上架策略无效")
|
||||
}
|
||||
if strategy == model.TopupSaleStrategyPeriodic && (request.PeriodDays <= 0 || request.PeriodDays > 3650 || request.PeriodPurchaseLimit <= 0 || request.PeriodPurchaseLimit > 100000) {
|
||||
return nil, BadAuthRequest("周期性商品需设置有效的周期天数和购买次数")
|
||||
}
|
||||
if strategy == model.TopupSaleStrategyInventory && request.StockTotal <= 0 {
|
||||
return nil, BadAuthRequest("库存性商品库存必须大于 0")
|
||||
}
|
||||
if strategy == model.TopupSaleStrategyTimed && (request.SaleStartAt == nil || request.SaleEndAt == nil || !request.SaleEndAt.After(*request.SaleStartAt)) {
|
||||
return nil, BadAuthRequest("限时商品必须设置有效的发售和结束时间")
|
||||
}
|
||||
stockRemaining := int64(0)
|
||||
if strategy == model.TopupSaleStrategyInventory {
|
||||
if consumed < 0 {
|
||||
consumed = 0
|
||||
}
|
||||
if request.StockTotal < consumed {
|
||||
return nil, BadAuthRequest(fmt.Sprintf("库存不能低于已售数量(至少 %d)", consumed))
|
||||
}
|
||||
stockRemaining = request.StockTotal - consumed
|
||||
}
|
||||
return &model.TopupProduct{
|
||||
ID: id, Name: name, Description: truncateRunes(strings.TrimSpace(request.Description), 500),
|
||||
AmountFen: request.AmountFen, CreditsMicrocredits: request.CreditsMicrocredits,
|
||||
Enabled: request.Enabled, SortOrder: request.SortOrder, CreatedBy: actorID, UpdatedBy: actorID,
|
||||
Enabled: request.Enabled, SortOrder: request.SortOrder, SaleStrategy: strategy,
|
||||
PeriodDays: request.PeriodDays, PeriodPurchaseLimit: request.PeriodPurchaseLimit,
|
||||
StockTotal: request.StockTotal, StockRemaining: stockRemaining,
|
||||
SaleStartAt: request.SaleStartAt, SaleEndAt: request.SaleEndAt,
|
||||
CreatedBy: actorID, UpdatedBy: actorID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -551,7 +631,10 @@ func (s *Service) CreatePaymentOrder(ctx context.Context, actor *model.User, req
|
||||
Status: model.PaymentOrderCreated, CheckoutMode: provider.Descriptor().CheckoutMode,
|
||||
ExpiresAt: now.Add(time.Duration(config.CloseAfterMinutes) * time.Minute),
|
||||
}
|
||||
order, created, err := s.repo.CreatePaymentOrder(order)
|
||||
order, created, err := s.repo.CreatePaymentOrderWithProductReservation(order)
|
||||
if errors.Is(err, repository.ErrTopupUnavailable) {
|
||||
return nil, NewAppError(http.StatusConflict, "充值商品当前不可购买")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -81,8 +81,10 @@ type Service struct {
|
||||
piRunnerMu sync.Mutex
|
||||
piRunnerWg sync.WaitGroup
|
||||
piRunners map[string]context.CancelFunc
|
||||
piRunnersClosed bool
|
||||
disablePiRuntime bool
|
||||
// piRunnerRestarts 记录审批恢复时旧会话仍在收尾的运行,旧会话退出后再启动一次。
|
||||
piRunnerRestarts map[string]struct{}
|
||||
piRunnersClosed bool
|
||||
disablePiRuntime bool
|
||||
// legacyCloudAgentRootTask is enabled only by tests that exercise the pre-Pi
|
||||
// model-worker path. Runtime availability must not change root task semantics.
|
||||
legacyCloudAgentRootTask bool
|
||||
|
||||
Reference in new issue
Block a user