初始化
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"juhe-factory/api/internal/billing"
|
||||
"juhe-factory/api/internal/model"
|
||||
queuepkg "juhe-factory/api/internal/queue"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type ScriptAnalysisInput struct {
|
||||
Name string `json:"name"`
|
||||
SourceContent string `json:"source_content"`
|
||||
ResultContent string `json:"result_content"`
|
||||
}
|
||||
|
||||
type ScriptAnalysisImportProject struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
EpisodeCount int `json:"episode_count"`
|
||||
CharCount int `json:"char_count"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s *Creative) ListScriptAnalyses(userID uuid.UUID) ([]model.ScriptAnalysis, error) {
|
||||
items := make([]model.ScriptAnalysis, 0)
|
||||
err := s.DB.Where("user_id=? AND deleted_at IS NULL", userID).Order("updated_at DESC").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *Creative) CreateScriptAnalysis(userID uuid.UUID) (*model.ScriptAnalysis, error) {
|
||||
item := &model.ScriptAnalysis{UserID: userID, Name: "未命名剧本", AnalysisResult: json.RawMessage(`{}`), AnalysisStatus: "idle"}
|
||||
if err := s.DB.Create(item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Creative) ListScriptAnalysisImportProjects(userID uuid.UUID) ([]ScriptAnalysisImportProject, error) {
|
||||
items := make([]ScriptAnalysisImportProject, 0)
|
||||
err := s.DB.Table("creative_projects project").
|
||||
Select(`project.id,project.name,project.updated_at,
|
||||
count(episode.id) FILTER (WHERE episode.deleted_at IS NULL AND episode.status='review' AND btrim(coalesce(episode.redraw_script,'')) <> '') AS episode_count,
|
||||
coalesce(sum(char_length(episode.redraw_script)) FILTER (WHERE episode.deleted_at IS NULL AND episode.status='review' AND btrim(coalesce(episode.redraw_script,'')) <> ''),0) AS char_count`).
|
||||
Joins("LEFT JOIN project_episodes episode ON episode.project_id=project.id").
|
||||
Where(`project.user_id=? AND project.project_type='video_redraw' AND project.deleted_at IS NULL AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM project_episodes source_episode
|
||||
WHERE source_episode.project_id=project.id AND source_episode.deleted_at IS NULL
|
||||
AND source_episode.status='review' AND btrim(coalesce(source_episode.redraw_script,'')) <> ''
|
||||
)`, userID).
|
||||
Group("project.id").Order("project.updated_at DESC").Scan(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *Creative) ImportScriptAnalysisProject(userID, analysisID, projectID uuid.UUID) (*model.ScriptAnalysis, error) {
|
||||
err := s.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var project model.CreativeProject
|
||||
if err := tx.Select("id", "name").
|
||||
Where("id=? AND user_id=? AND project_type='video_redraw' AND deleted_at IS NULL", projectID, userID).
|
||||
Take(&project).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var episodes []model.ProjectEpisode
|
||||
if err := tx.Select("episode_no", "redraw_script").
|
||||
Where("project_id=? AND deleted_at IS NULL AND status='review' AND btrim(coalesce(redraw_script,'')) <> ''", projectID).
|
||||
Order("episode_no").Find(&episodes).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(episodes) == 0 {
|
||||
return errors.New("该项目暂无可导入的剧本")
|
||||
}
|
||||
parts := make([]string, 0, len(episodes))
|
||||
for _, episode := range episodes {
|
||||
parts = append(parts, fmt.Sprintf("【第%d集】\n%s", episode.EpisodeNo, strings.TrimSpace(episode.RedrawScript)))
|
||||
}
|
||||
name := project.Name
|
||||
return replaceScriptAnalysisSource(tx, userID, analysisID, name, strings.Join(parts, "\n\n"))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetScriptAnalysis(userID, analysisID)
|
||||
}
|
||||
|
||||
func (s *Creative) ImportScriptAnalysisFile(userID, analysisID uuid.UUID, filename string, data []byte) (*model.ScriptAnalysis, error) {
|
||||
extension := strings.ToLower(filepath.Ext(filename))
|
||||
if extension != ".txt" && extension != ".doc" {
|
||||
return nil, errors.New("仅支持TXT和DOC文件")
|
||||
}
|
||||
if len(data) > maxDramaImportBytes {
|
||||
return nil, errors.New("剧本文件不能超过3MB")
|
||||
}
|
||||
content, _, err := parseDramaSource(filename, data, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content = normalizeDramaText(content)
|
||||
if content == "" {
|
||||
return nil, errors.New("未读取到有效剧本正文")
|
||||
}
|
||||
name := strings.TrimSpace(strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)))
|
||||
if name == "" {
|
||||
name = "未命名剧本"
|
||||
}
|
||||
if err := s.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return replaceScriptAnalysisSource(tx, userID, analysisID, name, content)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetScriptAnalysis(userID, analysisID)
|
||||
}
|
||||
|
||||
func replaceScriptAnalysisSource(tx *gorm.DB, userID, analysisID uuid.UUID, name, content string) error {
|
||||
var item model.ScriptAnalysis
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id=? AND user_id=? AND deleted_at IS NULL", analysisID, userID).Take(&item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if item.AnalysisStatus == "queued" || item.AnalysisStatus == "running" {
|
||||
return errors.New("剧本正在分析,暂时不能导入")
|
||||
}
|
||||
if err := tx.Where("script_analysis_id=?", analysisID).Delete(&model.ScriptAnalysisCharacter{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&item).Updates(map[string]any{
|
||||
"name": strings.TrimSpace(name),
|
||||
"source_content": content, "result_content": "",
|
||||
"analysis_result": json.RawMessage(`{}`), "analysis_status": "idle", "analysis_message": "",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (s *Creative) GetScriptAnalysis(userID, id uuid.UUID) (*model.ScriptAnalysis, error) {
|
||||
var item model.ScriptAnalysis
|
||||
if err := s.DB.Preload("Characters", func(db *gorm.DB) *gorm.DB { return db.Order("sort_order,name") }).
|
||||
Where("id=? AND user_id=? AND deleted_at IS NULL", id, userID).Take(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
type scriptAnalysisModelConfig struct {
|
||||
ModelID uuid.UUID
|
||||
ChannelID uuid.UUID
|
||||
ModelName string
|
||||
BaseURL string
|
||||
APIKeyCiphertext string
|
||||
Pricing billing.TextPricing `gorm:"-"`
|
||||
}
|
||||
|
||||
func (s *Creative) scriptAnalysisModel(userID uuid.UUID) (scriptAnalysisModelConfig, error) {
|
||||
var config scriptAnalysisModelConfig
|
||||
result := s.DB.Raw(`SELECT model.id AS model_id,channel.id AS channel_id,model.name AS model_name,
|
||||
channel.base_url,channel.api_key_ciphertext
|
||||
FROM user_model_configs preference
|
||||
JOIN models model ON model.id=preference.model_id AND model.model_type='text' AND model.enabled=true AND model.deleted_at IS NULL
|
||||
JOIN channels channel ON channel.id=model.channel_id AND channel.enabled=true AND channel.deleted_at IS NULL
|
||||
WHERE preference.user_id=? AND preference.project_type='script_analysis' AND preference.model_type='text'`, userID).Scan(&config)
|
||||
if result.Error != nil {
|
||||
return config, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return config, errors.New("请先配置剧本分析文本模型")
|
||||
}
|
||||
pricing, err := billing.LoadTextPricing(s.DB, config.ModelID)
|
||||
if err != nil {
|
||||
return config, errors.New("剧本分析文本模型计费配置无效")
|
||||
}
|
||||
config.Pricing = pricing
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (s *Creative) QueueScriptAnalysis(userID, analysisID uuid.UUID) (*model.GenerationTask, error) {
|
||||
if s.Queue == nil {
|
||||
return nil, errors.New("剧本分析任务队列不可用")
|
||||
}
|
||||
config, err := s.scriptAnalysisModel(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task := &model.GenerationTask{
|
||||
RequestID: "script_analysis_" + uuid.NewString(), UserID: userID, ChannelID: &config.ChannelID,
|
||||
ModelID: &config.ModelID, ScriptAnalysisID: &analysisID, TaskType: "script_analysis", Status: "submitted",
|
||||
EstimatedPoints: "0.00", PrepaidPoints: "0.00",
|
||||
}
|
||||
err = s.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var item model.ScriptAnalysis
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id=? AND user_id=? AND deleted_at IS NULL", analysisID, userID).Take(&item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(item.SourceContent) == "" {
|
||||
return errors.New("请先填写剧本原文")
|
||||
}
|
||||
var active int64
|
||||
if err := tx.Model(&model.GenerationTask{}).
|
||||
Where("script_analysis_id=? AND status IN ?", analysisID, activeTaskStatuses).Count(&active).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if active > 0 {
|
||||
return errors.New("当前剧本正在分析,请勿重复提交")
|
||||
}
|
||||
prompt, err := selectedPromptContent(tx, userID, "剧本分析")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.PromptSnapshot = prompt
|
||||
task.InputData = mustJSON(map[string]string{"source_content": item.SourceContent})
|
||||
if err := billing.CreateTextGenerationTask(tx, task, config.Pricing, "剧本分析"); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&item).Updates(map[string]any{"analysis_status": "queued", "analysis_message": "剧本分析排队中"}).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := queuepkg.EnqueueID(s.Queue, queuepkg.TypeAnalyzeScript, task.ID, 0); err != nil {
|
||||
if settleErr := s.failQueuedTask(task.ID, "queue_unavailable", "剧本分析任务入队失败"); settleErr != nil {
|
||||
return nil, fmt.Errorf("剧本分析任务入队失败且预扣返还失败: %w", settleErr)
|
||||
}
|
||||
_ = s.DB.Model(&model.ScriptAnalysis{}).Where("id=?", analysisID).
|
||||
Updates(map[string]any{"analysis_status": "failed", "analysis_message": "剧本分析任务入队失败"}).Error
|
||||
return nil, errors.New("剧本分析任务队列暂时不可用,请稍后重试")
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (s *Creative) CancelScriptAnalysis(userID, analysisID uuid.UUID) error {
|
||||
return s.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var task model.GenerationTask
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("script_analysis_id=? AND user_id=? AND task_type='script_analysis' AND status IN ?", analysisID, userID,
|
||||
[]string{"submitted", "processing", "cancel_requested"}).
|
||||
Order("created_at DESC").Take(&task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if task.Status == "cancel_requested" {
|
||||
return nil
|
||||
}
|
||||
if task.Status == "submitted" {
|
||||
if err := tx.Model(&task).Updates(map[string]any{
|
||||
"status": "cancelled", "actual_points": task.PrepaidPoints, "finished_at": time.Now(),
|
||||
"error_code": "user_cancelled", "error_message": "用户取消,已扣积分不退",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.ScriptAnalysis{}).Where("id=? AND user_id=? AND deleted_at IS NULL", analysisID, userID).Updates(map[string]any{
|
||||
"analysis_status": "idle", "analysis_message": "剧本分析已取消,已扣积分不退",
|
||||
}).Error
|
||||
}
|
||||
if err := tx.Model(&task).Updates(map[string]any{
|
||||
"status": "cancel_requested", "error_code": "user_cancelled", "error_message": "正在取消分析",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.ScriptAnalysis{}).Where("id=? AND user_id=? AND deleted_at IS NULL", analysisID, userID).
|
||||
Update("analysis_message", "正在取消分析").Error
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Creative) UpdateScriptAnalysis(userID, id uuid.UUID, input ScriptAnalysisInput) (*model.ScriptAnalysis, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
if name == "" {
|
||||
return nil, errors.New("剧本名称不能为空")
|
||||
}
|
||||
result := s.DB.Model(&model.ScriptAnalysis{}).
|
||||
Where("id=? AND user_id=? AND deleted_at IS NULL", id, userID).
|
||||
Updates(map[string]any{"name": name, "source_content": input.SourceContent, "result_content": input.ResultContent})
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return s.GetScriptAnalysis(userID, id)
|
||||
}
|
||||
|
||||
func (s *Creative) DeleteScriptAnalysis(userID, id uuid.UUID) error {
|
||||
result := s.DB.Model(&model.ScriptAnalysis{}).
|
||||
Where("id=? AND user_id=? AND deleted_at IS NULL", id, userID).
|
||||
Update("deleted_at", gorm.Expr("CURRENT_TIMESTAMP"))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user