package persona import ( "context" "strings" "time" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" libthreads "haixun-backend/internal/library/threadsapi" "haixun-backend/internal/library/threadspost" copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" publishqueuedomain "haixun-backend/internal/model/publish_queue/domain/usecase" "haixun-backend/internal/svc" "haixun-backend/internal/types" ) type PublishPersonaCopyDraftLogic struct { ctx context.Context svcCtx *svc.ServiceContext } func NewPublishPersonaCopyDraftLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishPersonaCopyDraftLogic { return &PublishPersonaCopyDraftLogic{ctx: ctx, svcCtx: svcCtx} } func (l *PublishPersonaCopyDraftLogic) PublishPersonaCopyDraft( req *types.PublishCopyDraftHandlerReq, ) (*types.PublishCopyDraftData, error) { tenantID, uid, err := actorFrom(l.ctx) if err != nil { return nil, err } personaID := strings.TrimSpace(req.ID) draftID := strings.TrimSpace(req.DraftID) if !req.Confirm { return nil, app.For(code.Persona).InputMissingRequired("請確認 confirm=true 後再發布貼文") } if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil { return nil, err } draftItem, err := l.svcCtx.CopyDraft.Get(l.ctx, tenantID, uid, personaID, draftID) if err != nil { return nil, err } draft := draftItem if draft.Status == "published" { return nil, app.For(code.Persona).ResInvalidState("此草稿已發布") } text := strings.TrimSpace(req.Text) if text == "" { text = strings.TrimSpace(draft.Text) } if text == "" { return nil, app.For(code.Persona).InputMissingRequired("text is required") } if err := threadspost.ValidatePublish(text); err != nil { return nil, app.For(code.Persona).InputInvalidFormat(err.Error()) } topicTag := firstNonEmpty(req.TopicTag, draft.TopicTag) patch := copydraftdomain.CopyDraftPatch{} needsUpdate := false if strings.TrimSpace(req.Text) != "" && req.Text != draft.Text { patch.Text = &req.Text needsUpdate = true } if strings.TrimSpace(req.TopicTag) != "" && req.TopicTag != draft.TopicTag { patch.TopicTag = &req.TopicTag needsUpdate = true } if needsUpdate { updated, err := l.svcCtx.CopyDraft.Update(l.ctx, copydraftdomain.UpdateRequest{ TenantID: tenantID, OwnerUID: uid, PersonaID: personaID, DraftID: draftID, Patch: patch, }) if err != nil { return nil, err } draft = updated text = strings.TrimSpace(updated.Text) topicTag = firstNonEmpty(updated.TopicTag, req.TopicTag) } cred, err := l.svcCtx.ThreadsAccount.ResolveMemberThreadsPublishCredential(l.ctx, tenantID, uid) if err != nil { return nil, err } result, err := libthreads.PublishText(l.ctx, libthreads.PublishTextInput{ ThreadsUserID: cred.ThreadsUserID, AccessToken: cred.AccessToken, Text: text, TopicTag: topicTag, }) if err != nil { return nil, app.For(code.ThreadsAccount).SvcThirdParty("Threads API 發布貼文失敗:" + err.Error()) } now := time.Now().UnixNano() if cred.AccountID != "" { _, _ = l.svcCtx.PublishQueue.RecordPublished(l.ctx, publishqueuedomain.RecordPublishedRequest{ TenantID: tenantID, OwnerUID: uid, AccountID: cred.AccountID, Text: text, TopicTag: topicTag, MediaID: result.MediaID, Permalink: result.Permalink, PublishedAt: now, }) } updated, err := l.svcCtx.CopyDraft.MarkPublished(l.ctx, copydraftdomain.MarkPublishedRequest{ TenantID: tenantID, OwnerUID: uid, PersonaID: personaID, DraftID: draftID, MediaID: result.MediaID, Permalink: result.Permalink, }) if err != nil { return nil, err } return &types.PublishCopyDraftData{ DraftID: draftID, MediaID: result.MediaID, Permalink: result.Permalink, Status: updated.Status, Message: "仿寫貼文已透過 Threads API 發布", }, nil }