feat: integrate creator hub douyin workflows

This commit is contained in:
2026-09-14 19:22:07 +08:00
parent 025fe62c37
commit 44a28954cf
42 changed files with 2850 additions and 880 deletions
+119
View File
@@ -3,6 +3,7 @@ package creator
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/url"
@@ -184,6 +185,26 @@ func (s *Store) ListDueCompetitors(ctx context.Context, now time.Time) ([]Compet
return result, rows.Err()
}
func (s *Store) validateWorkSource(ctx context.Context, platform, sourceType, sourceID string) error {
var query string
switch sourceType {
case SourceOwned:
query = `SELECT 1 FROM social_account WHERE id=$1 AND platform=$2`
case SourceCompetitor:
query = `SELECT 1 FROM creator_competitor WHERE id=$1 AND platform=$2`
default:
return ErrInvalid
}
var exists int
if err := s.db.QueryRowContext(ctx, query, sourceID, platform).Scan(&exists); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ErrInvalid
}
return databaseError(err)
}
return nil
}
func (s *Store) UpsertWork(ctx context.Context, input WorkInput, now time.Time) (Work, bool, error) {
input.Platform = strings.TrimSpace(input.Platform)
input.WorkKey = strings.TrimSpace(input.WorkKey)
@@ -200,6 +221,9 @@ func (s *Store) UpsertWork(ctx context.Context, input WorkInput, now time.Time)
utf8.RuneCountInString(input.CoverURL) > 2000 {
return Work{}, false, ErrInvalid
}
if err := s.validateWorkSource(ctx, input.Platform, input.SourceType, input.SourceID); err != nil {
return Work{}, false, err
}
if now.IsZero() {
now = time.Now().UTC()
}
@@ -532,6 +556,59 @@ func (s *Store) ConfirmRewrite(ctx context.Context, workID, requirement string)
return s.GetMaterial(ctx, workID)
}
func parseGeneratedRewrite(content string) (string, string, error) {
var result struct {
Title string `json:"title"`
Script string `json:"script"`
}
if err := json.Unmarshal([]byte(strings.TrimSpace(content)), &result); err != nil {
return "", "", fmt.Errorf("decode generated rewrite: %w", err)
}
result.Title, result.Script = strings.TrimSpace(result.Title), strings.TrimSpace(result.Script)
if result.Title == "" || result.Script == "" || utf8.RuneCountInString(result.Title) > 1000 || utf8.RuneCountInString(result.Script) > 100000 {
return "", "", ErrInvalid
}
return result.Title, result.Script, nil
}
func (s *Store) GenerateRewrite(ctx context.Context, workID string, generator TextGenerator) (MaterialJob, error) {
if generator == nil {
return MaterialJob{}, fmt.Errorf("%w: text generator is not configured", ErrUnavailable)
}
settings, err := s.GetSettings(ctx)
if err != nil {
return MaterialJob{}, err
}
if !settings.AIConfigured || strings.TrimSpace(settings.AIProvider) == "" || strings.TrimSpace(settings.AIModel) == "" {
return MaterialJob{}, fmt.Errorf("%w: AI settings are not approved", ErrUnavailable)
}
job, err := s.GetMaterial(ctx, workID)
if err != nil {
return MaterialJob{}, err
}
if job.RewriteConfirmedAt == nil {
return MaterialJob{}, ErrConflict
}
work, err := s.GetWork(ctx, workID)
if err != nil {
return MaterialJob{}, err
}
content, err := generator.Generate(ctx,
"根据原作品和转写内容生成仿写草稿。只返回 JSON,不要 Markdown 或额外文字,格式必须是 {\"title\":\"标题\",\"script\":\"脚本\"}。不得编造原文没有的事实。",
fmt.Sprintf("改写要求:%s\n原标题:%s\n原文案:%s\n转写:%s", job.RewriteRequirement, work.Title, work.Body, job.Transcript))
if err != nil {
return MaterialJob{}, err
}
title, script, err := parseGeneratedRewrite(content)
if err != nil {
return MaterialJob{}, err
}
if _, err := s.db.ExecContext(ctx, `UPDATE creator_material_job SET generated_title = $2, generated_script = $3, updated_at = now() WHERE work_id = $1`, workID, title, script); err != nil {
return MaterialJob{}, databaseError(err)
}
return s.GetMaterial(ctx, workID)
}
func (s *Store) SaveRewrite(ctx context.Context, workID, title, script string) (MaterialJob, error) {
title, script = strings.TrimSpace(title), strings.TrimSpace(script)
if title == "" || script == "" || utf8.RuneCountInString(title) > 1000 || utf8.RuneCountInString(script) > 100000 {
@@ -591,6 +668,48 @@ func (s *Store) GetComment(ctx context.Context, id string) (Comment, error) {
return result, rowError(err)
}
func pageBounds(page, pageSize int) (int, int, error) {
if page < 1 || pageSize < 1 || pageSize > 100 {
return 0, 0, ErrInvalid
}
if page > int(maxDurationSeconds) || pageSize > int(maxDurationSeconds)/page {
return 0, 0, ErrInvalid
}
return (page - 1) * pageSize, pageSize, nil
}
func slicePage[T any](items []T, page, pageSize int) (Page[T], error) {
offset, limit, err := pageBounds(page, pageSize)
if err != nil {
return Page[T]{}, err
}
if offset > len(items) {
offset = len(items)
}
end := offset + limit
if end > len(items) {
end = len(items)
}
data := append([]T(nil), items[offset:end]...)
return Page[T]{Data: data, Page: page, PageSize: pageSize, Total: len(items), HasNext: end < len(items)}, nil
}
func (s *Store) ListWorksPage(ctx context.Context, filter WorkFilter, page, pageSize int) (Page[Work], error) {
items, err := s.ListWorks(ctx, filter)
if err != nil {
return Page[Work]{}, err
}
return slicePage(items, page, pageSize)
}
func (s *Store) ListCommentsPage(ctx context.Context, platform, workID string, page, pageSize int) (Page[Comment], error) {
items, err := s.ListComments(ctx, platform, workID)
if err != nil {
return Page[Comment]{}, err
}
return slicePage(items, page, pageSize)
}
func (s *Store) ListComments(ctx context.Context, platform, workID string) ([]Comment, error) {
query, args := commentSelect, []any{}
where := []string{}