package job import ( "context" "fmt" "strings" "haixun-backend/internal/library/publishschedule" copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase" missiondomain "haixun-backend/internal/model/copy_mission/domain/usecase" jobdom "haixun-backend/internal/model/job/domain/usecase" inventorydomain "haixun-backend/internal/model/publish_inventory/domain/usecase" pqdomain "haixun-backend/internal/model/publish_queue/domain/usecase" ) type RefillPublishInventoryDeps struct { Jobs jobdom.UseCase PublishInventory inventorydomain.UseCase PublishQueue pqdomain.UseCase CopyMission missiondomain.UseCase CopyDraft copydraftdomain.UseCase } func RegisterRefillPublishInventoryHandler(runner *Runner, deps RefillPublishInventoryDeps) { if runner == nil { return } runner.RegisterStepHandler("refill_publish_inventory", func(ctx context.Context, step StepContext) error { return runRefillPublishInventory(ctx, step, deps) }) } func runRefillPublishInventory(ctx context.Context, step StepContext, deps RefillPublishInventoryDeps) error { payload := step.Run.Payload tenantID, ownerUID := runActorFromPayload(payload, step.Run) accountID := stringField(payload, "account_id") if tenantID == "" || ownerUID == "" || accountID == "" { return fmt.Errorf("refill publish inventory payload missing tenant_id, owner_uid, or account_id") } if deps.PublishInventory == nil || deps.PublishQueue == nil { return fmt.Errorf("publish inventory dependencies are not configured") } policy, err := deps.PublishInventory.Get(ctx, tenantID, ownerUID, accountID) if err != nil { return err } if !policy.Enabled { return nil } health, err := deps.PublishQueue.ListPublishHealth(ctx, tenantID, ownerUID, accountID, 1, 1) if err != nil { return err } need := policy.LowStockThreshold - int(health.Summary.PendingScheduled) if requested := intField(payload, "count"); requested > 0 { need = requested } if need <= 0 { return nil } if need > 12 { need = 12 } slots := make([]publishschedule.Slot, 0, len(policy.Slots)) for _, slot := range policy.Slots { slots = append(slots, publishschedule.Slot{Weekday: slot.Weekday, Time: slot.Time}) } times := publishschedule.BuildSchedule(0, policy.Timezone, slots, need) for i := 0; i < need; i++ { if err := step.Heartbeat(ctx); err != nil { return err } ref := pickSourceRef(policy.SourceRefs, i) text, personaID, missionID := refillText(ctx, deps, tenantID, ownerUID, ref, i) createReq := pqdomain.CreateRequest{ TenantID: tenantID, OwnerUID: ownerUID, AccountID: accountID, PersonaID: personaID, CopyMissionID: missionID, Text: text, } if i < len(times) { createReq.ScheduledAt = times[i] } if deps.CopyDraft != nil && personaID != "" { draft, err := deps.CopyDraft.Create(ctx, copydraftdomain.CreateRequest{ TenantID: tenantID, OwnerUID: ownerUID, PersonaID: personaID, CopyMissionID: missionID, DraftType: "inventory-refill", SortOrder: i + 1, Text: text, Rationale: "自動補庫存產生", }) if err == nil && draft != nil { createReq.CopyDraftID = draft.ID } } item, err := deps.PublishQueue.Create(ctx, createReq) if err != nil { return err } if deps.CopyDraft != nil && createReq.CopyDraftID != "" { _, _ = deps.CopyDraft.MarkScheduled(ctx, copydraftdomain.MarkScheduledRequest{ TenantID: tenantID, OwnerUID: ownerUID, PersonaID: personaID, DraftID: createReq.CopyDraftID, QueueID: item.ID, }) } } return nil } func pickSourceRef(items []inventorydomain.SourceRef, idx int) inventorydomain.SourceRef { if len(items) == 0 { return inventorydomain.SourceRef{Type: "manual_seed", ManualSeed: "分享一個今天巡樓觀察到的重點"} } return items[idx%len(items)] } func refillText(ctx context.Context, deps RefillPublishInventoryDeps, tenantID, ownerUID string, ref inventorydomain.SourceRef, idx int) (string, string, string) { personaID := strings.TrimSpace(ref.PersonaID) missionID := strings.TrimSpace(ref.CopyMissionID) seed := strings.TrimSpace(ref.ManualSeed) if missionID != "" && personaID != "" && deps.CopyMission != nil { if mission, err := deps.CopyMission.Get(ctx, tenantID, ownerUID, personaID, missionID); err == nil { seed = strings.TrimSpace(firstNonEmpty(mission.Brief, mission.Label, mission.SeedQuery)) } } if seed == "" { seed = "分享一個今天巡樓觀察到的重點" } return fmt.Sprintf("%s\n\n巡樓筆記 #%d", seed, idx+1), personaID, missionID } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" }