thread-master/old/backend/internal/svc/service_context.go

641 lines
26 KiB
Go
Raw Normal View History

2026-06-26 08:37:04 +00:00
package svc
import (
"context"
"fmt"
2026-06-26 16:02:06 +00:00
"net/http"
2026-06-26 08:37:04 +00:00
"os"
"time"
"haixun-backend/internal/config"
2026-06-26 16:02:06 +00:00
libcrypto "haixun-backend/internal/library/crypto"
2026-07-07 14:02:41 +00:00
"haixun-backend/internal/library/mailer"
2026-06-26 08:37:04 +00:00
libmongo "haixun-backend/internal/library/mongo"
libredis "haixun-backend/internal/library/redis"
2026-07-07 14:02:41 +00:00
"haixun-backend/internal/library/storage"
libthreads "haixun-backend/internal/library/threadsapi"
2026-06-26 08:37:04 +00:00
"haixun-backend/internal/library/validate"
"haixun-backend/internal/middleware"
aisettings "haixun-backend/internal/model/ai/usecase"
authrepodomain "haixun-backend/internal/model/auth/domain/repository"
authdomain "haixun-backend/internal/model/auth/domain/usecase"
authrepo "haixun-backend/internal/model/auth/repository"
authuc "haixun-backend/internal/model/auth/usecase"
branddomain "haixun-backend/internal/model/brand/domain/usecase"
brandrepo "haixun-backend/internal/model/brand/repository"
brandusecase "haixun-backend/internal/model/brand/usecase"
contentformuladomain "haixun-backend/internal/model/content_formula/domain/usecase"
contentformularepo "haixun-backend/internal/model/content_formula/repository"
contentformulausecase "haixun-backend/internal/model/content_formula/usecase"
2026-06-26 08:37:04 +00:00
cmatrixdomain "haixun-backend/internal/model/content_matrix/domain/usecase"
cmatrixrepo "haixun-backend/internal/model/content_matrix/repository"
cmatrixusecase "haixun-backend/internal/model/content_matrix/usecase"
contentopsdomain "haixun-backend/internal/model/content_ops/domain/usecase"
contentopsrepo "haixun-backend/internal/model/content_ops/repository"
contentopsusecase "haixun-backend/internal/model/content_ops/usecase"
2026-06-26 08:37:04 +00:00
copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase"
copydraftrepo "haixun-backend/internal/model/copy_draft/repository"
copydraftusecase "haixun-backend/internal/model/copy_draft/usecase"
copymissiondomain "haixun-backend/internal/model/copy_mission/domain/usecase"
copymissionrepo "haixun-backend/internal/model/copy_mission/repository"
copymissionusecase "haixun-backend/internal/model/copy_mission/usecase"
jobrepo "haixun-backend/internal/model/job/repository"
jobusecase "haixun-backend/internal/model/job/usecase"
kgdomain "haixun-backend/internal/model/knowledge_graph/domain/usecase"
kgrepo "haixun-backend/internal/model/knowledge_graph/repository"
kgusecase "haixun-backend/internal/model/knowledge_graph/usecase"
memberdomain "haixun-backend/internal/model/member/domain/usecase"
memberrepo "haixun-backend/internal/model/member/repository"
memberuc "haixun-backend/internal/model/member/usecase"
mentioninboxdomain "haixun-backend/internal/model/mention_inbox/domain/usecase"
mentioninboxrepo "haixun-backend/internal/model/mention_inbox/repository"
mentioninboxusecase "haixun-backend/internal/model/mention_inbox/usecase"
2026-06-26 08:37:04 +00:00
outreachdraftdomain "haixun-backend/internal/model/outreach_draft/domain/usecase"
outreachdraftrepo "haixun-backend/internal/model/outreach_draft/repository"
outreachdraftusecase "haixun-backend/internal/model/outreach_draft/usecase"
ownpostformularepo "haixun-backend/internal/model/own_post_formula/repository"
ownpostformuladomain "haixun-backend/internal/model/own_post_formula/usecase"
2026-06-26 08:37:04 +00:00
permissiondomain "haixun-backend/internal/model/permission/domain/usecase"
permissionrepo "haixun-backend/internal/model/permission/repository"
permissionuc "haixun-backend/internal/model/permission/usecase"
personadomain "haixun-backend/internal/model/persona/domain/usecase"
personarepo "haixun-backend/internal/model/persona/repository"
personausecase "haixun-backend/internal/model/persona/usecase"
placementusecase "haixun-backend/internal/model/placement/usecase"
placementtopicdomain "haixun-backend/internal/model/placement_topic/domain/usecase"
placementtopicrepo "haixun-backend/internal/model/placement_topic/repository"
placementtopicusecase "haixun-backend/internal/model/placement_topic/usecase"
2026-06-27 16:03:28 +00:00
publishanalyticsdomain "haixun-backend/internal/model/publish_analytics/domain/usecase"
publishanalyticsrepo "haixun-backend/internal/model/publish_analytics/repository"
publishanalyticsusecase "haixun-backend/internal/model/publish_analytics/usecase"
publishguarddomain "haixun-backend/internal/model/publish_guard/domain/usecase"
publishguardrepo "haixun-backend/internal/model/publish_guard/repository"
publishguardusecase "haixun-backend/internal/model/publish_guard/usecase"
publishinventorydomain "haixun-backend/internal/model/publish_inventory/domain/usecase"
publishinventoryrepo "haixun-backend/internal/model/publish_inventory/repository"
publishinventoryusecase "haixun-backend/internal/model/publish_inventory/usecase"
publishqueuedomain "haixun-backend/internal/model/publish_queue/domain/usecase"
publishqueuerepo "haixun-backend/internal/model/publish_queue/repository"
publishqueueusecase "haixun-backend/internal/model/publish_queue/usecase"
publishqueueeventdomain "haixun-backend/internal/model/publish_queue_event/domain/usecase"
publishqueueeventrepo "haixun-backend/internal/model/publish_queue_event/repository"
publishqueueeventusecase "haixun-backend/internal/model/publish_queue_event/usecase"
2026-06-26 08:37:04 +00:00
scanpostdomain "haixun-backend/internal/model/scan_post/domain/usecase"
scanpostrepo "haixun-backend/internal/model/scan_post/repository"
scanpostusecase "haixun-backend/internal/model/scan_post/usecase"
settingrepo "haixun-backend/internal/model/setting/repository"
settingusecase "haixun-backend/internal/model/setting/usecase"
stylepresetdomain "haixun-backend/internal/model/style_preset/domain/usecase"
stylepresetrepo "haixun-backend/internal/model/style_preset/repository"
stylepresetusecase "haixun-backend/internal/model/style_preset/usecase"
threadsaccountrepodomain "haixun-backend/internal/model/threads_account/domain/repository"
2026-06-26 08:37:04 +00:00
threadsaccountdomain "haixun-backend/internal/model/threads_account/domain/usecase"
threadsaccountrepo "haixun-backend/internal/model/threads_account/repository"
threadsaccountusecase "haixun-backend/internal/model/threads_account/usecase"
jobworker "haixun-backend/internal/worker/job"
goredis "github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/rest"
)
type ServiceContext struct {
Config config.Config
Validator *validate.Validate
Mongo *libmongo.Client
Redis *goredis.Client
Setting settingusecase.UseCase
AI aisettings.UseCase
Job jobusecase.UseCase
AuthToken authdomain.TokenUseCase
Member memberdomain.UseCase
Permission permissiondomain.UseCase
Persona personadomain.UseCase
CopyMission copymissiondomain.UseCase
Brand branddomain.UseCase
PlacementTopic placementtopicdomain.UseCase
KnowledgeGraph kgdomain.UseCase
Placement placementusecase.UseCase
ScanPost scanpostdomain.UseCase
OutreachDraft outreachdraftdomain.UseCase
ContentMatrix cmatrixdomain.UseCase
ContentOps contentopsdomain.UseCase
CopyDraft copydraftdomain.UseCase
ThreadsAccount threadsaccountdomain.UseCase
PublishAnalytics publishanalyticsdomain.UseCase
PublishQueue publishqueuedomain.UseCase
PublishInventory publishinventorydomain.UseCase
PublishGuard publishguarddomain.UseCase
PublishQueueEvent publishqueueeventdomain.UseCase
StylePreset stylepresetdomain.UseCase
OwnPostFormula ownpostformuladomain.UseCase
ContentFormula contentformuladomain.UseCase
MentionInbox mentioninboxdomain.UseCase
2026-07-07 14:02:41 +00:00
AvatarStorage storage.Uploader
Mailer mailer.Mailer
2026-06-26 08:37:04 +00:00
// Middlewares mounted per route group via generate/api `middleware:` directive.
AuthJWT rest.Middleware
MemberAuth rest.Middleware
WorkerSecret rest.Middleware
stopWorker context.CancelFunc
stopScheduler context.CancelFunc
stopReaper context.CancelFunc
stopThreadsTokenSweep context.CancelFunc
stopPublishQueueSweep context.CancelFunc
2026-06-26 08:37:04 +00:00
}
func NewServiceContext(c config.Config) *ServiceContext {
ctx := context.Background()
mongoClient, err := libmongo.NewClient(ctx, c.Mongo)
if err != nil {
panic(err)
}
redisClient := libredis.NewClient(c.Redis)
2026-06-26 16:02:06 +00:00
secretsCipher, err := libcrypto.New(c.Secrets.EncryptionKey)
if err != nil {
panic(err)
}
2026-06-26 08:37:04 +00:00
settingRepository := settingrepo.NewMongoRepository(mongoClient.Database())
if err := settingRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
settingUseCase := settingusecase.NewUseCase(settingRepository)
var tokenRevokeStore authrepodomain.TokenRevokeStore
if redisClient != nil {
tokenRevokeStore = authrepo.NewRedisTokenRevokeStore(redisClient)
}
authTokenUseCase := authuc.NewTokenUseCase(c.Auth, tokenRevokeStore)
2026-07-07 14:02:41 +00:00
avatarStorage, err := storage.NewS3Uploader(ctx, c.Storage.S3)
if err != nil {
panic(err)
}
smtpMailer := mailer.NewSMTPMailer(c.SMTP)
2026-06-26 08:37:04 +00:00
memberRepository := memberrepo.NewMongoRepository(mongoClient.Database())
if err := memberRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
memberUseCase := memberuc.NewUseCase(memberRepository, authTokenUseCase)
permissionRepository := permissionrepo.NewMongoPermissionRepository(mongoClient.Database())
rolePermissionRepository := permissionrepo.NewMongoRolePermissionRepository(mongoClient.Database())
if err := permissionRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := rolePermissionRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
permissionUseCase := permissionuc.NewUseCase(permissionRepository, rolePermissionRepository)
if err := permissionUseCase.EnsureDefaultPermissions(ctx); err != nil {
panic(err)
}
2026-07-07 14:02:41 +00:00
if c.Permission.SeedAdminRegister {
if err := permissionUseCase.EnsureAdminRegisterPermission(ctx); err != nil {
panic(err)
}
}
2026-06-26 08:37:04 +00:00
if err := permissionUseCase.EnsureDefaultRolePermissions(ctx, "default"); err != nil {
panic(err)
}
jobTemplateRepository := jobrepo.NewMongoTemplateRepository(mongoClient.Database())
jobRunRepository := jobrepo.NewMongoRunRepository(mongoClient.Database())
jobScheduleRepository := jobrepo.NewMongoScheduleRepository(mongoClient.Database())
jobEventRepository := jobrepo.NewMongoEventRepository(mongoClient.Database())
jobQueueRepository := jobrepo.NewRedisQueueRepository(redisClient)
if err := jobTemplateRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := jobRunRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := jobScheduleRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := jobEventRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
jobUseCase := jobusecase.NewUseCase(jobTemplateRepository, jobRunRepository, jobScheduleRepository, jobEventRepository, jobQueueRepository)
if err := jobUseCase.EnsureDemoTemplate(ctx); err != nil {
panic(err)
}
if err := jobUseCase.EnsureStyle8DTemplate(ctx); err != nil {
panic(err)
}
if err := jobUseCase.EnsureExpandGraphTemplate(ctx); err != nil {
panic(err)
}
if err := jobUseCase.EnsurePlacementScanTemplate(ctx); err != nil {
panic(err)
}
if err := jobUseCase.EnsureScanViralTemplate(ctx); err != nil {
panic(err)
}
if err := jobUseCase.EnsureGenerateOutreachDraftTemplate(ctx); err != nil {
2026-06-26 08:37:04 +00:00
panic(err)
}
if err := jobUseCase.EnsureGenerateTopicMatrixTemplate(ctx); err != nil {
2026-06-28 08:28:42 +00:00
panic(err)
}
if err := jobUseCase.EnsureGenerateFormulaDraftTemplate(ctx); err != nil {
2026-06-26 08:37:04 +00:00
panic(err)
}
if err := jobUseCase.EnsureGenerateRewriteDraftTemplate(ctx); err != nil {
2026-06-26 08:37:04 +00:00
panic(err)
}
2026-06-27 16:03:28 +00:00
if err := jobUseCase.EnsureRefreshThreadsTokenTemplate(ctx); err != nil {
panic(err)
}
if err := jobUseCase.EnsurePublishAnalyticsTemplate(ctx); err != nil {
panic(err)
}
2026-06-26 08:37:04 +00:00
copyMissionRepository := copymissionrepo.NewMongoRepository(mongoClient.Database())
if err := copyMissionRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
copyMissionUseCase := copymissionusecase.NewUseCase(copyMissionRepository)
copyDraftRepository := copydraftrepo.NewMongoRepository(mongoClient.Database())
if err := copyDraftRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
2026-06-28 08:28:42 +00:00
copyDraftUseCase := copydraftusecase.NewUseCase(copyDraftRepository, redisClient)
2026-06-26 08:37:04 +00:00
scanPostRepository := scanpostrepo.NewMongoRepository(mongoClient.Database())
if err := scanPostRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
scanPostUseCase := scanpostusecase.NewUseCase(scanPostRepository)
outreachDraftRepository := outreachdraftrepo.NewMongoRepository(mongoClient.Database())
if err := outreachDraftRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
outreachDraftUseCase := outreachdraftusecase.NewUseCase(outreachDraftRepository)
contentMatrixRepository := cmatrixrepo.NewMongoRepository(mongoClient.Database())
if err := contentMatrixRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
contentMatrixUseCase := cmatrixusecase.NewUseCase(contentMatrixRepository)
knowledgeGraphRepository := kgrepo.NewMongoRepository(mongoClient.Database())
if err := knowledgeGraphRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
knowledgeGraphUseCase := kgusecase.NewUseCase(knowledgeGraphRepository)
personaRepository := personarepo.NewMongoRepository(mongoClient.Database())
if err := personaRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
personaUseCase := personausecase.NewUseCase(personaRepository)
brandRepository := brandrepo.NewMongoRepository(mongoClient.Database())
if err := brandRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
brandUseCase := brandusecase.NewUseCase(brandRepository)
placementTopicRepository := placementtopicrepo.NewMongoRepository(mongoClient.Database())
if err := placementTopicRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
placementTopicUseCase := placementtopicusecase.NewUseCase(
placementTopicRepository,
brandRepository,
knowledgeGraphUseCase,
scanPostRepository,
)
threadsAccountRepository := threadsaccountrepo.NewMongoRepository(mongoClient.Database())
2026-06-26 16:02:06 +00:00
threadsAccountSecretsRepository := threadsaccountrepo.NewSecretsMongoRepository(mongoClient.Database(), secretsCipher)
publishAnalyticsRepository := publishanalyticsrepo.NewMongoRepository(mongoClient.Database())
publishQueueRepository := publishqueuerepo.NewMongoRepository(mongoClient.Database())
publishInventoryRepository := publishinventoryrepo.NewMongoRepository(mongoClient.Database())
publishGuardRepository := publishguardrepo.NewMongoRepository(mongoClient.Database())
publishQueueEventRepository := publishqueueeventrepo.NewMongoRepository(mongoClient.Database())
stylePresetRepository := stylepresetrepo.NewMongoRepository(mongoClient.Database())
ownPostFormulaRepository := ownpostformularepo.NewMongoRepository(mongoClient.Database())
contentFormulaRepository := contentformularepo.NewMongoRepository(mongoClient.Database())
contentOpsRepository := contentopsrepo.NewMongoRepository(mongoClient.Database())
mentionInboxRepository := mentioninboxrepo.NewMongoRepository(mongoClient.Database())
var threadsOAuthStateStore threadsaccountrepodomain.OAuthStateStore
if redisClient != nil {
threadsOAuthStateStore = threadsaccountrepo.NewOAuthStateRedisStore(redisClient)
}
threadsOAuthConfig := libthreads.OAuthConfig{
AppID: c.ThreadsOAuth.AppID,
AppSecret: c.ThreadsOAuth.AppSecret,
RedirectURI: c.ThreadsOAuth.RedirectURI,
SuccessRedirect: c.ThreadsOAuth.SuccessRedirect,
}
2026-06-26 08:37:04 +00:00
if err := threadsAccountRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := threadsAccountSecretsRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := publishAnalyticsRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := publishQueueRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := publishInventoryRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := publishGuardRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := publishQueueEventRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := stylePresetRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := ownPostFormulaRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := contentFormulaRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := contentOpsRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
if err := mentionInboxRepository.EnsureIndexes(ctx); err != nil {
panic(err)
}
ownPostFormulaUseCase := ownpostformuladomain.NewUseCase(ownPostFormulaRepository)
contentFormulaUseCase := contentformulausecase.NewUseCase(contentFormulaRepository)
contentOpsUseCase := contentopsusecase.NewUseCase(contentOpsRepository)
2026-06-26 08:37:04 +00:00
threadsAccountUseCase := threadsaccountusecase.NewUseCase(
threadsAccountRepository,
threadsAccountSecretsRepository,
threadsOAuthStateStore,
2026-06-26 08:37:04 +00:00
memberUseCase,
settingUseCase,
personaUseCase,
publishQueueRepository,
publishAnalyticsRepository,
publishInventoryRepository,
publishGuardRepository,
publishQueueEventRepository,
2026-06-26 16:02:06 +00:00
secretsCipher,
threadsOAuthConfig,
2026-06-26 08:37:04 +00:00
)
mentionInboxUseCase := mentioninboxusecase.NewUseCase(mentionInboxRepository, threadsAccountUseCase)
2026-06-26 08:37:04 +00:00
2026-06-26 16:02:06 +00:00
placementUseCase := placementusecase.NewUseCase(settingUseCase, secretsCipher)
2026-06-26 08:37:04 +00:00
publishAnalyticsUseCase := publishanalyticsusecase.NewUseCase(publishAnalyticsRepository, jobUseCase)
publishQueueEventUseCase := publishqueueeventusecase.NewUseCase(publishQueueEventRepository)
publishInventoryUseCase := publishinventoryusecase.NewUseCase(publishInventoryRepository, threadsAccountUseCase)
publishGuardUseCase := publishguardusecase.NewUseCase(publishGuardRepository, threadsAccountUseCase)
stylePresetUseCase := stylepresetusecase.NewUseCase(stylePresetRepository, personaUseCase)
2026-07-09 03:45:28 +00:00
var publishAttachmentStore storage.PublishAttachmentStore
if avatarStorage != nil {
publishAttachmentStore = avatarStorage
}
publishQueueUseCase := publishqueueusecase.NewUseCase(
publishQueueRepository,
threadsAccountUseCase,
publishAnalyticsUseCase,
publishGuardUseCase,
publishQueueEventUseCase,
copyDraftUseCase,
2026-07-09 03:45:28 +00:00
publishAttachmentStore,
)
2026-06-27 16:03:28 +00:00
2026-06-26 08:37:04 +00:00
sc := &ServiceContext{
Config: c,
Validator: validate.New(),
Mongo: mongoClient,
Redis: redisClient,
Setting: settingUseCase,
AI: aisettings.NewUseCase(),
Job: jobUseCase,
AuthToken: authTokenUseCase,
Member: memberUseCase,
Permission: permissionUseCase,
Persona: personaUseCase,
CopyMission: copyMissionUseCase,
Brand: brandUseCase,
PlacementTopic: placementTopicUseCase,
KnowledgeGraph: knowledgeGraphUseCase,
Placement: placementUseCase,
ScanPost: scanPostUseCase,
OutreachDraft: outreachDraftUseCase,
ContentMatrix: contentMatrixUseCase,
ContentOps: contentOpsUseCase,
CopyDraft: copyDraftUseCase,
ThreadsAccount: threadsAccountUseCase,
PublishAnalytics: publishAnalyticsUseCase,
PublishQueue: publishQueueUseCase,
PublishInventory: publishInventoryUseCase,
PublishGuard: publishGuardUseCase,
PublishQueueEvent: publishQueueEventUseCase,
StylePreset: stylePresetUseCase,
OwnPostFormula: ownPostFormulaUseCase,
ContentFormula: contentFormulaUseCase,
MentionInbox: mentionInboxUseCase,
2026-07-07 14:02:41 +00:00
AvatarStorage: avatarStorage,
Mailer: smtpMailer,
2026-06-26 08:37:04 +00:00
}
hostname, _ := os.Hostname()
if c.JobWorker.Enabled && redisClient != nil {
workerType := c.JobWorker.WorkerType
if workerType == "" {
workerType = "go"
}
workerID := c.JobWorker.WorkerID
if workerID == "" {
workerID = fmt.Sprintf("%s-%s-worker", hostname, workerType)
}
workerCtx, cancel := context.WithCancel(context.Background())
sc.stopWorker = cancel
runner := jobworker.NewRunner(workerID, workerType, jobUseCase)
jobworker.RegisterExpandGraphHandler(runner, jobworker.ExpandGraphDeps{
Jobs: jobUseCase,
Brand: brandUseCase,
PlacementTopic: placementTopicUseCase,
KnowledgeGraph: knowledgeGraphUseCase,
ThreadsAccount: threadsAccountUseCase,
Placement: placementUseCase,
AI: sc.AI,
})
jobworker.RegisterScanPlacementHandler(runner, jobworker.ScanPlacementDeps{
Jobs: jobUseCase,
Brand: brandUseCase,
PlacementTopic: placementTopicUseCase,
KnowledgeGraph: knowledgeGraphUseCase,
ScanPost: scanPostUseCase,
ThreadsAccount: threadsAccountUseCase,
Placement: placementUseCase,
})
jobworker.RegisterScanViralHandler(runner, jobworker.ScanViralDeps{
Jobs: jobUseCase,
CopyMission: copyMissionUseCase,
Persona: personaUseCase,
ScanPost: scanPostUseCase,
CopyDraft: copyDraftUseCase,
ThreadsAccount: threadsAccountUseCase,
Placement: placementUseCase,
AI: sc.AI,
})
jobworker.RegisterGenerateOutreachDraftHandler(runner, jobworker.GenerateOutreachDraftDeps{
2026-06-26 08:37:04 +00:00
Jobs: jobUseCase,
Brand: brandUseCase,
2026-06-26 08:37:04 +00:00
Persona: personaUseCase,
ScanPost: scanPostUseCase,
KnowledgeGraph: knowledgeGraphUseCase,
2026-06-26 08:37:04 +00:00
ThreadsAccount: threadsAccountUseCase,
AI: sc.AI,
OutreachDraft: outreachDraftUseCase,
2026-06-26 08:37:04 +00:00
})
jobworker.RegisterGenerateTopicMatrixHandler(runner, jobworker.GenerateTopicMatrixDeps{
2026-06-28 08:28:42 +00:00
Jobs: jobUseCase,
Persona: personaUseCase,
CopyDraft: copyDraftUseCase,
ContentOps: contentOpsUseCase,
2026-06-28 08:28:42 +00:00
ThreadsAccount: threadsAccountUseCase,
Placement: placementUseCase,
AI: sc.AI,
})
jobworker.RegisterGenerateFormulaDraftHandler(runner, jobworker.GenerateFormulaDraftDeps{
2026-06-26 08:37:04 +00:00
Jobs: jobUseCase,
Persona: personaUseCase,
ContentFormula: contentFormulaUseCase,
2026-06-26 08:37:04 +00:00
CopyDraft: copyDraftUseCase,
ThreadsAccount: threadsAccountUseCase,
Placement: placementUseCase,
2026-06-26 08:37:04 +00:00
AI: sc.AI,
})
jobworker.RegisterGenerateRewriteDraftHandler(runner, jobworker.GenerateRewriteDraftDeps{
2026-06-26 08:37:04 +00:00
Jobs: jobUseCase,
Persona: personaUseCase,
ContentFormula: contentFormulaUseCase,
2026-06-26 08:37:04 +00:00
CopyDraft: copyDraftUseCase,
ThreadsAccount: threadsAccountUseCase,
Placement: placementUseCase,
2026-06-26 08:37:04 +00:00
AI: sc.AI,
})
2026-06-27 16:03:28 +00:00
jobworker.RegisterRefreshThreadsTokenHandler(runner, jobworker.RefreshThreadsTokenDeps{
Jobs: jobUseCase,
ThreadsAccount: threadsAccountUseCase,
})
jobworker.RegisterPublishAnalyticsHandler(runner, jobworker.PublishAnalyticsDeps{
Jobs: jobUseCase,
ThreadsAccount: threadsAccountUseCase,
PublishAnalytics: publishAnalyticsUseCase,
})
2026-06-26 08:37:04 +00:00
go runner.Start(workerCtx)
} else if redisClient != nil {
inlineCtx, cancel := context.WithCancel(context.Background())
sc.stopWorker = cancel
sc.startInlineCopyWorkers(inlineCtx, hostname)
2026-06-26 08:37:04 +00:00
}
if c.JobScheduler.Enabled && redisClient != nil {
schedulerHolder := fmt.Sprintf("%s-scheduler", hostname)
interval := time.Duration(c.JobScheduler.IntervalSeconds) * time.Second
if interval <= 0 {
interval = time.Minute
}
schedulerCtx, cancel := context.WithCancel(context.Background())
sc.stopScheduler = cancel
scheduler := jobworker.NewScheduler(schedulerHolder, jobUseCase)
go scheduler.Start(schedulerCtx, interval)
}
if c.ThreadsTokenRefresh.Enabled && redisClient != nil {
leadDays := c.ThreadsTokenRefresh.LeadDays
if leadDays <= 0 {
leadDays = 7
}
interval := time.Duration(c.ThreadsTokenRefresh.IntervalSeconds) * time.Second
if interval <= 0 {
interval = time.Hour
}
sweepCtx, cancel := context.WithCancel(context.Background())
sc.stopThreadsTokenSweep = cancel
sweeper := jobworker.NewThreadsTokenSweeper(
threadsAccountUseCase,
jobUseCase,
time.Duration(leadDays)*24*time.Hour,
)
go sweeper.Start(sweepCtx, interval)
}
pqEnabled := c.ThreadsPublishQueue.Enabled
if !pqEnabled && c.ThreadsPublishQueue.IntervalSeconds == 0 && c.ThreadsPublishQueue.BatchSize == 0 {
pqEnabled = true
}
if pqEnabled {
interval := time.Duration(c.ThreadsPublishQueue.IntervalSeconds) * time.Second
if interval <= 0 {
interval = time.Minute
}
batchSize := c.ThreadsPublishQueue.BatchSize
if batchSize <= 0 {
batchSize = 20
}
pqCtx, cancel := context.WithCancel(context.Background())
sc.stopPublishQueueSweep = cancel
pqSweeper := jobworker.NewPublishQueueSweeper(publishQueueUseCase, batchSize)
go pqSweeper.Start(pqCtx, interval)
}
2026-06-26 08:37:04 +00:00
if c.JobReaper.Enabled && redisClient != nil {
interval := time.Duration(c.JobReaper.IntervalSeconds) * time.Second
if interval <= 0 {
interval = 30 * time.Second
}
reaperCtx, cancel := context.WithCancel(context.Background())
sc.stopReaper = cancel
reaper := jobworker.NewReaper(jobUseCase)
go reaper.Start(reaperCtx, interval)
}
2026-06-26 16:02:06 +00:00
// 認證 + RBAC先驗 JWT把 actor 放進 context再用 catalog 權限做路由級授權。
// 兩者組合後掛在受保護的 route group 上routes.go 以 AuthJWT / MemberAuth 引用)。
authJWT := middleware.NewAuthJWTMiddleware(sc.AuthToken, sc.Config.Auth).Handle
memberAuth := middleware.NewMemberAuthMiddleware(sc.AuthToken, sc.Config.Auth).Handle
2026-07-09 03:45:28 +00:00
emailVerified := middleware.NewEmailVerifiedMiddleware(sc.Member).Handle
2026-06-26 16:02:06 +00:00
rbac := middleware.NewPermissionRBACMiddleware(sc.Member, sc.Permission).Handle
2026-07-09 03:45:28 +00:00
sc.AuthJWT = func(next http.HandlerFunc) http.HandlerFunc { return authJWT(emailVerified(rbac(next))) }
sc.MemberAuth = func(next http.HandlerFunc) http.HandlerFunc { return memberAuth(emailVerified(rbac(next))) }
2026-06-26 08:37:04 +00:00
sc.WorkerSecret = middleware.NewWorkerSecretMiddleware(sc.Config.InternalWorker).Handle
return sc
}
func (sc *ServiceContext) Close(ctx context.Context) {
if sc == nil {
return
}
if sc.stopWorker != nil {
sc.stopWorker()
}
if sc.stopScheduler != nil {
sc.stopScheduler()
}
if sc.stopReaper != nil {
sc.stopReaper()
}
if sc.stopThreadsTokenSweep != nil {
sc.stopThreadsTokenSweep()
}
2026-06-26 08:37:04 +00:00
_ = sc.Mongo.Close(ctx)
if sc.Redis != nil {
_ = sc.Redis.Close()
}
}