feat: split account management

This commit is contained in:
2026-09-21 16:44:09 +08:00
parent 86a332997e
commit 4f736cba21
25 changed files with 1246 additions and 630 deletions
@@ -135,8 +135,7 @@ func TestCreatorRouteValidationCoverage(t *testing.T) {
{http.MethodPost, "/api/creator/strategies/missing/disable"},
{http.MethodDelete, "/api/creator/strategies/missing"},
{http.MethodPut, "/api/creator/strategies/missing"},
{http.MethodPost, "/api/creator/competitors/preview"},
{http.MethodPost, "/api/creator/competitors"},
{http.MethodPost, "/api/creator/competitor-share-jobs"},
{http.MethodPost, "/api/creator/competitors/missing/pause"},
{http.MethodPost, "/api/creator/competitors/missing/resume"},
{http.MethodPost, "/api/creator/competitors/missing/sync"},
+77 -23
View File
@@ -273,36 +273,45 @@ func registerCreatorWithServices(app *fiber.App, store *creator.Store, phaseASto
}
return c.JSON(items)
})
app.Post("/api/creator/competitors/preview", func(c fiber.Ctx) error {
var input competitorShareRequest
if err := decodeCreator(c, &input); err != nil {
return creatorError(c, err)
}
preview, err := previewCompetitorShare(c.Context(), store, phaseAStore, hubStore, input.AccountID, input.Platform, input.ShareURL)
app.Get("/api/creator/competitor-share-jobs", func(c fiber.Ctx) error {
items, err := store.ListCompetitorShareJobs(c.Context(), c.Query("platform"), c.Query("status"))
if err != nil {
return creatorError(c, err)
}
return c.JSON(preview)
return c.JSON(items)
})
app.Post("/api/creator/competitors", func(c fiber.Ctx) error {
var request competitorShareRequest
app.Get("/api/creator/competitor-share-jobs/:id", func(c fiber.Ctx) error {
item, err := store.GetCompetitorShareJob(c.Context(), c.Params("id"))
if err != nil {
return creatorError(c, err)
}
return c.JSON(item)
})
app.Post("/api/creator/competitor-share-jobs", func(c fiber.Ctx) error {
var request creator.CompetitorShareJobInput
if err := decodeCreator(c, &request); err != nil {
return creatorError(c, err)
}
preview, err := previewCompetitorShare(c.Context(), store, phaseAStore, hubStore, request.AccountID, request.Platform, request.ShareURL)
platform, err := competitorSharePlatform(request.ShareURL)
if err != nil {
return creatorError(c, err)
}
input := preview.input()
input.Tags = request.Tags
if err := validateXiaohongshuCompetitor(input); err != nil {
return creatorError(c, err)
if request.Platform != "" && request.Platform != platform {
return creatorError(c, creator.ErrInvalid)
}
item, err := store.CreateCompetitor(c.Context(), input)
request.Platform = platform
item, err := store.CreateCompetitorShareJob(c.Context(), request)
if err != nil {
return creatorError(c, err)
}
return c.Status(fiber.StatusCreated).JSON(item)
return c.Status(fiber.StatusAccepted).JSON(item)
})
app.Post("/api/creator/competitor-share-jobs/:id/retry", func(c fiber.Ctx) error {
item, err := store.RetryCompetitorShareJob(c.Context(), c.Params("id"))
if err != nil {
return creatorError(c, err)
}
return c.Status(fiber.StatusAccepted).JSON(item)
})
app.Get("/api/creator/competitors/:id", func(c fiber.Ctx) error {
item, err := store.GetCompetitor(c.Context(), c.Params("id"))
@@ -1524,13 +1533,6 @@ func (browser creatorGatewayBrowser) Media(ctx context.Context, target, destinat
return writeCreatorMedia(destination, data)
}
type competitorShareRequest struct {
AccountID string `json:"account_id"`
Platform string `json:"platform"`
ShareURL string `json:"share_url"`
Tags []string `json:"tags"`
}
type competitorSharePreview struct {
AccountID string `json:"account_id"`
Platform string `json:"platform"`
@@ -1952,6 +1954,49 @@ func syncCreatorCompetitorWithClaim(ctx context.Context, store *creator.Store, p
return report, nil
}
func processCompetitorShareJob(ctx context.Context, store *creator.Store, phaseAStore *accountdomain.Store, hubStore *hub.Store, jobID string) error {
for {
job, leaseToken, claimed, err := store.ClaimCompetitorShareJob(ctx, jobID, time.Now().UTC())
if err != nil {
return err
}
if !claimed {
return nil
}
preview, processErr := previewCompetitorShare(ctx, store, phaseAStore, hubStore, "", job.Platform, job.ShareURL)
competitorID := ""
if processErr == nil {
input := preview.input()
input.Tags = job.Tags
if processErr = validateXiaohongshuCompetitor(input); processErr == nil {
var competitor creator.Competitor
competitor, processErr = store.UpsertCompetitor(ctx, input)
competitorID = competitor.ID
}
}
if processErr == nil {
if err := store.MarkCompetitorShareJob(ctx, job.ID, leaseToken, creator.CompetitorShareJobSucceeded, competitorID, ""); err != nil {
return err
}
logrus.WithFields(logrus.Fields{"job_id": job.ID, "competitor_id": competitorID, "attempts": job.Attempts}).Info("creator competitor share job completed")
return nil
}
status := creator.CompetitorShareJobQueued
if job.Attempts >= creator.MaxCompetitorShareJobAttempts {
status = creator.CompetitorShareJobFailed
}
if err := store.MarkCompetitorShareJob(ctx, job.ID, leaseToken, status, "", processErr.Error()); err != nil {
return errors.Join(processErr, err)
}
logrus.WithFields(logrus.Fields{"job_id": job.ID, "attempts": job.Attempts, "status": status}).WithError(processErr).Warn("creator competitor share job attempt failed")
if status == creator.CompetitorShareJobFailed || ctx.Err() != nil {
return nil
}
}
}
func runCreatorScheduleOnce(ctx context.Context, store *creator.Store, phaseAStore *accountdomain.Store, hubStore *hub.Store) error {
if store == nil {
return creator.ErrUnavailable
@@ -1961,6 +2006,15 @@ func runCreatorScheduleOnce(ctx context.Context, store *creator.Store, phaseASto
return err
}
now := time.Now().UTC()
shareJobs, err := store.ListDueCompetitorShareJobs(ctx, now)
if err != nil {
return err
}
for _, job := range shareJobs {
if err := processCompetitorShareJob(ctx, store, phaseAStore, hubStore, job.ID); err != nil {
logrus.WithError(err).WithField("job_id", job.ID).Warn("creator competitor share job failed")
}
}
competitors, err := store.ListDueCompetitors(ctx, now)
if err != nil {
return err
@@ -52,8 +52,7 @@ func TestCreatorWriteRoutesRejectMalformedInputBeforeStoreAccess(t *testing.T) {
{method: http.MethodPost, path: "/api/creator/relations"},
{method: http.MethodPost, path: "/api/creator/accounts/account-1/strategies"},
{method: http.MethodPut, path: "/api/creator/strategies/strategy-1"},
{method: http.MethodPost, path: "/api/creator/competitors/preview"},
{method: http.MethodPost, path: "/api/creator/competitors"},
{method: http.MethodPost, path: "/api/creator/competitor-share-jobs"},
{method: http.MethodPut, path: "/api/creator/competitors/competitor-1"},
{method: http.MethodPost, path: "/api/creator/competitors/competitor-1/sync"},
{method: http.MethodPost, path: "/api/creator/xiaohongshu/search"},