初始化
This commit is contained in:
@@ -0,0 +1,502 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"juhe-factory/api/internal/billing"
|
||||
"juhe-factory/api/internal/model"
|
||||
"juhe-factory/api/internal/provider/apimart"
|
||||
queuepkg "juhe-factory/api/internal/queue"
|
||||
"juhe-factory/api/internal/security"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hibiken/asynq"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type ScriptAnalysis struct {
|
||||
DB *gorm.DB
|
||||
Queue *asynq.Client
|
||||
Encryptor *security.Encryptor
|
||||
Provider apimart.Client
|
||||
}
|
||||
|
||||
type scriptAnalysisSnapshot struct {
|
||||
Task model.GenerationTask
|
||||
AnalysisID uuid.UUID
|
||||
SourceContent string
|
||||
ModelName string
|
||||
BaseURL string
|
||||
APIKeyCiphertext string
|
||||
}
|
||||
|
||||
type scriptHighlight struct {
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type scriptEmotionStage struct {
|
||||
Stage string `json:"stage"`
|
||||
Emotion string `json:"emotion"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type scriptEpisodeOutline struct {
|
||||
EpisodeNo int `json:"episode_no"`
|
||||
Title string `json:"title"`
|
||||
Outline string `json:"outline"`
|
||||
Emotion string `json:"emotion"`
|
||||
}
|
||||
|
||||
type scriptAnalysisCharacter struct {
|
||||
Name string `json:"name"`
|
||||
Faction string `json:"faction"`
|
||||
Biography string `json:"biography"`
|
||||
Motivation string `json:"motivation"`
|
||||
Relationship string `json:"relationship"`
|
||||
}
|
||||
|
||||
type scriptEpisodeHook struct {
|
||||
EpisodeNo int `json:"episode_no"`
|
||||
HookType string `json:"hook_type"`
|
||||
Hook string `json:"hook"`
|
||||
Basis string `json:"basis"`
|
||||
}
|
||||
|
||||
type scriptPaywallHook struct {
|
||||
HookType string `json:"hook_type"`
|
||||
Content string `json:"content"`
|
||||
Appeal string `json:"appeal"`
|
||||
}
|
||||
|
||||
type scriptCustomAnalysis struct {
|
||||
Objective string `json:"objective"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
Evidence []string `json:"evidence"`
|
||||
}
|
||||
|
||||
type scriptUnmatchedObjective struct {
|
||||
Objective string `json:"objective"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type scriptAnalysisResult struct {
|
||||
Tags struct {
|
||||
DramaTags []string `json:"drama_tags"`
|
||||
ApplicableSceneTags []string `json:"applicable_scene_tags"`
|
||||
} `json:"tags"`
|
||||
SellingPoint struct {
|
||||
Summary string `json:"summary"`
|
||||
Highlights []scriptHighlight `json:"highlights"`
|
||||
} `json:"selling_point"`
|
||||
StoryStructure struct {
|
||||
Synopsis string `json:"synopsis"`
|
||||
StoryOutline []string `json:"story_outline"`
|
||||
EmotionOutline []scriptEmotionStage `json:"emotion_outline"`
|
||||
EpisodeOutlines []scriptEpisodeOutline `json:"episode_outlines"`
|
||||
Characters []scriptAnalysisCharacter `json:"characters"`
|
||||
} `json:"story_structure"`
|
||||
OpeningHook struct {
|
||||
FirstScene string `json:"first_scene"`
|
||||
FirstLine string `json:"first_line"`
|
||||
AttentionMethods []string `json:"attention_methods"`
|
||||
Analysis string `json:"analysis"`
|
||||
Optimization string `json:"optimization"`
|
||||
} `json:"opening_hook"`
|
||||
EpisodeHooks []scriptEpisodeHook `json:"episode_hooks"`
|
||||
Episode10PaywallHooks []scriptPaywallHook `json:"episode_10_paywall_hooks"`
|
||||
CustomAnalysis []scriptCustomAnalysis `json:"custom_analysis"`
|
||||
UnmatchedObjectives []scriptUnmatchedObjective `json:"unmatched_objectives"`
|
||||
}
|
||||
|
||||
func NewScriptAnalysis(db *gorm.DB, queue *asynq.Client, encryptor *security.Encryptor, client *http.Client) *ScriptAnalysis {
|
||||
return &ScriptAnalysis{DB: db, Queue: queue, Encryptor: encryptor, Provider: apimart.NewClient(dramaHTTPClient(client))}
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) Register(mux *asynq.ServeMux) {
|
||||
mux.HandleFunc(queuepkg.TypeAnalyzeScript, w.analyze)
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) Recover(ctx context.Context) error {
|
||||
if err := w.DB.WithContext(ctx).Model(&model.GenerationTask{}).
|
||||
Where("task_type='script_analysis' AND status='processing'").Update("status", "submitted").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var ids []uuid.UUID
|
||||
if err := w.DB.WithContext(ctx).Model(&model.GenerationTask{}).
|
||||
Where("task_type='script_analysis' AND status='submitted'").Pluck("id", &ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var firstErr error
|
||||
for _, id := range ids {
|
||||
if err := queuepkg.EnqueueID(w.Queue, queuepkg.TypeAnalyzeScript, id, 0); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
var cancelledIDs []uuid.UUID
|
||||
if err := w.DB.WithContext(ctx).Model(&model.GenerationTask{}).
|
||||
Where("task_type='script_analysis' AND status='cancel_requested'").Pluck("id", &cancelledIDs).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range cancelledIDs {
|
||||
if err := w.finishCancellation(ctx, id, uuid.Nil); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) analyze(ctx context.Context, raw *asynq.Task) error {
|
||||
taskID, err := decodeID(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshot, err := w.claim(ctx, taskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return w.fail(ctx, taskID, uuid.Nil, err)
|
||||
}
|
||||
apiKey, err := w.Encryptor.Decrypt(snapshot.APIKeyCiphertext)
|
||||
if err != nil {
|
||||
return w.fail(ctx, taskID, snapshot.AnalysisID, errors.New("剧本分析渠道密钥不可用"))
|
||||
}
|
||||
workCtx, cancel := context.WithTimeout(ctx, 12*time.Minute)
|
||||
defer cancel()
|
||||
var cancelled atomic.Bool
|
||||
go w.watchCancellation(workCtx, cancel, taskID, &cancelled)
|
||||
result, chat, inputTokens, outputTokens, tokenSource, err := w.callModel(workCtx, snapshot, apiKey)
|
||||
if cancelled.Load() || w.cancellationRequested(ctx, taskID) {
|
||||
return w.finishCancellation(ctx, taskID, snapshot.AnalysisID)
|
||||
}
|
||||
if err != nil {
|
||||
return w.fail(ctx, taskID, snapshot.AnalysisID, err)
|
||||
}
|
||||
if err := w.persist(ctx, snapshot, result, chat, inputTokens, outputTokens, tokenSource); err != nil {
|
||||
return w.fail(ctx, taskID, snapshot.AnalysisID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) watchCancellation(ctx context.Context, cancel context.CancelFunc, taskID uuid.UUID, flag *atomic.Bool) {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if w.cancellationRequested(ctx, taskID) {
|
||||
flag.Store(true)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) cancellationRequested(ctx context.Context, taskID uuid.UUID) bool {
|
||||
var status string
|
||||
return w.DB.WithContext(ctx).Model(&model.GenerationTask{}).Where("id=?", taskID).Pluck("status", &status).Error == nil &&
|
||||
status == "cancel_requested"
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) claim(ctx context.Context, taskID uuid.UUID) (scriptAnalysisSnapshot, error) {
|
||||
var snapshot scriptAnalysisSnapshot
|
||||
err := w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id=? AND task_type='script_analysis' AND status='submitted'", taskID).Take(&snapshot.Task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if snapshot.Task.ScriptAnalysisID == nil {
|
||||
return errors.New("剧本分析任务缺少剧本标识")
|
||||
}
|
||||
snapshot.AnalysisID = *snapshot.Task.ScriptAnalysisID
|
||||
var input struct {
|
||||
SourceContent string `json:"source_content"`
|
||||
}
|
||||
if err := json.Unmarshal(snapshot.Task.InputData, &input); err != nil || strings.TrimSpace(input.SourceContent) == "" {
|
||||
return errors.New("剧本分析原文快照无效")
|
||||
}
|
||||
snapshot.SourceContent = input.SourceContent
|
||||
var config struct {
|
||||
ModelName string
|
||||
BaseURL string
|
||||
APIKeyCiphertext string
|
||||
}
|
||||
result := tx.Raw(`SELECT model.name AS model_name,channel.base_url,channel.api_key_ciphertext
|
||||
FROM models model JOIN channels channel ON channel.id=model.channel_id
|
||||
WHERE model.id=? AND channel.id=? AND model.model_type='text'
|
||||
AND model.enabled=true AND model.deleted_at IS NULL AND channel.enabled=true AND channel.deleted_at IS NULL`,
|
||||
snapshot.Task.ModelID, snapshot.Task.ChannelID).Scan(&config)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("剧本分析模型或渠道不可用")
|
||||
}
|
||||
snapshot.ModelName, snapshot.BaseURL, snapshot.APIKeyCiphertext = config.ModelName, config.BaseURL, config.APIKeyCiphertext
|
||||
if err := tx.Model(&snapshot.Task).Update("status", "processing").Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.ScriptAnalysis{}).Where("id=?", snapshot.AnalysisID).
|
||||
Updates(map[string]any{"analysis_status": "running", "analysis_message": "正在分析剧本"}).Error
|
||||
})
|
||||
return snapshot, err
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) callModel(ctx context.Context, snapshot scriptAnalysisSnapshot, apiKey string) (scriptAnalysisResult, apimart.ChatResult, int64, int64, string, error) {
|
||||
var parsed scriptAnalysisResult
|
||||
var chat apimart.ChatResult
|
||||
pricing, err := billing.ParseTextPricingSnapshot(snapshot.Task.BillingSnapshot)
|
||||
if err != nil {
|
||||
return parsed, chat, 0, 0, "", err
|
||||
}
|
||||
systemPrompt := scriptAnalysisSystemPrompt(snapshot.Task.PromptSnapshot)
|
||||
userPrompt := "请分析以下剧本原文:\n\n" + snapshot.SourceContent
|
||||
if pricing.Mode == billing.TextBillingPerToken {
|
||||
estimated := billing.EstimateTextTokens(systemPrompt + userPrompt)
|
||||
if err := w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
_, reserveErr := billing.ReserveTextCall(tx, snapshot.Task.UserID, snapshot.Task.ID.String(), "analysis", pricing, estimated, "剧本分析")
|
||||
return reserveErr
|
||||
}); err != nil {
|
||||
return parsed, chat, 0, 0, "", err
|
||||
}
|
||||
}
|
||||
payload := map[string]any{
|
||||
"model": snapshot.ModelName,
|
||||
"messages": []map[string]any{
|
||||
{"role": "system", "content": systemPrompt},
|
||||
{"role": "user", "content": userPrompt},
|
||||
},
|
||||
"temperature": 0.2,
|
||||
"response_format": map[string]any{"type": "json_object"},
|
||||
}
|
||||
var parseErr error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if attempt > 0 {
|
||||
payload["temperature"] = 0
|
||||
payload["messages"] = []map[string]any{
|
||||
{"role": "system", "content": systemPrompt},
|
||||
{"role": "user", "content": userPrompt + "\n\n上次返回未通过结构校验。请严格补齐固定协议的全部字段,并按分析目标决定标准字段、补充分析或无法完成的目标,只返回完整合法 JSON。"},
|
||||
}
|
||||
}
|
||||
chat, err = w.Provider.ChatWithUsage(ctx, snapshot.BaseURL, apiKey, payload)
|
||||
if err != nil {
|
||||
return parsed, chat, 0, 0, "", err
|
||||
}
|
||||
parsed = scriptAnalysisResult{}
|
||||
parseErr = json.Unmarshal([]byte(stripJSONFence(chat.Content)), &parsed)
|
||||
if parseErr == nil {
|
||||
parseErr = validateScriptAnalysisResult(parsed)
|
||||
}
|
||||
if parseErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if parseErr != nil {
|
||||
finishReason := strings.TrimSpace(chat.FinishReason)
|
||||
if finishReason == "" {
|
||||
finishReason = "未知"
|
||||
}
|
||||
return parsed, chat, 0, 0, "", fmt.Errorf("文本模型连续两次返回无法解析(结束原因:%s): %w", finishReason, parseErr)
|
||||
}
|
||||
inputTokens, outputTokens := chat.InputTokens, chat.OutputTokens
|
||||
tokenSource := "upstream"
|
||||
if inputTokens == 0 {
|
||||
inputTokens = billing.EstimateTextTokens(systemPrompt + userPrompt)
|
||||
tokenSource = "local"
|
||||
}
|
||||
if outputTokens == 0 {
|
||||
outputTokens = billing.EstimateTextTokens(chat.Content)
|
||||
if tokenSource == "upstream" {
|
||||
tokenSource = "mixed"
|
||||
}
|
||||
}
|
||||
if pricing.Mode == billing.TextBillingPerToken {
|
||||
if err := w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
settled, settleErr := billing.SettleTextCall(tx, snapshot.Task.UserID, snapshot.Task.ID.String(), "analysis", pricing,
|
||||
billing.TextUsage{Input: inputTokens, Output: outputTokens}, "剧本分析")
|
||||
if settleErr != nil {
|
||||
return settleErr
|
||||
}
|
||||
return tx.Model(&model.GenerationTask{}).Where("id=?", snapshot.Task.ID).Updates(map[string]any{
|
||||
"estimated_points": gorm.Expr("estimated_points+?::numeric", settled),
|
||||
"prepaid_points": gorm.Expr("prepaid_points+?::numeric", settled),
|
||||
}).Error
|
||||
}); err != nil {
|
||||
return parsed, chat, inputTokens, outputTokens, tokenSource, err
|
||||
}
|
||||
}
|
||||
return parsed, chat, inputTokens, outputTokens, tokenSource, nil
|
||||
}
|
||||
|
||||
func validateScriptAnalysisResult(result scriptAnalysisResult) error {
|
||||
if strings.TrimSpace(result.SellingPoint.Summary) == "" && strings.TrimSpace(result.StoryStructure.Synopsis) == "" &&
|
||||
len(result.CustomAnalysis) == 0 && len(result.UnmatchedObjectives) == 0 {
|
||||
return errors.New("缺少标准分析结果、通用分析结果或无法匹配目标说明")
|
||||
}
|
||||
if len(result.EpisodeHooks) != 0 && len(result.EpisodeHooks) != 10 {
|
||||
return errors.New("前十集钩子数量必须为10")
|
||||
}
|
||||
for index, hook := range result.EpisodeHooks {
|
||||
if hook.EpisodeNo != index+1 {
|
||||
return errors.New("前十集钩子集数或顺序无效")
|
||||
}
|
||||
}
|
||||
if len(result.Episode10PaywallHooks) != 0 &&
|
||||
(len(result.Episode10PaywallHooks) < 3 || len(result.Episode10PaywallHooks) > 5) {
|
||||
return errors.New("第十集付费卡点候选必须为3至5个")
|
||||
}
|
||||
for _, character := range result.StoryStructure.Characters {
|
||||
if strings.TrimSpace(character.Name) == "" {
|
||||
return errors.New("人物名称不能为空")
|
||||
}
|
||||
if character.Faction != "正派" && character.Faction != "反派" && character.Faction != "中立派" {
|
||||
return errors.New("人物阵营无效")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) persist(ctx context.Context, snapshot scriptAnalysisSnapshot, result scriptAnalysisResult, chat apimart.ChatResult, inputTokens, outputTokens int64, tokenSource string) error {
|
||||
encoded, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pretty, _ := json.MarshalIndent(result, "", " ")
|
||||
return w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var task model.GenerationTask
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", snapshot.Task.ID).Take(&task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if task.Status == "cancel_requested" {
|
||||
return w.finishCancellationLocked(tx, &task, snapshot.AnalysisID)
|
||||
}
|
||||
if task.Status != "processing" {
|
||||
return errors.New("剧本分析任务状态已变化")
|
||||
}
|
||||
if err := tx.Where("script_analysis_id=?", snapshot.AnalysisID).Delete(&model.ScriptAnalysisCharacter{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
for index, character := range result.StoryStructure.Characters {
|
||||
name := strings.TrimSpace(character.Name)
|
||||
key := strings.ToLower(name)
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
item := model.ScriptAnalysisCharacter{
|
||||
ScriptAnalysisID: snapshot.AnalysisID, Name: name, Faction: character.Faction,
|
||||
Biography: strings.TrimSpace(character.Biography), Motivation: strings.TrimSpace(character.Motivation),
|
||||
Relationship: strings.TrimSpace(character.Relationship), SortOrder: index,
|
||||
}
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
taskUpdates := map[string]any{
|
||||
"status": "succeeded", "finished_at": time.Now(), "actual_points": task.PrepaidPoints,
|
||||
"input_tokens": inputTokens, "output_tokens": outputTokens, "total_tokens": inputTokens + outputTokens,
|
||||
"token_count_source": tokenSource, "usage_raw": chat.UsageRaw, "result_data": gorm.Expr("?::jsonb", string(encoded)),
|
||||
"error_code": nil, "error_message": nil,
|
||||
}
|
||||
if err := tx.Model(&task).Updates(taskUpdates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.ScriptAnalysis{}).Where("id=?", snapshot.AnalysisID).Updates(map[string]any{
|
||||
"analysis_result": gorm.Expr("?::jsonb", string(encoded)), "result_content": string(pretty),
|
||||
"analysis_status": "succeeded", "analysis_message": "剧本分析完成",
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) fail(ctx context.Context, taskID, analysisID uuid.UUID, taskErr error) error {
|
||||
return w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var task model.GenerationTask
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", taskID).Take(&task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if task.Status == "succeeded" || task.Status == "failed" || task.Status == "cancelled" {
|
||||
return nil
|
||||
}
|
||||
if task.Status == "cancel_requested" {
|
||||
return w.finishCancellationLocked(tx, &task, analysisID)
|
||||
}
|
||||
refunded, err := billing.RefundTextGenerationTask(tx, &task, "剧本分析失败返还")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updates := map[string]any{
|
||||
"status": "failed", "actual_points": "0.00", "finished_at": time.Now(),
|
||||
"error_code": "script_analysis_failed", "error_message": truncate(taskErr.Error(), 4000),
|
||||
}
|
||||
if refunded {
|
||||
updates["cost_refunded"] = true
|
||||
}
|
||||
if err := tx.Model(&task).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if analysisID == uuid.Nil && task.ScriptAnalysisID != nil {
|
||||
analysisID = *task.ScriptAnalysisID
|
||||
}
|
||||
if analysisID == uuid.Nil {
|
||||
return nil
|
||||
}
|
||||
return tx.Model(&model.ScriptAnalysis{}).Where("id=?", analysisID).Updates(map[string]any{
|
||||
"analysis_status": "failed", "analysis_message": truncate(taskErr.Error(), 1000),
|
||||
}).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) finishCancellation(ctx context.Context, taskID, analysisID uuid.UUID) error {
|
||||
return w.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var task model.GenerationTask
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id=?", taskID).Take(&task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return w.finishCancellationLocked(tx, &task, analysisID)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *ScriptAnalysis) finishCancellationLocked(tx *gorm.DB, task *model.GenerationTask, analysisID uuid.UUID) error {
|
||||
if task.Status == "cancelled" {
|
||||
return nil
|
||||
}
|
||||
if task.Status == "succeeded" || task.Status == "failed" {
|
||||
return nil
|
||||
}
|
||||
actual := task.PrepaidPoints
|
||||
committed, err := billing.CommitTextReserves(tx, task.UserID, task.ID.String(), "剧本分析取消")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if committed != "" && committed != "0" && committed != "0.00" {
|
||||
actual = committed
|
||||
}
|
||||
if err := tx.Model(task).Updates(map[string]any{
|
||||
"status": "cancelled", "prepaid_points": actual, "actual_points": actual, "finished_at": time.Now(),
|
||||
"error_code": "user_cancelled", "error_message": "用户取消,已扣积分不退",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if analysisID == uuid.Nil && task.ScriptAnalysisID != nil {
|
||||
analysisID = *task.ScriptAnalysisID
|
||||
}
|
||||
if analysisID == uuid.Nil {
|
||||
return nil
|
||||
}
|
||||
return tx.Model(&model.ScriptAnalysis{}).Where("id=?", analysisID).Updates(map[string]any{
|
||||
"analysis_status": "idle", "analysis_message": "剧本分析已取消,已扣积分不退",
|
||||
}).Error
|
||||
}
|
||||
Reference in New Issue
Block a user