fix(platform): 任务并发 - 账号活动任务上限统一跟随后台热配置
This commit is contained in:
1 parent
0b064e532a
commit
dd4af80b98
9 files changed
+58
-122
No files matched your search
@@ -523,19 +523,18 @@ type CharacterVoiceBinding struct {
|
|||||||
|
|
||||||
// Project 是短剧领域聚合根;CanvasProject 仍代表可独立创作的画布文档。
|
// Project 是短剧领域聚合根;CanvasProject 仍代表可独立创作的画布文档。
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID string `json:"id" gorm:"primaryKey;size:36"`
|
ID string `json:"id" gorm:"primaryKey;size:36"`
|
||||||
UserID string `json:"userId" gorm:"index;size:36;uniqueIndex:idx_projects_user_name,priority:1"`
|
UserID string `json:"userId" gorm:"index;size:36;uniqueIndex:idx_projects_user_name,priority:1"`
|
||||||
Name string `json:"name" gorm:"size:240;uniqueIndex:idx_projects_user_name,priority:2"`
|
Name string `json:"name" gorm:"size:240;uniqueIndex:idx_projects_user_name,priority:2"`
|
||||||
Type string `json:"type" gorm:"size:32;index"`
|
Type string `json:"type" gorm:"size:32;index"`
|
||||||
AspectRatio string `json:"aspectRatio" gorm:"size:16"`
|
AspectRatio string `json:"aspectRatio" gorm:"size:16"`
|
||||||
SourceType string `json:"sourceType" gorm:"size:32"`
|
SourceType string `json:"sourceType" gorm:"size:32"`
|
||||||
Description string `json:"description" gorm:"type:text"`
|
Description string `json:"description" gorm:"type:text"`
|
||||||
StylePresetID string `json:"stylePresetId" gorm:"size:64"`
|
StylePresetID string `json:"stylePresetId" gorm:"size:64"`
|
||||||
ActiveTaskLimit int `json:"activeTaskLimit" gorm:"default:3"`
|
Status ProjectStatus `json:"status" gorm:"index;size:24"`
|
||||||
Status ProjectStatus `json:"status" gorm:"index;size:24"`
|
Revision int64 `json:"revision"`
|
||||||
Revision int64 `json:"revision"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
UpdatedAt time.Time `json:"updatedAt" gorm:"index"`
|
||||||
UpdatedAt time.Time `json:"updatedAt" gorm:"index"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectUnit struct {
|
type ProjectUnit struct {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ var (
|
|||||||
ErrInsufficientCredits = errors.New("insufficient credits")
|
ErrInsufficientCredits = errors.New("insufficient credits")
|
||||||
ErrRedeemCodeInvalid = errors.New("redeem code invalid")
|
ErrRedeemCodeInvalid = errors.New("redeem code invalid")
|
||||||
ErrActiveTaskLimit = errors.New("active task limit reached")
|
ErrActiveTaskLimit = errors.New("active task limit reached")
|
||||||
ErrProjectTaskLimit = errors.New("project active task limit reached")
|
|
||||||
ErrTaskNotRetryable = errors.New("task is not retryable")
|
ErrTaskNotRetryable = errors.New("task is not retryable")
|
||||||
ErrBillingStateConflict = errors.New("billing state conflict")
|
ErrBillingStateConflict = errors.New("billing state conflict")
|
||||||
)
|
)
|
||||||
@@ -187,14 +186,11 @@ func (r *Repository) CreditLedgerReferenceExists(referenceKey string) (bool, err
|
|||||||
return count > 0, err
|
return count > 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateTaskWithCreditReservation(task *model.Task, order *model.BillingOrder, activeTaskLimit int, domainProjectID string, projectTaskLimit int) error {
|
func (r *Repository) CreateTaskWithCreditReservation(task *model.Task, order *model.BillingOrder, activeTaskLimit int) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := enforceActiveTaskLimit(tx, task.UserID, activeTaskLimit); err != nil {
|
if err := enforceActiveTaskLimit(tx, task.UserID, activeTaskLimit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := enforceProjectTaskLimit(tx, task.UserID, domainProjectID, projectTaskLimit); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := reserveBillingOrder(tx, order); err != nil {
|
if err := reserveBillingOrder(tx, order); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -202,27 +198,21 @@ func (r *Repository) CreateTaskWithCreditReservation(task *model.Task, order *mo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateTaskWithActiveLimit(task *model.Task, activeTaskLimit int, domainProjectID string, projectTaskLimit int) error {
|
func (r *Repository) CreateTaskWithActiveLimit(task *model.Task, activeTaskLimit int) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := enforceActiveTaskLimit(tx, task.UserID, activeTaskLimit); err != nil {
|
if err := enforceActiveTaskLimit(tx, task.UserID, activeTaskLimit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := enforceProjectTaskLimit(tx, task.UserID, domainProjectID, projectTaskLimit); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Create(task).Error
|
return tx.Create(task).Error
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) RetryTaskWithBilling(userID string, taskID string, order *model.BillingOrder, activeTaskLimit int, domainProjectID string, projectTaskLimit int) (*model.Task, error) {
|
func (r *Repository) RetryTaskWithBilling(userID string, taskID string, order *model.BillingOrder, activeTaskLimit int) (*model.Task, error) {
|
||||||
var task model.Task
|
var task model.Task
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := enforceActiveTaskLimit(tx, userID, activeTaskLimit); err != nil {
|
if err := enforceActiveTaskLimit(tx, userID, activeTaskLimit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := enforceProjectTaskLimit(tx, userID, domainProjectID, projectTaskLimit); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if order != nil {
|
if order != nil {
|
||||||
if err := reserveBillingOrder(tx, order); err != nil {
|
if err := reserveBillingOrder(tx, order); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -260,23 +250,6 @@ func enforceActiveTaskLimit(tx *gorm.DB, userID string, activeTaskLimit int) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func enforceProjectTaskLimit(tx *gorm.DB, userID string, projectID string, limit int) error {
|
|
||||||
if projectID == "" || limit <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
canvasIDs := tx.Model(&model.CanvasProject{}).Select("id").Where("user_id = ? AND project_id = ?", userID, projectID)
|
|
||||||
var count int64
|
|
||||||
if err := tx.Model(&model.Task{}).
|
|
||||||
Where("user_id = ? AND status IN ? AND (project_id = ? OR project_id IN (?))", userID, []model.TaskStatus{model.TaskStatusQueued, model.TaskStatusRunning}, projectID, canvasIDs).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count >= int64(limit) {
|
|
||||||
return ErrProjectTaskLimit
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repository) ReserveBillingOrder(order *model.BillingOrder) error {
|
func (r *Repository) ReserveBillingOrder(order *model.BillingOrder) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
return reserveBillingOrder(tx, order)
|
return reserveBillingOrder(tx, order)
|
||||||
|
|||||||
@@ -768,7 +768,7 @@ func (r *Repository) CreateProject(project *model.Project) error {
|
|||||||
func (r *Repository) UpdateProject(project *model.Project) error {
|
func (r *Repository) UpdateProject(project *model.Project) error {
|
||||||
return r.db.Model(&model.Project{}).Where("id = ? AND user_id = ?", project.ID, project.UserID).Updates(map[string]any{
|
return r.db.Model(&model.Project{}).Where("id = ? AND user_id = ?", project.ID, project.UserID).Updates(map[string]any{
|
||||||
"name": project.Name, "type": project.Type, "aspect_ratio": project.AspectRatio, "source_type": project.SourceType,
|
"name": project.Name, "type": project.Type, "aspect_ratio": project.AspectRatio, "source_type": project.SourceType,
|
||||||
"description": project.Description, "style_preset_id": project.StylePresetID, "active_task_limit": project.ActiveTaskLimit,
|
"description": project.Description, "style_preset_id": project.StylePresetID,
|
||||||
"status": project.Status, "revision": project.Revision, "updated_at": project.UpdatedAt,
|
"status": project.Status, "revision": project.Revision, "updated_at": project.UpdatedAt,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,24 +12,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateProjectRequest struct {
|
type CreateProjectRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
AspectRatio string `json:"aspectRatio"`
|
AspectRatio string `json:"aspectRatio"`
|
||||||
SourceType string `json:"sourceType"`
|
SourceType string `json:"sourceType"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
StylePresetID string `json:"stylePresetId"`
|
StylePresetID string `json:"stylePresetId"`
|
||||||
ActiveTaskLimit int `json:"activeTaskLimit"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateProjectRequest struct {
|
type UpdateProjectRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
AspectRatio string `json:"aspectRatio"`
|
AspectRatio string `json:"aspectRatio"`
|
||||||
SourceType string `json:"sourceType"`
|
SourceType string `json:"sourceType"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
StylePresetID *string `json:"stylePresetId"`
|
StylePresetID *string `json:"stylePresetId"`
|
||||||
ActiveTaskLimit *int `json:"activeTaskLimit"`
|
Status string `json:"status"`
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateProjectUnitRequest struct {
|
type CreateProjectUnitRequest struct {
|
||||||
@@ -147,9 +145,6 @@ func (s *Service) ProjectDetail(userID string, id string) (ProjectDetail, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ProjectDetail{}, err
|
return ProjectDetail{}, err
|
||||||
}
|
}
|
||||||
if project.ActiveTaskLimit <= 0 {
|
|
||||||
project.ActiveTaskLimit = 3
|
|
||||||
}
|
|
||||||
return ProjectDetail{Project: *project, Units: units, Canvases: canvases, CanvasUnitLinks: canvasUnitLinks, Assets: assets, Workflows: workflows, Shots: shots, ShotReferences: shotReferences, AssetCandidates: candidates}, nil
|
return ProjectDetail{Project: *project, Units: units, Canvases: canvases, CanvasUnitLinks: canvasUnitLinks, Assets: assets, Workflows: workflows, Shots: shots, ShotReferences: shotReferences, AssetCandidates: candidates}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,14 +169,7 @@ func (s *Service) CreateProject(userID string, req CreateProjectRequest) (model.
|
|||||||
sourceType = "blank"
|
sourceType = "blank"
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
activeTaskLimit := req.ActiveTaskLimit
|
project := model.Project{ID: newID(), UserID: userID, Name: name, Type: projectType, AspectRatio: aspectRatio, SourceType: sourceType, Description: strings.TrimSpace(req.Description), StylePresetID: strings.TrimSpace(req.StylePresetID), Status: model.ProjectStatusActive, Revision: 1, CreatedAt: now, UpdatedAt: now}
|
||||||
if activeTaskLimit == 0 {
|
|
||||||
activeTaskLimit = 3
|
|
||||||
}
|
|
||||||
if activeTaskLimit < 1 || activeTaskLimit > 20 {
|
|
||||||
return model.Project{}, BadAuthRequest("项目活跃任务上限必须在 1 到 20 之间")
|
|
||||||
}
|
|
||||||
project := model.Project{ID: newID(), UserID: userID, Name: name, Type: projectType, AspectRatio: aspectRatio, SourceType: sourceType, Description: strings.TrimSpace(req.Description), StylePresetID: strings.TrimSpace(req.StylePresetID), ActiveTaskLimit: activeTaskLimit, Status: model.ProjectStatusActive, Revision: 1, CreatedAt: now, UpdatedAt: now}
|
|
||||||
if err := s.repo.CreateProject(&project); err != nil {
|
if err := s.repo.CreateProject(&project); err != nil {
|
||||||
return model.Project{}, err
|
return model.Project{}, err
|
||||||
}
|
}
|
||||||
@@ -217,12 +205,6 @@ func (s *Service) UpdateProject(userID string, id string, req UpdateProjectReque
|
|||||||
if req.StylePresetID != nil {
|
if req.StylePresetID != nil {
|
||||||
project.StylePresetID = strings.TrimSpace(*req.StylePresetID)
|
project.StylePresetID = strings.TrimSpace(*req.StylePresetID)
|
||||||
}
|
}
|
||||||
if req.ActiveTaskLimit != nil {
|
|
||||||
if *req.ActiveTaskLimit < 1 || *req.ActiveTaskLimit > 20 {
|
|
||||||
return model.Project{}, BadAuthRequest("项目活跃任务上限必须在 1 到 20 之间")
|
|
||||||
}
|
|
||||||
project.ActiveTaskLimit = *req.ActiveTaskLimit
|
|
||||||
}
|
|
||||||
if status := model.ProjectStatus(strings.TrimSpace(req.Status)); status != "" {
|
if status := model.ProjectStatus(strings.TrimSpace(req.Status)); status != "" {
|
||||||
if status != model.ProjectStatusActive && status != model.ProjectStatusArchived {
|
if status != model.ProjectStatusActive && status != model.ProjectStatusArchived {
|
||||||
return model.Project{}, BadAuthRequest("不支持的项目状态")
|
return model.Project{}, BadAuthRequest("不支持的项目状态")
|
||||||
@@ -474,43 +456,36 @@ func IsProjectNotFound(err error) bool {
|
|||||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务仍以画布 ID 作为 projectId;这里统一解析到业务项目,避免项目限额和画布主键语义混淆。
|
// 任务仍以画布 ID 作为 projectId;写入前必须解析到业务项目并阻止归档项目继续生成。
|
||||||
func (s *Service) projectTaskLimit(userID string, canvasOrProjectID string) (string, int, error) {
|
func (s *Service) ensureTaskProjectActive(userID string, canvasOrProjectID string) error {
|
||||||
id := strings.TrimSpace(canvasOrProjectID)
|
id := strings.TrimSpace(canvasOrProjectID)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return "", 0, nil
|
return nil
|
||||||
}
|
}
|
||||||
if canvas, err := s.repo.CanvasProjectForUser(userID, id); err == nil {
|
if canvas, err := s.repo.CanvasProjectForUser(userID, id); err == nil {
|
||||||
if canvas.ProjectID == "" {
|
if canvas.ProjectID == "" {
|
||||||
return "", 0, nil
|
return nil
|
||||||
}
|
}
|
||||||
project, projectErr := s.repo.ProjectForUser(userID, canvas.ProjectID)
|
project, projectErr := s.repo.ProjectForUser(userID, canvas.ProjectID)
|
||||||
if projectErr != nil {
|
if projectErr != nil {
|
||||||
return "", 0, projectErr
|
return projectErr
|
||||||
}
|
}
|
||||||
if project.Status == model.ProjectStatusArchived {
|
if project.Status == model.ProjectStatusArchived {
|
||||||
return "", 0, BadAuthRequest("项目已归档,无法创建生成任务")
|
return BadAuthRequest("项目已归档,无法创建生成任务")
|
||||||
}
|
}
|
||||||
return project.ID, normalizedProjectTaskLimit(project.ActiveTaskLimit), nil
|
return nil
|
||||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return "", 0, err
|
return err
|
||||||
}
|
}
|
||||||
project, err := s.repo.ProjectForUser(userID, id)
|
project, err := s.repo.ProjectForUser(userID, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return "", 0, nil
|
return nil
|
||||||
}
|
}
|
||||||
return "", 0, err
|
return err
|
||||||
}
|
}
|
||||||
if project.Status == model.ProjectStatusArchived {
|
if project.Status == model.ProjectStatusArchived {
|
||||||
return "", 0, BadAuthRequest("项目已归档,无法创建生成任务")
|
return BadAuthRequest("项目已归档,无法创建生成任务")
|
||||||
}
|
}
|
||||||
return project.ID, normalizedProjectTaskLimit(project.ActiveTaskLimit), nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func normalizedProjectTaskLimit(limit int) int {
|
|
||||||
if limit <= 0 {
|
|
||||||
return 3
|
|
||||||
}
|
|
||||||
return limit
|
|
||||||
}
|
}
|
||||||
@@ -292,8 +292,7 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
|
|||||||
taskType = "video_image_to_video"
|
taskType = "video_image_to_video"
|
||||||
}
|
}
|
||||||
task := model.Task{ID: newID(), UserID: userID, SessionID: req.SessionID, ProjectID: req.ProjectID, Type: taskType, Status: model.TaskStatusQueued, Stage: "等待队列调度", Progress: 5, Prompt: prompt, Operation: req.Operation, Provider: req.Provider, Model: req.Model}
|
task := model.Task{ID: newID(), UserID: userID, SessionID: req.SessionID, ProjectID: req.ProjectID, Type: taskType, Status: model.TaskStatusQueued, Stage: "等待队列调度", Progress: 5, Prompt: prompt, Operation: req.Operation, Provider: req.Provider, Model: req.Model}
|
||||||
domainProjectID, projectTaskLimit, err := s.projectTaskLimit(userID, req.ProjectID)
|
if err := s.ensureTaskProjectActive(userID, req.ProjectID); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
billingOrder, err := s.taskBillingOrder(userID, &task, normalizedInput)
|
billingOrder, err := s.taskBillingOrder(userID, &task, normalizedInput)
|
||||||
@@ -308,13 +307,10 @@ func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task,
|
|||||||
if billingOrder != nil {
|
if billingOrder != nil {
|
||||||
task.BillingOrderID = billingOrder.ID
|
task.BillingOrderID = billingOrder.ID
|
||||||
}
|
}
|
||||||
err = s.createTaskWithinStorageQuota(&task, billingOrder, policy, domainProjectID, projectTaskLimit)
|
err = s.createTaskWithinStorageQuota(&task, billingOrder, policy)
|
||||||
if errors.Is(err, repository.ErrActiveTaskLimit) {
|
if errors.Is(err, repository.ErrActiveTaskLimit) {
|
||||||
return nil, BadAuthRequest(fmt.Sprintf("同时排队或运行的任务最多 %d 个,请等待已有任务完成", policy.Task.ActiveTaskLimit))
|
return nil, BadAuthRequest(fmt.Sprintf("同时排队或运行的任务最多 %d 个,请等待已有任务完成", policy.Task.ActiveTaskLimit))
|
||||||
}
|
}
|
||||||
if errors.Is(err, repository.ErrProjectTaskLimit) {
|
|
||||||
return nil, BadAuthRequest(fmt.Sprintf("当前项目同时排队或运行的任务最多 %d 个", projectTaskLimit))
|
|
||||||
}
|
|
||||||
if errors.Is(err, repository.ErrInsufficientCredits) {
|
if errors.Is(err, repository.ErrInsufficientCredits) {
|
||||||
return nil, BadAuthRequest("积分不足,请先使用兑换码充值")
|
return nil, BadAuthRequest("积分不足,请先使用兑换码充值")
|
||||||
}
|
}
|
||||||
@@ -419,20 +415,16 @@ func (s *Service) RetryTask(userID string, id string) (*model.Task, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
domainProjectID, projectTaskLimit, err := s.projectTaskLimit(userID, task.ProjectID)
|
if err := s.ensureTaskProjectActive(userID, task.ProjectID); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
task, err = s.repo.RetryTaskWithBilling(userID, task.ID, billingOrder, policy.Task.ActiveTaskLimit, domainProjectID, projectTaskLimit)
|
task, err = s.repo.RetryTaskWithBilling(userID, task.ID, billingOrder, policy.Task.ActiveTaskLimit)
|
||||||
if errors.Is(err, repository.ErrInsufficientCredits) {
|
if errors.Is(err, repository.ErrInsufficientCredits) {
|
||||||
return nil, BadAuthRequest("积分不足,请先使用兑换码充值")
|
return nil, BadAuthRequest("积分不足,请先使用兑换码充值")
|
||||||
}
|
}
|
||||||
if errors.Is(err, repository.ErrActiveTaskLimit) {
|
if errors.Is(err, repository.ErrActiveTaskLimit) {
|
||||||
return nil, BadAuthRequest(fmt.Sprintf("同时排队或运行的任务最多 %d 个,请等待已有任务完成", policy.Task.ActiveTaskLimit))
|
return nil, BadAuthRequest(fmt.Sprintf("同时排队或运行的任务最多 %d 个,请等待已有任务完成", policy.Task.ActiveTaskLimit))
|
||||||
}
|
}
|
||||||
if errors.Is(err, repository.ErrProjectTaskLimit) {
|
|
||||||
return nil, BadAuthRequest(fmt.Sprintf("当前项目同时排队或运行的任务最多 %d 个", projectTaskLimit))
|
|
||||||
}
|
|
||||||
if errors.Is(err, repository.ErrTaskNotRetryable) {
|
if errors.Is(err, repository.ErrTaskNotRetryable) {
|
||||||
return nil, BadAuthRequest("任务已被其他请求重新入队,请勿重复重试")
|
return nil, BadAuthRequest("任务已被其他请求重新入队,请勿重复重试")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func validateStructuredReplacementQuotaWithPolicy(usage repository.UserStorageUs
|
|||||||
return validateStructuredStorageQuotaWithPolicy(usage, kind, false, deltaBytes, policy)
|
return validateStructuredStorageQuotaWithPolicy(usage, kind, false, deltaBytes, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) createTaskWithinStorageQuota(task *model.Task, billingOrder *model.BillingOrder, policy RuntimePolicySetting, domainProjectID string, projectTaskLimit int) error {
|
func (s *Service) createTaskWithinStorageQuota(task *model.Task, billingOrder *model.BillingOrder, policy RuntimePolicySetting) error {
|
||||||
s.storageMu.Lock()
|
s.storageMu.Lock()
|
||||||
defer s.storageMu.Unlock()
|
defer s.storageMu.Unlock()
|
||||||
usage, err := s.repo.UserStorageUsage(task.UserID)
|
usage, err := s.repo.UserStorageUsage(task.UserID)
|
||||||
@@ -86,9 +86,9 @@ func (s *Service) createTaskWithinStorageQuota(task *model.Task, billingOrder *m
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if billingOrder != nil {
|
if billingOrder != nil {
|
||||||
return s.repo.CreateTaskWithCreditReservation(task, billingOrder, policy.Task.ActiveTaskLimit, domainProjectID, projectTaskLimit)
|
return s.repo.CreateTaskWithCreditReservation(task, billingOrder, policy.Task.ActiveTaskLimit)
|
||||||
}
|
}
|
||||||
return s.repo.CreateTaskWithActiveLimit(task, policy.Task.ActiveTaskLimit, domainProjectID, projectTaskLimit)
|
return s.repo.CreateTaskWithActiveLimit(task, policy.Task.ActiveTaskLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 任务完成会同时扩张任务历史和 Agent 会话数据,必须在同一临界区核算并原子写入。
|
// 任务完成会同时扩张任务历史和 Agent 会话数据,必须在同一临界区核算并原子写入。
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const resourceFields: PolicyField[] = [
|
|||||||
const concurrencyFields: PolicyField[] = [
|
const concurrencyFields: PolicyField[] = [
|
||||||
{ group: "task", name: "workerConcurrency", label: "Worker 并发", extra: "集群同时执行的后台任务数。", unit: "个", max: 999 },
|
{ group: "task", name: "workerConcurrency", label: "Worker 并发", extra: "集群同时执行的后台任务数。", unit: "个", max: 999 },
|
||||||
{ group: "task", name: "channelConcurrency", label: "全局渠道并发", extra: "渠道选择跟随系统时采用的并发上限。", unit: "个", max: 999 },
|
{ group: "task", name: "channelConcurrency", label: "全局渠道并发", extra: "渠道选择跟随系统时采用的并发上限。", unit: "个", max: 999 },
|
||||||
{ group: "task", name: "activeTaskLimit", label: "账号活动任务", extra: "单账号同时排队或运行的任务数。", unit: "个", max: 999 },
|
{ group: "task", name: "activeTaskLimit", label: "账号活动任务", extra: "单账号跨项目同时排队或运行的任务总数。", unit: "个", max: 999 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const timeoutFields: PolicyField[] = [
|
const timeoutFields: PolicyField[] = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { App, Button, Input, InputNumber, Modal, Select, Tooltip } from "antd";
|
import { App, Button, Input, Modal, Select, Tooltip } from "antd";
|
||||||
import { Archive, Check, Save, ShieldAlert } from "lucide-react";
|
import { Archive, Check, Save, ShieldAlert } from "lucide-react";
|
||||||
|
|
||||||
import { canvasStylePresets } from "@/components/canvas/canvas-style-picker-modal";
|
import { canvasStylePresets } from "@/components/canvas/canvas-style-picker-modal";
|
||||||
@@ -16,16 +16,15 @@ export default function ProjectSettingsView({ detail, refreshProject }: ProjectD
|
|||||||
const [aspectRatio, setAspectRatio] = useState(project.aspectRatio);
|
const [aspectRatio, setAspectRatio] = useState(project.aspectRatio);
|
||||||
const [sourceType, setSourceType] = useState(project.sourceType);
|
const [sourceType, setSourceType] = useState(project.sourceType);
|
||||||
const [stylePresetId, setStylePresetId] = useState(project.stylePresetId || "");
|
const [stylePresetId, setStylePresetId] = useState(project.stylePresetId || "");
|
||||||
const [activeTaskLimit, setActiveTaskLimit] = useState(project.activeTaskLimit || 3);
|
|
||||||
const [archiveOpen, setArchiveOpen] = useState(false);
|
const [archiveOpen, setArchiveOpen] = useState(false);
|
||||||
useEffect(() => { setName(project.name); setDescription(project.description || ""); setAspectRatio(project.aspectRatio); setSourceType(project.sourceType); setStylePresetId(project.stylePresetId || ""); setActiveTaskLimit(project.activeTaskLimit || 3); }, [project]);
|
useEffect(() => { setName(project.name); setDescription(project.description || ""); setAspectRatio(project.aspectRatio); setSourceType(project.sourceType); setStylePresetId(project.stylePresetId || ""); }, [project]);
|
||||||
const dirty = useMemo(() => name.trim() !== project.name || description !== (project.description || "") || aspectRatio !== project.aspectRatio || sourceType !== project.sourceType || stylePresetId !== (project.stylePresetId || "") || activeTaskLimit !== (project.activeTaskLimit || 3), [activeTaskLimit, aspectRatio, description, name, project, sourceType, stylePresetId]);
|
const dirty = useMemo(() => name.trim() !== project.name || description !== (project.description || "") || aspectRatio !== project.aspectRatio || sourceType !== project.sourceType || stylePresetId !== (project.stylePresetId || ""), [aspectRatio, description, name, project, sourceType, stylePresetId]);
|
||||||
const saveMutation = useMutation({ mutationFn: () => updateProject(project.id, { name: name.trim(), description, aspectRatio, sourceType, stylePresetId, activeTaskLimit }), onSuccess: () => { refreshProject(); message.success("项目设置已保存"); }, onError: (error) => message.error(error instanceof Error ? error.message : "项目设置保存失败") });
|
const saveMutation = useMutation({ mutationFn: () => updateProject(project.id, { name: name.trim(), description, aspectRatio, sourceType, stylePresetId }), onSuccess: () => { refreshProject(); message.success("项目设置已保存"); }, onError: (error) => message.error(error instanceof Error ? error.message : "项目设置保存失败") });
|
||||||
const archiveMutation = useMutation({ mutationFn: () => updateProject(project.id, { status: project.status === "archived" ? "active" : "archived" }), onSuccess: () => { setArchiveOpen(false); refreshProject(); message.success(project.status === "archived" ? "项目已恢复" : "项目已归档"); }, onError: (error) => message.error(error instanceof Error ? error.message : "项目状态更新失败") });
|
const archiveMutation = useMutation({ mutationFn: () => updateProject(project.id, { status: project.status === "archived" ? "active" : "archived" }), onSuccess: () => { setArchiveOpen(false); refreshProject(); message.success(project.status === "archived" ? "项目已恢复" : "项目已归档"); }, onError: (error) => message.error(error instanceof Error ? error.message : "项目状态更新失败") });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<header className="flex items-end justify-between gap-3 border-b border-border/70 pb-3"><div><h2 className="text-lg font-semibold">项目设置</h2><p className="mt-1 text-xs text-foreground/48">基础信息、项目画风与运行限额</p></div><Button type={dirty ? "primary" : "default"} icon={dirty ? <Save className="size-3.5" /> : <Check className="size-3.5" />} disabled={!dirty || !name.trim()} loading={saveMutation.isPending} onClick={() => saveMutation.mutate()}>{dirty ? "保存设置" : "已保存"}</Button></header>
|
<header className="flex items-end justify-between gap-3 border-b border-border/70 pb-3"><div><h2 className="text-lg font-semibold">项目设置</h2><p className="mt-1 text-xs text-foreground/48">基础信息、项目画风与归档管理</p></div><Button type={dirty ? "primary" : "default"} icon={dirty ? <Save className="size-3.5" /> : <Check className="size-3.5" />} disabled={!dirty || !name.trim()} loading={saveMutation.isPending} onClick={() => saveMutation.mutate()}>{dirty ? "保存设置" : "已保存"}</Button></header>
|
||||||
|
|
||||||
<section className="border-b border-border/70 py-4">
|
<section className="border-b border-border/70 py-4">
|
||||||
<h3 className="mb-3 text-sm font-semibold">基础设置</h3>
|
<h3 className="mb-3 text-sm font-semibold">基础设置</h3>
|
||||||
@@ -33,8 +32,7 @@ export default function ProjectSettingsView({ detail, refreshProject }: ProjectD
|
|||||||
<Field label="项目名称" className="xl:col-span-2"><Input value={name} onChange={(event) => setName(event.target.value)} /></Field>
|
<Field label="项目名称" className="xl:col-span-2"><Input value={name} onChange={(event) => setName(event.target.value)} /></Field>
|
||||||
<Field label="默认画幅"><Select className="w-full" value={aspectRatio} options={[{ label: "9:16 · 竖屏短剧", value: "9:16" }, { label: "16:9 · 横屏", value: "16:9" }, { label: "1:1 · 方形", value: "1:1" }]} onChange={setAspectRatio} /></Field>
|
<Field label="默认画幅"><Select className="w-full" value={aspectRatio} options={[{ label: "9:16 · 竖屏短剧", value: "9:16" }, { label: "16:9 · 横屏", value: "16:9" }, { label: "1:1 · 方形", value: "1:1" }]} onChange={setAspectRatio} /></Field>
|
||||||
<Field label="内容来源"><Select className="w-full" value={sourceType} options={[{ label: "空白开始", value: "blank" }, { label: "导入小说", value: "novel" }, { label: "粘贴文本", value: "text" }]} onChange={setSourceType} /></Field>
|
<Field label="内容来源"><Select className="w-full" value={sourceType} options={[{ label: "空白开始", value: "blank" }, { label: "导入小说", value: "novel" }, { label: "粘贴文本", value: "text" }]} onChange={setSourceType} /></Field>
|
||||||
<Field label="项目简介" className="md:col-span-2 xl:col-span-3"><Input value={description} onChange={(event) => setDescription(event.target.value)} placeholder="一句话说明项目目标" /></Field>
|
<Field label="项目简介" className="md:col-span-2 xl:col-span-4"><Input value={description} onChange={(event) => setDescription(event.target.value)} placeholder="一句话说明项目目标" /></Field>
|
||||||
<Field label="活跃任务上限(个)"><InputNumber className="!w-full" min={1} max={20} value={activeTaskLimit} onChange={(value) => setActiveTaskLimit(value || 1)} /></Field>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export type Project = {
|
|||||||
sourceType: string;
|
sourceType: string;
|
||||||
description: string;
|
description: string;
|
||||||
stylePresetId: string;
|
stylePresetId: string;
|
||||||
activeTaskLimit: number;
|
|
||||||
status: "active" | "archived" | string;
|
status: "active" | "archived" | string;
|
||||||
revision: number;
|
revision: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -187,11 +186,11 @@ export function getProject(id: string) {
|
|||||||
return request<ProjectDetail>(api.get(`/projects/${encodeURIComponent(id)}`));
|
return request<ProjectDetail>(api.get(`/projects/${encodeURIComponent(id)}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProject(input: { name: string; type: string; aspectRatio: string; sourceType: string; description?: string; stylePresetId?: string; activeTaskLimit?: number }) {
|
export function createProject(input: { name: string; type: string; aspectRatio: string; sourceType: string; description?: string; stylePresetId?: string }) {
|
||||||
return request<{ project: Project }>(api.post("/projects", input));
|
return request<{ project: Project }>(api.post("/projects", input));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateProject(projectId: string, input: Partial<Pick<Project, "name" | "type" | "aspectRatio" | "sourceType" | "description" | "stylePresetId" | "activeTaskLimit" | "status">>) {
|
export function updateProject(projectId: string, input: Partial<Pick<Project, "name" | "type" | "aspectRatio" | "sourceType" | "description" | "stylePresetId" | "status">>) {
|
||||||
return request<{ project: Project }>(api.patch(`/projects/${encodeURIComponent(projectId)}`, input));
|
return request<{ project: Project }>(api.patch(`/projects/${encodeURIComponent(projectId)}`, input));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user