Compare commits
No commits in common. "main" and "feat/frontend" have entirely different histories.
main
...
feat/front
|
|
@ -31,7 +31,7 @@ B 未過關前,不大寫業務後端。
|
|||
|
||||
- 新殼在 `apps/web`;不引入 MUI/Ant/Chakra。
|
||||
- 不複製任天堂 / Nook UI;不用 emoji 當主 icon。
|
||||
- 字體:Inter + Noto Sans TC(中文單一家族,可變字重 400–700)。
|
||||
- 字體:Inter + Taipei Sans TC。
|
||||
- mock | live 同一 repository 介面。
|
||||
- 詳見 `docs/product/haixun-console/spec.md`。
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ test-integration:
|
|||
./internal/module/usage/... ./internal/module/ai/... ./internal/module/search/... \
|
||||
./internal/module/permission/... ./internal/module/studio/... \
|
||||
./internal/module/inspire/... ./internal/module/scout/... \
|
||||
./internal/module/radar/... ./internal/logic/radar/ \
|
||||
./internal/logic/extension/ ./internal/logic/media/ \
|
||||
-count=1 -timeout 180s
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"time"
|
||||
|
||||
"apps/backend/internal/config"
|
||||
scoutRepo "apps/backend/internal/module/scout/repository"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
|
@ -61,10 +60,6 @@ func ownerIndex(sortField, name string) mongo.IndexModel {
|
|||
// These are deliberately all non-unique. Adding a uniqueness constraint to existing data can make
|
||||
// CreateMany fail and take a deploy down, so that belongs in its own migration with a duplicate
|
||||
// pre-check rather than here.
|
||||
//
|
||||
// Never list an index here that a migration creates as unique: Mongo rejects a second createIndex
|
||||
// with the same name and a different spec (IndexKeySpecsConflict, code 86), so the duplicate would
|
||||
// make this whole command panic once the migration has run.
|
||||
func indexModels() map[string][]mongo.IndexModel {
|
||||
return map[string][]mongo.IndexModel{
|
||||
// members.uid is read on every authenticated request via the auth middleware, and it is a
|
||||
|
|
@ -87,8 +82,6 @@ func indexModels() map[string][]mongo.IndexModel {
|
|||
// The jobs list is polled continuously by the UI and runs a count plus a find per poll.
|
||||
ownerIndex("updated_at", "owner_jobs_updated"),
|
||||
},
|
||||
"scout_runs": scoutRepo.RunIndexModels(),
|
||||
"scout_seen_identities": scoutRepo.SeenIdentityIndexModels(),
|
||||
// One notification is written per job state change and nothing purges them, so this
|
||||
// collection grows without bound and its scan cost grows with it.
|
||||
"notifications": {
|
||||
|
|
@ -115,11 +108,11 @@ func indexModels() map[string][]mongo.IndexModel {
|
|||
},
|
||||
"scout_brands": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_brands")}},
|
||||
"scout_products": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_products")}},
|
||||
"scout_posts": append([]mongo.IndexModel{
|
||||
"scout_posts": {
|
||||
ownerIndex("created_at", "owner_posts_created"),
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "brand_id", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("owner_brand_posts")},
|
||||
}, scoutRepo.RunPostIndexModels()...),
|
||||
"scout_homework": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_homework")}},
|
||||
},
|
||||
"scout_homework": {{Keys: bson.D{{Key: "owner_uid", Value: 1}}, Options: options.Index().SetName("owner_homework")}},
|
||||
"inspire_elements": {ownerIndex("updated_at", "owner_elements_updated")},
|
||||
"inspire_sessions": {ownerIndex("updated_at", "owner_sessions_updated")},
|
||||
"usage_events": {
|
||||
|
|
@ -161,38 +154,6 @@ func indexModels() map[string][]mongo.IndexModel {
|
|||
"growth_ws_members": {
|
||||
{Keys: bson.D{{Key: "workspace_id", Value: 1}, {Key: "uid", Value: 1}}, Options: options.Index().SetName("workspace_member")},
|
||||
},
|
||||
// demand-radar. Names and specs match migration 000014 exactly so both paths are
|
||||
// idempotent; the two unique keys (radar_opportunities.owner_opportunity_external and
|
||||
// crm_contacts.owner_contact_identity) are intentionally absent and owned by that migration.
|
||||
"radar_watches": {
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "status", Value: 1}}, Options: options.Index().SetName("owner_watches_status")},
|
||||
ownerIndex("created_at", "owner_watches_created"),
|
||||
},
|
||||
"radar_sweeps": {
|
||||
ownerIndex("created_at", "owner_sweeps_created"),
|
||||
{Keys: bson.D{{Key: "watch_id", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("watch_sweeps_created")},
|
||||
{Keys: bson.D{{Key: "job_id", Value: 1}}, Options: options.Index().SetName("sweep_job")},
|
||||
},
|
||||
"radar_opportunities": {
|
||||
ownerIndex("created_at", "owner_opportunities_created"),
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "intent_band", Value: 1}, {Key: "status", Value: 1}}, Options: options.Index().SetName("owner_opportunity_band_status")},
|
||||
},
|
||||
"radar_replies": {
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "opportunity_id", Value: 1}, {Key: "variant", Value: 1}}, Options: options.Index().SetName("owner_reply_variant")},
|
||||
},
|
||||
"crm_contacts": {
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "stage", Value: 1}, {Key: "last_touch_at", Value: -1}}, Options: options.Index().SetName("owner_contacts_stage_touch")},
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "needs_follow_up", Value: 1}}, Options: options.Index().SetName("owner_contacts_follow_up")},
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "removed_at", Value: 1}, {Key: "stage", Value: 1}, {Key: "last_touch_at", Value: -1}}, Options: options.Index().SetName("owner_contacts_active_stage_touch")},
|
||||
},
|
||||
"crm_touches": {
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "contact_id", Value: 1}, {Key: "created_at", Value: -1}}, Options: options.Index().SetName("owner_touches_created")},
|
||||
},
|
||||
// The follow-up scan is a cross-tenant due sweep, so its driving index is not owner-scoped.
|
||||
"crm_followups": {
|
||||
{Keys: bson.D{{Key: "status", Value: 1}, {Key: "due_at", Value: 1}}, Options: options.Index().SetName("followup_due_scan")},
|
||||
{Keys: bson.D{{Key: "owner_uid", Value: 1}, {Key: "contact_id", Value: 1}}, Options: options.Index().SetName("owner_contact_followup")},
|
||||
},
|
||||
"billing_checkout_attempts": {
|
||||
{Keys: bson.D{{Key: "uid", Value: 1}, {Key: "request_id", Value: 1}}, Options: options.Index().SetName("uid_request")},
|
||||
{Keys: bson.D{{Key: "session_id", Value: 1}}, Options: options.Index().SetName("checkout_session")},
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
// radarjudgesample exports recent opportunities for human judge calibration (T535).
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"apps/backend/internal/config"
|
||||
"apps/backend/internal/module/radar/domain"
|
||||
"apps/backend/internal/module/radar/repository"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/conf"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := flag.String("f", "etc/gateway.yaml", "config")
|
||||
owner := flag.Int64("owner", 0, "owner uid")
|
||||
out := flag.String("o", "judge-sample.csv", "output csv")
|
||||
flag.Parse()
|
||||
var c config.Config
|
||||
conf.MustLoad(*f, &c)
|
||||
c.ApplyEnv()
|
||||
repo := repository.NewMonStore(c.Mongo.URI, c.Mongo.Database)
|
||||
ctx := context.Background()
|
||||
list, _, err := repo.ListOpportunities(ctx, *owner, domain.OpportunityListFilter{Page: 1, PageSize: 100})
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
w := csv.NewWriter(os.Stdout)
|
||||
if *out != "-" {
|
||||
fp, err := os.Create(*out)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer fp.Close()
|
||||
w = csv.NewWriter(fp)
|
||||
}
|
||||
_ = w.Write([]string{"id", "status", "intent_score", "intent_band", "region_match", "text", "human_label", "notes"})
|
||||
for _, o := range list {
|
||||
_ = w.Write([]string{o.ID, o.Status, fmt.Sprintf("%d", o.IntentScore), o.IntentBand, o.RegionMatch, trim(o.Text, 120), "", ""})
|
||||
}
|
||||
w.Flush()
|
||||
fmt.Fprintf(os.Stderr, "exported %d rows at %s\n", len(list), time.Now().UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func trim(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n])
|
||||
}
|
||||
|
|
@ -18,22 +18,18 @@ import (
|
|||
"apps/backend/internal/module/ai"
|
||||
appnotifRepo "apps/backend/internal/module/appnotif/repository"
|
||||
appnotifUC "apps/backend/internal/module/appnotif/usecase"
|
||||
crmRepo "apps/backend/internal/module/crm/repository"
|
||||
crmUC "apps/backend/internal/module/crm/usecase"
|
||||
fsDomain "apps/backend/internal/module/filestorage/domain"
|
||||
"apps/backend/internal/module/filestorage/noop"
|
||||
"apps/backend/internal/module/filestorage/s3store"
|
||||
growthRepo "apps/backend/internal/module/growth/repository"
|
||||
growthUC "apps/backend/internal/module/growth/usecase"
|
||||
jobDomain "apps/backend/internal/module/job/domain"
|
||||
jobRepo "apps/backend/internal/module/job/repository"
|
||||
jobUC "apps/backend/internal/module/job/usecase"
|
||||
memberDomain "apps/backend/internal/module/member/domain"
|
||||
memberRepo "apps/backend/internal/module/member/repository"
|
||||
radarRepo "apps/backend/internal/module/radar/repository"
|
||||
radarUC "apps/backend/internal/module/radar/usecase"
|
||||
scoutDomain "apps/backend/internal/module/scout/domain"
|
||||
scoutRepo "apps/backend/internal/module/scout/repository"
|
||||
growthRepo "apps/backend/internal/module/growth/repository"
|
||||
growthUC "apps/backend/internal/module/growth/usecase"
|
||||
scoutUC "apps/backend/internal/module/scout/usecase"
|
||||
studioPublish "apps/backend/internal/module/studio/publish"
|
||||
studioRepo "apps/backend/internal/module/studio/repository"
|
||||
|
|
@ -158,35 +154,7 @@ func main() {
|
|||
_, _ = growthSvc.RecordPublished(ctx, ownerUID, "outbox_step", bundleID+":"+stepID, accountID, 0)
|
||||
}
|
||||
|
||||
radarSvc := radarUC.New(radarRepo.NewMonStore(c.Mongo.URI, c.Mongo.Database))
|
||||
// Product watches are validated again inside the worker at execution time.
|
||||
// Keep the worker on the same read-only Scout catalog bridge as the gateway;
|
||||
// without it queued product sweeps fail with "product catalog unavailable".
|
||||
radarSvc.ProductSource = &workerRadarProductContextBridge{Scout: scoutSvc}
|
||||
radarSvc.Usage = usageSvc
|
||||
radarSvc.HitFetch = radarUC.HitFetcherFunc(func(ctx context.Context, ownerUID int64, terms []string, limit int) ([]radarUC.ThreadHit, string, error) {
|
||||
hits, path, err := scoutSvc.SearchHitsOnly(ctx, ownerUID, terms, limit)
|
||||
if err != nil {
|
||||
return nil, path, err
|
||||
}
|
||||
o := make([]radarUC.ThreadHit, 0, len(hits))
|
||||
for _, h := range hits {
|
||||
o = append(o, radarUC.ThreadHit{URL: h.URL, Title: h.Title, Snippet: h.Snippet, PublishedAt: h.PublishedAt})
|
||||
}
|
||||
return o, path, nil
|
||||
})
|
||||
radarSvc.SweepJobs = radarUC.SweepJobSchedulerFunc(func(ctx context.Context, ownerUID int64, watchID string, runAt int64) (string, error) {
|
||||
j, err := jobs.ScheduleRadarSweep(ctx, ownerUID, watchID, runAt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return j.ID, nil
|
||||
})
|
||||
radarSvc.Notifier = radarUC.NotifierFromAppNotif(&workerRadarNotif{App: appN})
|
||||
crmSvc := crmUC.New(crmRepo.NewMonStore(c.Mongo.URI, c.Mongo.Database))
|
||||
crmSvc.Notifier = &workerCrmFollowUpNotif{App: appN}
|
||||
|
||||
logx.Infof("haixun worker started id=%s interval=%s (demo + token_renew + persona_analyze + compose_mimic + scout + radar_sweep + followup + outbox + growth)", workerID, interval)
|
||||
logx.Infof("haixun worker started id=%s interval=%s (demo + token_renew + persona_analyze + compose_mimic + outbox + growth)", workerID, interval)
|
||||
fmt.Printf("worker running id=%s interval=%s (Ctrl+C to stop)\n", workerID, interval)
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
|
|
@ -263,14 +231,6 @@ func main() {
|
|||
logx.Errorf("worker %s scout scan %s failed: %v", workerID, j.ID, err)
|
||||
_, _ = jobs.FailJob(ctx, j.ID, err.Error())
|
||||
}
|
||||
case jobDomain.TemplateRadarSweep:
|
||||
logx.Infof("worker %s radar_sweep job %s ref=%s", workerID, j.ID, j.RefID)
|
||||
if err := runRadarSweep(ctx, jobs, radarSvc, j); err != nil {
|
||||
logx.Errorf("worker %s radar_sweep %s failed: %v", workerID, j.ID, err)
|
||||
_, _ = jobs.FailJob(ctx, j.ID, err.Error())
|
||||
} else {
|
||||
logx.Infof("worker %s radar_sweep %s ok", workerID, j.ID)
|
||||
}
|
||||
default:
|
||||
logx.Infof("worker %s unknown template %s job %s — fail", workerID, j.TemplateType, j.ID)
|
||||
_, _ = jobs.FailJob(ctx, j.ID, "unknown template: "+j.TemplateType)
|
||||
|
|
@ -278,9 +238,9 @@ func main() {
|
|||
}
|
||||
// 2) One worker owns an outbox tick and renews its lease while claims run.
|
||||
processOutbox(ctx, studio, outboxLock, workerID, outboxLockTTL)
|
||||
// 3) 巡場維護:過期 outcome + 清終態 job + 雷達每日排程 + 追蹤掃描(單一 worker、低頻)
|
||||
// 3) 巡場維護:過期 outcome + 清終態 job(單一 worker、低頻)
|
||||
if time.Since(lastMaintenance) >= maintenanceEvery {
|
||||
if runMaintenance(ctx, growthSvc, jobs, radarSvc, crmSvc, maintenanceLock, workerID) {
|
||||
if runMaintenance(ctx, growthSvc, jobs, maintenanceLock, workerID) {
|
||||
lastMaintenance = time.Now()
|
||||
}
|
||||
}
|
||||
|
|
@ -294,8 +254,6 @@ func runMaintenance(
|
|||
ctx context.Context,
|
||||
growthSvc *growthUC.Service,
|
||||
jobs *jobUC.Service,
|
||||
radarSvc *radarUC.Service,
|
||||
crmSvc *crmUC.Service,
|
||||
lock *redislock.Lock,
|
||||
workerID string,
|
||||
) bool {
|
||||
|
|
@ -323,109 +281,9 @@ func runMaintenance(
|
|||
} else if purged > 0 {
|
||||
logx.Infof("worker %s purged %d expired terminal job(s)", workerID, purged)
|
||||
}
|
||||
// 雷達每日排程:UTC 22:00 之後為每個 active watch 建一筆 radar_sweep(同日去重)。
|
||||
if n, err := radarSvc.ScheduleDailySweeps(ctx, time.Now().UTC()); err != nil {
|
||||
logx.Errorf("worker %s radar daily schedule: %v", workerID, err)
|
||||
} else if n > 0 {
|
||||
logx.Infof("worker %s radar daily schedule ensured %d watch job(s)", workerID, n)
|
||||
}
|
||||
// 待追蹤到期掃描:通知站內鈴鐺,第二次無動作 → escalated。
|
||||
if crmSvc != nil {
|
||||
if n, err := crmSvc.ScanFollowUps(ctx, 0); err != nil {
|
||||
logx.Errorf("worker %s followup scan: %v", workerID, err)
|
||||
} else if n > 0 {
|
||||
logx.Infof("worker %s followup scan notified %d", workerID, n)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type workerRadarNotif struct{ App *appnotifUC.Service }
|
||||
|
||||
func (b *workerRadarNotif) InsertSystem(ctx context.Context, ownerUID int64, title, body, refType, refID string) error {
|
||||
if b == nil || b.App == nil {
|
||||
return nil
|
||||
}
|
||||
return b.App.NotifyJobState(ctx, ownerUID, refType+":"+refID, "radar", "failed", title+": "+body, 0)
|
||||
}
|
||||
|
||||
type workerCrmFollowUpNotif struct{ App *appnotifUC.Service }
|
||||
|
||||
func (b *workerCrmFollowUpNotif) NotifyFollowUp(ctx context.Context, ownerUID int64, contactID, followUpID string) error {
|
||||
if b == nil || b.App == nil {
|
||||
return nil
|
||||
}
|
||||
return b.App.NotifyJobState(ctx, ownerUID, "followup:"+followUpID, "radar_followup", "succeeded",
|
||||
"待追蹤到期:請回訪聯絡人", 100)
|
||||
}
|
||||
|
||||
type workerRadarProductContextBridge struct {
|
||||
Scout *scoutUC.Service
|
||||
}
|
||||
|
||||
func (b *workerRadarProductContextBridge) GetBrand(ctx context.Context, ownerUID int64, id string) (*radarUC.ProductBrand, error) {
|
||||
brand, err := b.Scout.GetBrand(ctx, ownerUID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &radarUC.ProductBrand{
|
||||
ID: brand.ID, OwnerUID: brand.OwnerUID, DisplayName: brand.DisplayName,
|
||||
TargetAudience: brand.TargetAudience, UpdatedAt: brand.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *workerRadarProductContextBridge) GetProduct(ctx context.Context, ownerUID int64, id string) (*radarUC.ProductCatalogProduct, error) {
|
||||
product, err := b.Scout.GetProduct(ctx, ownerUID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &radarUC.ProductCatalogProduct{
|
||||
ID: product.ID, OwnerUID: product.OwnerUID, BrandID: product.BrandID,
|
||||
Label: product.Label, ProductContext: product.ProductContext,
|
||||
PainPoints: product.PainPoints, MatchTags: product.MatchTags,
|
||||
ProviderCapabilityTerms: product.ProviderCapabilityTerms,
|
||||
ProviderExcludeTerms: product.ProviderExcludeTerms, UpdatedAt: product.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// runRadarSweep executes fetch → judge → persist for one watch.
|
||||
func runRadarSweep(ctx context.Context, jobs *jobUC.Service, radar *radarUC.Service, j *jobDomain.Job) error {
|
||||
var payload jobUC.RadarSweepPayload
|
||||
if err := json.Unmarshal([]byte(j.Payload), &payload); err != nil {
|
||||
return fmt.Errorf("radar_sweep payload: %w", err)
|
||||
}
|
||||
watchID := strings.TrimSpace(payload.WatchID)
|
||||
if watchID == "" {
|
||||
if i := strings.LastIndex(j.RefID, ":"); i > 0 {
|
||||
watchID = j.RefID[:i]
|
||||
}
|
||||
}
|
||||
if watchID == "" {
|
||||
return fmt.Errorf("radar_sweep missing watch_id")
|
||||
}
|
||||
if _, err := jobs.MarkRunningProgress(ctx, j.ID, 15, "雷達巡檢 · 抓取中"); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := radar.RunSweep(ctx, j.OwnerUID, watchID, j.ID)
|
||||
if err != nil {
|
||||
// RunSweep persists a user-facing reason on fetch failures. Return that
|
||||
// instead of leaking the crawler/provider transport response into Jobs UI.
|
||||
if res != nil && res.FetchFailed && strings.TrimSpace(res.FailedReason) != "" {
|
||||
return errors.New(res.FailedReason)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if res.FetchFailed {
|
||||
return fmt.Errorf("%s", res.FailedReason)
|
||||
}
|
||||
summary := fmt.Sprintf("雷達巡檢完成 · 新建 %d · 再次命中 %d · 判定 %d · 截斷 %d", res.Created, res.Rematched, res.Judged, res.Truncated)
|
||||
if _, err := jobs.MarkRunningProgress(ctx, j.ID, 90, summary); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = jobs.SucceedJob(ctx, j.ID, summary)
|
||||
return err
|
||||
}
|
||||
|
||||
func processOutbox(ctx context.Context, studio *studioUC.Service, lock *redislock.Lock, workerID string, ttl time.Duration) {
|
||||
locked, err := lock.Acquire(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -487,57 +345,20 @@ func processOutbox(ctx context.Context, studio *studioUC.Service, lock *redisloc
|
|||
}
|
||||
|
||||
func runScoutScan(ctx context.Context, jobs *jobUC.Service, scout *scoutUC.Service, j *jobDomain.Job) error {
|
||||
var payload struct {
|
||||
RunID string `json:"run_id"`
|
||||
scoutDomain.RunBrief
|
||||
}
|
||||
if err := json.Unmarshal([]byte(j.Payload), &payload); err != nil {
|
||||
var brief scoutDomain.RunBrief
|
||||
if err := json.Unmarshal([]byte(j.Payload), &brief); err != nil {
|
||||
return fmt.Errorf("invalid scout scan payload: %w", err)
|
||||
}
|
||||
if payload.RunID == "" {
|
||||
// 部署前排入的舊 job 只存平面 RunBrief;run id 一律等於 Job.RefID
|
||||
payload.RunID = j.RefID
|
||||
}
|
||||
if payload.RunID == "" || payload.RunID != j.RefID {
|
||||
return fmt.Errorf("scout run/job reference mismatch")
|
||||
}
|
||||
run, err := scout.GetRun(ctx, j.OwnerUID, payload.RunID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if run.JobID != j.ID {
|
||||
return fmt.Errorf("scout run job binding mismatch")
|
||||
}
|
||||
// The run owns the normalized target (20 by default). Older payloads may
|
||||
// omit target_count, so make the worker honor the run contract rather than
|
||||
// accidentally stopping after the small legacy fan-out default.
|
||||
if payload.TargetCount <= 0 {
|
||||
payload.TargetCount = run.TargetCount
|
||||
}
|
||||
if _, err := scout.StartRun(ctx, j.OwnerUID, payload.RunID, j.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := jobs.MarkRunningProgress(ctx, j.ID, 15, "海巡 · 準備搜尋來源"); err != nil {
|
||||
_ = scout.FailRun(ctx, j.OwnerUID, payload.RunID, "scout progress update failed")
|
||||
return err
|
||||
}
|
||||
posts, err := scout.RunScanForRun(ctx, j.OwnerUID, payload.RunID, &payload.RunBrief)
|
||||
posts, err := scout.RunScanFromBrief(ctx, j.OwnerUID, &brief)
|
||||
if err != nil {
|
||||
_ = scout.FailRun(ctx, j.OwnerUID, payload.RunID, err.Error())
|
||||
return err
|
||||
}
|
||||
if _, err := jobs.MarkRunningProgress(ctx, j.ID, 75, fmt.Sprintf("海巡 · 篩選完成 · 合格 %d / 目標 %d", len(posts), run.TargetCount)); err != nil {
|
||||
_ = scout.FailRun(ctx, j.OwnerUID, payload.RunID, "scout progress update failed")
|
||||
if _, err := jobs.MarkRunningProgress(ctx, j.ID, 90, fmt.Sprintf("海巡 · 已寫入 %d 筆候選", len(posts))); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := scout.PublishRun(ctx, j.OwnerUID, payload.RunID, posts); err != nil {
|
||||
_ = scout.FailRun(ctx, j.OwnerUID, payload.RunID, err.Error())
|
||||
return err
|
||||
}
|
||||
// 已發佈即成功;進度回報失敗只記 log,不可把 job 標為失敗
|
||||
if _, err := jobs.MarkRunningProgress(ctx, j.ID, 90, fmt.Sprintf("海巡 · 已發佈 %d 筆候選", len(posts))); err != nil {
|
||||
logx.Errorf("scout scan %s progress after publish: %v", j.ID, err)
|
||||
}
|
||||
_, err = jobs.SucceedJob(ctx, j.ID, fmt.Sprintf("海巡完成 · 命中 %d 筆", len(posts)))
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,166 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
jobDomain "apps/backend/internal/module/job/domain"
|
||||
jobRepo "apps/backend/internal/module/job/repository"
|
||||
jobUC "apps/backend/internal/module/job/usecase"
|
||||
scoutDomain "apps/backend/internal/module/scout/domain"
|
||||
scoutRepo "apps/backend/internal/module/scout/repository"
|
||||
scoutUC "apps/backend/internal/module/scout/usecase"
|
||||
)
|
||||
|
||||
type workerScoutProvider struct{ err error }
|
||||
|
||||
func (p workerScoutProvider) SearchThreads(_ context.Context, terms []string, limit int) ([]scoutUC.ThreadSearchResult, error) {
|
||||
if p.err != nil {
|
||||
return nil, p.err
|
||||
}
|
||||
term := "市集"
|
||||
if len(terms) > 0 && terms[0] != "" {
|
||||
term = terms[0]
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
return []scoutUC.ThreadSearchResult{{
|
||||
URL: "https://www.threads.net/@worker/post/ok", Snippet: term + "討論內容",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
type failingPublishScoutRepo struct{ scoutDomain.Repository }
|
||||
|
||||
func (failingPublishScoutRepo) PublishRunPosts(context.Context, int64, string, []*scoutDomain.Post) error {
|
||||
return errors.New("publish storage unavailable")
|
||||
}
|
||||
|
||||
func workerPayload(runID string, brief scoutDomain.RunBrief) string {
|
||||
raw, _ := json.Marshal(struct {
|
||||
RunID string `json:"run_id"`
|
||||
scoutDomain.RunBrief
|
||||
}{RunID: runID, RunBrief: brief})
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func workerRunAndJob(t *testing.T, scoutStore scoutDomain.Repository, jobStore jobDomain.Repository, status string) (*scoutUC.Service, *jobUC.Service, *jobDomain.Job, *scoutDomain.Run) {
|
||||
t.Helper()
|
||||
brief := scoutDomain.RunBrief{Intent: "市集", Mode: scoutDomain.ModeActivity, ScanTerms: []string{"市集"}}
|
||||
run := scoutDomain.NewRun("worker-run", "worker-job", 7, brief, 1)
|
||||
if err := scoutStore.CreateRun(context.Background(), run); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
job := &jobDomain.Job{ID: run.JobID, OwnerUID: 7, TemplateType: jobDomain.TemplateScoutScan,
|
||||
Status: jobDomain.StatusRunning, RefID: run.ID, Payload: workerPayload(run.ID, brief), CreatedAt: 1, UpdatedAt: 1}
|
||||
if err := jobStore.Insert(context.Background(), job); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != scoutDomain.RunQueued {
|
||||
if status == scoutDomain.RunCancelled {
|
||||
run.Status = scoutDomain.RunCancelled
|
||||
} else {
|
||||
run.Status = status
|
||||
}
|
||||
if err := scoutStore.ReplaceRunGuarded(context.Background(), 7, run.ID, []string{scoutDomain.RunQueued}, run); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
scout := scoutUC.New(scoutStore)
|
||||
scout.Provider = workerScoutProvider{}
|
||||
return scout, jobUC.New(jobStore), job, run
|
||||
}
|
||||
|
||||
func TestWorkerScoutScanSuccessPublishesExactlyOnce(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scoutStore := scoutRepo.NewMemory()
|
||||
jobStore := jobRepo.NewMemory()
|
||||
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||||
if err := runScoutScan(ctx, jobs, scout, job); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotRun, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||||
if err != nil || gotRun.Status != scoutDomain.RunSucceeded || gotRun.EligibleCount != 1 {
|
||||
t.Fatalf("run=%+v err=%v", gotRun, err)
|
||||
}
|
||||
posts, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
|
||||
if err != nil || len(posts.Items) != 1 {
|
||||
t.Fatalf("published posts=%+v err=%v", posts, err)
|
||||
}
|
||||
jobAfter, err := jobStore.FindByID(ctx, job.ID)
|
||||
if err != nil || jobAfter.Status != jobDomain.StatusSucceeded {
|
||||
t.Fatalf("job=%+v err=%v", jobAfter, err)
|
||||
}
|
||||
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||||
t.Fatal("terminal run retry should not search/publish again")
|
||||
}
|
||||
postsAgain, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
|
||||
if err != nil || len(postsAgain.Items) != 1 {
|
||||
t.Fatalf("retry changed result membership: %+v %v", postsAgain, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerScoutScanMismatchFailsBeforeSearch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scoutStore := scoutRepo.NewMemory()
|
||||
jobStore := jobRepo.NewMemory()
|
||||
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||||
job.RefID = "wrong-run"
|
||||
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||||
t.Fatal("reference mismatch should fail")
|
||||
}
|
||||
got, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||||
if err != nil || got.Status != scoutDomain.RunQueued {
|
||||
t.Fatalf("mismatch changed run=%+v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerScoutScanProviderFailureFailsRunAndHidesResults(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scoutStore := scoutRepo.NewMemory()
|
||||
jobStore := jobRepo.NewMemory()
|
||||
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||||
scout.Provider = workerScoutProvider{err: errors.New("provider timeout: token redacted")}
|
||||
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||||
t.Fatal("provider failure should fail")
|
||||
}
|
||||
got, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||||
if err != nil || got.Status != scoutDomain.RunFailed || got.Error == "" {
|
||||
t.Fatalf("failed run=%+v err=%v", got, err)
|
||||
}
|
||||
posts, err := scoutStore.ListRunPosts(ctx, 7, run.ID, 1, 10)
|
||||
if err != nil || len(posts.Items) != 0 {
|
||||
t.Fatalf("failed run exposed posts=%+v err=%v", posts, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerScoutScanPublishFailureFailsRun(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
base := scoutRepo.NewMemory()
|
||||
scoutStore := failingPublishScoutRepo{Repository: base}
|
||||
jobStore := jobRepo.NewMemory()
|
||||
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunQueued)
|
||||
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||||
t.Fatal("publish failure should fail")
|
||||
}
|
||||
got, err := base.GetRun(ctx, 7, run.ID)
|
||||
if err != nil || got.Status != scoutDomain.RunFailed {
|
||||
t.Fatalf("publish failed run=%+v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerScoutScanCancelledRunDoesNotRestart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
scoutStore := scoutRepo.NewMemory()
|
||||
jobStore := jobRepo.NewMemory()
|
||||
scout, jobs, job, run := workerRunAndJob(t, scoutStore, jobStore, scoutDomain.RunCancelled)
|
||||
if err := runScoutScan(ctx, jobs, scout, job); err == nil {
|
||||
t.Fatal("cancelled run should not restart")
|
||||
}
|
||||
got, err := scoutStore.GetRun(ctx, 7, run.ID)
|
||||
if err != nil || got.Status != scoutDomain.RunCancelled {
|
||||
t.Fatalf("cancelled run changed=%+v err=%v", got, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,6 @@ Optional: `SCOUT_CRAWLER_PORT=8891` changes the local port.
|
|||
- `dev_mode=false` does not use this service; it uses the configured API provider.
|
||||
- `dev_mode=true` requires a freshly synchronized Chrome session. Missing or expired sessions fail with a user-actionable error.
|
||||
- The service supports `/v1/threads/search` and `/v1/threads/resolve`; both require `Authorization: Bearer <CrawlerToken>`.
|
||||
- **Dual-track search** (熱門 + 最新): each query hits Top and Recent in parallel; merge is **Recent-primary order + Top fill** (both is a badge only — does not reshuffle relevance). Published age is returned as metadata and is not hard-dropped; backend score may down-rank older candidates. Response: `track`, `serp_rank`, `published_at`.
|
||||
|
||||
## Operations
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,9 @@
|
|||
import { chromium, type BrowserContext, type Page } from "playwright";
|
||||
import { chromium, type Page } from "playwright";
|
||||
import { createServer } from "node:http";
|
||||
|
||||
type SearchRequest = { storage_state?: string; terms?: string[]; limit?: number };
|
||||
type ResolveRequest = { storage_state?: string; permalink?: string };
|
||||
|
||||
type Post = {
|
||||
permalink: string;
|
||||
author: string;
|
||||
text: string;
|
||||
track?: "top" | "recent" | "both";
|
||||
serp_rank?: number;
|
||||
published_at?: string;
|
||||
published_label?: string;
|
||||
};
|
||||
type Post = { permalink: string; author: string; text: string };
|
||||
|
||||
const port = Number(process.env.SCOUT_CRAWLER_PORT || 8891);
|
||||
const token = process.env.SCOUT_CRAWLER_TOKEN || "";
|
||||
|
|
@ -26,258 +17,42 @@ function isThreadsURL(value: string): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
function normalizePermalink(href: string): string {
|
||||
const absolute = href.startsWith("http") ? href : `https://www.threads.com${href}`;
|
||||
try {
|
||||
const u = new URL(absolute);
|
||||
u.hash = "";
|
||||
u.search = "";
|
||||
u.pathname = u.pathname.replace(/\/+$/, "");
|
||||
return u.toString();
|
||||
} catch {
|
||||
return absolute.split("?")[0]!.replace(/\/+$/, "");
|
||||
}
|
||||
}
|
||||
|
||||
/** 口語錨點:不能單獨代表主題相關 */
|
||||
const ANCHORS = new Set(["求推薦", "推薦", "分享", "心得", "活動", "怎麼辦", "詢問", "討論", "有人知道", "請問"]);
|
||||
|
||||
/**
|
||||
* 正文須含主題核(非口語錨點)。「外包 求推薦」→ 必須出現「外包」。
|
||||
* 防搜尋頁沒載入/抓到推薦流時灌進無關貼。
|
||||
*/
|
||||
function textMatchesQuery(text: string, query: string): boolean {
|
||||
const body = text.replace(/\s+/g, "").toLowerCase();
|
||||
if (!body) return false;
|
||||
const q = query.replace(/\u3000/g, " ").trim();
|
||||
if (!q) return true;
|
||||
const parts = q.split(/\s+/).filter((p) => [...p].length >= 2);
|
||||
const tokens = parts.length ? parts : [...q].length >= 2 ? [q] : [];
|
||||
const content: string[] = [];
|
||||
const anchors: string[] = [];
|
||||
for (const p of tokens) {
|
||||
const t = p.toLowerCase();
|
||||
if (ANCHORS.has(t)) anchors.push(t.replace(/\s+/g, ""));
|
||||
else content.push(t.replace(/\s+/g, ""));
|
||||
}
|
||||
if (content.length > 0) {
|
||||
return content.some((c) => body.includes(c));
|
||||
}
|
||||
if (anchors.length > 0) {
|
||||
return anchors.some((a) => body.includes(a));
|
||||
}
|
||||
// 查詢過短無法斷詞時,退回要求正文含原始查詢字串,避免放行整條推薦流
|
||||
return body.includes(q.replace(/\s+/g, "").toLowerCase());
|
||||
}
|
||||
|
||||
function parsePublishedFromCardText(text: string): { iso?: string; label?: string } {
|
||||
const raw = text.replace(/\s+/g, " ").trim();
|
||||
if (!raw) return {};
|
||||
const head = raw.slice(0, 100);
|
||||
const now = Date.now();
|
||||
const relPatterns: Array<{ re: RegExp; ms: (n: number) => number; label: (n: number) => string }> = [
|
||||
{ re: /剛剛|just now/i, ms: () => 0, label: () => "剛剛" },
|
||||
{ re: /(\d+)\s*(秒|s|sec)/i, ms: (n) => n * 1000, label: (n) => `${n}秒` },
|
||||
{ re: /(\d+)\s*(分|分鐘|m|min)/i, ms: (n) => n * 60_000, label: (n) => `${n}分` },
|
||||
{ re: /(\d+)\s*(小時|時|h|hr)/i, ms: (n) => n * 3_600_000, label: (n) => `${n}小時` },
|
||||
{ re: /(\d+)\s*(天|日|d|day)/i, ms: (n) => n * 86_400_000, label: (n) => `${n}天` },
|
||||
{ re: /(\d+)\s*(週|周|w|week)/i, ms: (n) => n * 7 * 86_400_000, label: (n) => `${n}週` },
|
||||
{ re: /(\d+)\s*(月|mo|month)/i, ms: (n) => n * 30 * 86_400_000, label: (n) => `${n}月` },
|
||||
{ re: /(\d+)\s*年前/i, ms: (n) => n * 365 * 86_400_000, label: (n) => `${n}年` },
|
||||
];
|
||||
for (const p of relPatterns) {
|
||||
const m = head.match(p.re);
|
||||
if (!m) continue;
|
||||
const n = m[1] ? Number(m[1]) : 0;
|
||||
if (!Number.isFinite(n) && !/剛剛|just now/i.test(m[0])) continue;
|
||||
return { iso: new Date(now - p.ms(Number.isFinite(n) ? n : 0)).toISOString(), label: p.label(Number.isFinite(n) ? n : 0) };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 page.evaluate 抽 SERP 貼文:比 locator 祖先更穩,並用 query 過濾無關卡。
|
||||
* 只掃 document 出現順序中的 /post/ 連結,略過明顯非結果區(nav 等難以 100% 排除時靠 query 過濾)。
|
||||
*/
|
||||
async function readPosts(page: Page, query: string, limit: number): Promise<Post[]> {
|
||||
type Raw = { href: string; author: string; text: string };
|
||||
const raws = await page.evaluate((maxScan: number) => {
|
||||
const out: Raw[] = [];
|
||||
const seen = new Set<string>();
|
||||
const anchors = Array.from(document.querySelectorAll('a[href*="/post/"]')) as HTMLAnchorElement[];
|
||||
for (const a of anchors) {
|
||||
if (out.length >= maxScan) break;
|
||||
const href = a.getAttribute("href") || "";
|
||||
if (!href.includes("/post/")) continue;
|
||||
// 略過 nav / header 內連結
|
||||
if (a.closest("nav, header, [role='navigation']")) continue;
|
||||
const key = href.split("?")[0] || href;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
// 找小範圍卡片:往上最多 8 層,取文字長度 20–800 的最近祖先
|
||||
let el: HTMLElement | null = a;
|
||||
let best = "";
|
||||
for (let depth = 0; depth < 8 && el; depth++) {
|
||||
const t = (el.innerText || "").replace(/\s+/g, " ").trim();
|
||||
if (t.length >= 20 && t.length <= 1200) {
|
||||
best = t;
|
||||
// 再往上若突然暴衝(整欄 feed)就停在 best
|
||||
const parent = el.parentElement;
|
||||
if (parent) {
|
||||
const pt = (parent.innerText || "").replace(/\s+/g, " ").trim();
|
||||
if (pt.length > t.length * 3 && pt.length > 1500) break;
|
||||
}
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
if (best.length < 12) {
|
||||
best = (a.innerText || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
if (best.length < 8) continue;
|
||||
const author = href.match(/@([^/]+)\/post/)?.[1] || "";
|
||||
out.push({ href, author, text: best.slice(0, 2000) });
|
||||
}
|
||||
return out;
|
||||
}, Math.min(limit * 4, 80));
|
||||
|
||||
const posts: Post[] = [];
|
||||
let rank = 0;
|
||||
for (const r of raws) {
|
||||
if (posts.length >= limit) break;
|
||||
const permalink = normalizePermalink(r.href);
|
||||
async function readPosts(page: Page, limit: number): Promise<Post[]> {
|
||||
const posts = new Map<string, Post>();
|
||||
const links = page.locator('a[href*="/post/"]');
|
||||
const count = Math.min(await links.count(), 50);
|
||||
for (let i = 0; i < count && posts.size < limit; i++) {
|
||||
const link = links.nth(i);
|
||||
const href = await link.getAttribute("href").catch(() => null);
|
||||
if (!href) continue;
|
||||
const permalink = href.startsWith("http") ? href : `https://www.threads.com${href}`;
|
||||
if (!isThreadsURL(permalink)) continue;
|
||||
if (!textMatchesQuery(r.text, query)) continue;
|
||||
const { iso, label } = parsePublishedFromCardText(r.text);
|
||||
rank += 1;
|
||||
posts.push({
|
||||
permalink,
|
||||
author: r.author,
|
||||
text: r.text,
|
||||
serp_rank: rank,
|
||||
published_at: iso,
|
||||
published_label: label,
|
||||
});
|
||||
const author = href.match(/@([^/]+)\/post/)?.[1] || "";
|
||||
const scope = link.locator("xpath=ancestor::div[position()<=6]").first();
|
||||
const text = (await scope.innerText().catch(() => "")).trim();
|
||||
if (text.length < 5) continue;
|
||||
posts.set(permalink, { permalink, author, text: text.slice(0, 2000) });
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
|
||||
function searchURL(query: string, track: "top" | "recent"): string {
|
||||
const q = encodeURIComponent(query);
|
||||
// Top = default;Recent = filter=recent(與官方 web 一致)
|
||||
if (track === "recent") {
|
||||
return `https://www.threads.com/search?q=${q}&serp_type=default&filter=recent`;
|
||||
}
|
||||
return `https://www.threads.com/search?q=${q}&serp_type=default`;
|
||||
}
|
||||
|
||||
async function searchTrack(page: Page, query: string, track: "top" | "recent", limit: number): Promise<Post[]> {
|
||||
const url = searchURL(query, track);
|
||||
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 45_000 });
|
||||
// 必須仍在搜尋頁,否則 captcha/導向首頁會抓到無關貼文
|
||||
const landed = page.url();
|
||||
if (landed.includes("/login") || !landed.includes("/search")) {
|
||||
const body = await page.locator("body").innerText().catch(() => "");
|
||||
if (body.includes("登入") || landed.includes("/login")) throw new Error("crawler session expired");
|
||||
// 不在 search:直接空結果,寧可少抓也不亂抓推薦流
|
||||
return [];
|
||||
}
|
||||
const body = await page.locator("body").innerText().catch(() => "");
|
||||
if (body.includes("登入") && body.length < 400) throw new Error("crawler session expired");
|
||||
|
||||
await page.waitForSelector('a[href*="/post/"]', { timeout: 12_000 }).catch(() => undefined);
|
||||
await page.waitForTimeout(900);
|
||||
// 溫和捲動 2 次即可;過度捲動容易混進「為你推薦」
|
||||
await page.mouse.wheel(0, 1000);
|
||||
await page.waitForTimeout(700);
|
||||
await page.mouse.wheel(0, 1000);
|
||||
await page.waitForTimeout(600);
|
||||
|
||||
const posts = await readPosts(page, query, limit);
|
||||
return posts.map((p) => ({ ...p, track }));
|
||||
}
|
||||
|
||||
/** Recent 原序為主,Top 獨有補後;both 僅標記。合併後再做一次 query 相關性過濾。 */
|
||||
function mergeRecentPrimary(top: Post[], recent: Post[], query: string, limit: number): Post[] {
|
||||
const topMap = new Map<string, Post>();
|
||||
for (const p of top) topMap.set(normalizePermalink(p.permalink), p);
|
||||
|
||||
const out: Post[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const p of recent) {
|
||||
if (!textMatchesQuery(p.text, query)) continue;
|
||||
const key = normalizePermalink(p.permalink);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ ...p, track: topMap.has(key) ? "both" : "recent" });
|
||||
if (out.length >= limit) return out;
|
||||
}
|
||||
for (const p of top) {
|
||||
if (!textMatchesQuery(p.text, query)) continue;
|
||||
const key = normalizePermalink(p.permalink);
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push({ ...p, track: "top" });
|
||||
if (out.length >= limit) break;
|
||||
}
|
||||
return out;
|
||||
return [...posts.values()];
|
||||
}
|
||||
|
||||
async function search(storageState: string, terms: string[], limit: number): Promise<Post[]> {
|
||||
const state = JSON.parse(storageState) as { cookies?: unknown[] };
|
||||
if (!Array.isArray(state.cookies) || state.cookies.length === 0) throw new Error("crawler session is invalid");
|
||||
|
||||
// 上層 fan-out 一詞一搜;若誤傳多詞只取第一組
|
||||
const query = (terms.map((t) => t.trim()).filter(Boolean)[0] || "").slice(0, 180);
|
||||
if (!query) return [];
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const context: BrowserContext = await browser.newContext({
|
||||
storageState: state,
|
||||
locale: "zh-TW",
|
||||
timezoneId: "Asia/Taipei",
|
||||
userAgent:
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
||||
});
|
||||
const perTrack = Math.min(Math.max(limit, 10), 24);
|
||||
const pageTop = await context.newPage();
|
||||
const pageRecent = await context.newPage();
|
||||
// allSettled 保留成功軌結果;失敗軌各自重試一次(session 失效重試無意義)
|
||||
const isSessionError = (r: unknown) => r instanceof Error && r.message.includes("session");
|
||||
const [topResult, recentResult] = await Promise.allSettled([
|
||||
searchTrack(pageTop, query, "top", perTrack),
|
||||
searchTrack(pageRecent, query, "recent", perTrack),
|
||||
]);
|
||||
let top: Post[] = topResult.status === "fulfilled" ? topResult.value : [];
|
||||
let recent: Post[] = recentResult.status === "fulfilled" ? recentResult.value : [];
|
||||
const firstFailure =
|
||||
topResult.status === "rejected"
|
||||
? topResult.reason
|
||||
: recentResult.status === "rejected"
|
||||
? recentResult.reason
|
||||
: undefined;
|
||||
if (topResult.status === "rejected" && !isSessionError(topResult.reason)) {
|
||||
try {
|
||||
top = await searchTrack(pageTop, query, "top", perTrack);
|
||||
} catch {
|
||||
/* keep empty */
|
||||
}
|
||||
}
|
||||
if (recentResult.status === "rejected" && !isSessionError(recentResult.reason)) {
|
||||
try {
|
||||
recent = await searchTrack(pageRecent, query, "recent", perTrack);
|
||||
} catch {
|
||||
/* keep empty */
|
||||
}
|
||||
}
|
||||
if (firstFailure !== undefined && top.length === 0 && recent.length === 0) throw firstFailure;
|
||||
await pageTop.close().catch(() => undefined);
|
||||
await pageRecent.close().catch(() => undefined);
|
||||
const merged = mergeRecentPrimary(top, recent, query, limit);
|
||||
const context = await browser.newContext({ storageState: state, locale: "zh-TW", timezoneId: "Asia/Taipei" });
|
||||
const page = await context.newPage();
|
||||
const query = terms.filter(Boolean).join(" ").slice(0, 180);
|
||||
await page.goto(`https://www.threads.com/search?q=${encodeURIComponent(query)}&serp_type=default`, { waitUntil: "domcontentloaded", timeout: 45_000 });
|
||||
const body = await page.locator("body").innerText().catch(() => "");
|
||||
if (page.url().includes("/login") || body.includes("登入")) throw new Error("crawler session expired");
|
||||
await page.waitForSelector('a[href*="/post/"]', { timeout: 12_000 }).catch(() => undefined);
|
||||
await page.mouse.wheel(0, 900);
|
||||
await page.waitForTimeout(1000);
|
||||
const posts = await readPosts(page, limit);
|
||||
await context.close();
|
||||
return merged;
|
||||
return posts;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
|
@ -289,21 +64,12 @@ function shortcodeFromPermalink(permalink: string): string {
|
|||
|
||||
function findMediaID(value: unknown, shortcode: string): string {
|
||||
if (!value || typeof value !== "object") return "";
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
const id = findMediaID(item, shortcode);
|
||||
if (id) return id;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (Array.isArray(value)) { for (const item of value) { const id = findMediaID(item, shortcode); if (id) return id; } return ""; }
|
||||
const record = value as Record<string, unknown>;
|
||||
const code = String(record.code || record.shortcode || "");
|
||||
const id = String(record.id || record.pk || record.media_id || "");
|
||||
if (code === shortcode && /^\d{10,}$/.test(id)) return id;
|
||||
for (const child of Object.values(record)) {
|
||||
const found = findMediaID(child, shortcode);
|
||||
if (found) return found;
|
||||
}
|
||||
for (const child of Object.values(record)) { const found = findMediaID(child, shortcode); if (found) return found; }
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
@ -334,36 +100,28 @@ async function resolve(storageState: string, permalink: string): Promise<string>
|
|||
const responseReads: Promise<void>[] = [];
|
||||
page.on("response", async (response) => {
|
||||
if (mediaID || !/graphql|threads|instagram/.test(response.url())) return;
|
||||
responseReads.push(
|
||||
(async () => {
|
||||
try {
|
||||
const raw = await response.text();
|
||||
if (!mediaID) {
|
||||
try {
|
||||
mediaID = findMediaID(JSON.parse(raw), shortcode);
|
||||
} catch {
|
||||
/* non-JSON */
|
||||
}
|
||||
}
|
||||
if (!mediaID) mediaID = findMediaIDInText(raw, shortcode);
|
||||
} catch {
|
||||
/* ignored */
|
||||
responseReads.push((async () => {
|
||||
try {
|
||||
const raw = await response.text();
|
||||
if (!mediaID) {
|
||||
try { mediaID = findMediaID(JSON.parse(raw), shortcode); } catch { /* non-JSON response */ }
|
||||
}
|
||||
})(),
|
||||
);
|
||||
if (!mediaID) mediaID = findMediaIDInText(raw, shortcode);
|
||||
} catch { /* ignored */ }
|
||||
})());
|
||||
});
|
||||
await page.goto(permalink, { waitUntil: "domcontentloaded", timeout: 45_000 });
|
||||
const body = await page.locator("body").innerText().catch(() => "");
|
||||
if (page.url().includes("/login") || body.includes("登入")) throw new Error("crawler session expired");
|
||||
await page.waitForTimeout(2500);
|
||||
await Promise.allSettled(responseReads);
|
||||
if (!mediaID) mediaID = findMediaIDInText(await page.content(), shortcode);
|
||||
if (!mediaID) {
|
||||
mediaID = findMediaIDInText(await page.content(), shortcode);
|
||||
}
|
||||
await context.close();
|
||||
if (!mediaID) throw new Error("Threads media ID could not be resolved");
|
||||
if (!mediaID) throw new Error("Threads media ID could not be resolved")
|
||||
return mediaID;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
} finally { await browser.close(); }
|
||||
}
|
||||
|
||||
if (!token) throw new Error("SCOUT_CRAWLER_TOKEN is required");
|
||||
|
|
@ -381,10 +139,7 @@ createServer(async (req, res) => {
|
|||
const body = await new Promise<string>((resolve, reject) => {
|
||||
let raw = "";
|
||||
req.setEncoding("utf8");
|
||||
req.on("data", (chunk) => {
|
||||
raw += chunk;
|
||||
if (raw.length > 300_000) req.destroy();
|
||||
});
|
||||
req.on("data", (chunk) => { raw += chunk; if (raw.length > 300_000) req.destroy(); });
|
||||
req.on("end", () => resolve(raw));
|
||||
req.on("error", reject);
|
||||
});
|
||||
|
|
@ -395,16 +150,10 @@ createServer(async (req, res) => {
|
|||
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ media_id: mediaID }));
|
||||
} else {
|
||||
const input = JSON.parse(body) as SearchRequest;
|
||||
const posts = await search(
|
||||
String(input.storage_state || ""),
|
||||
Array.isArray(input.terms) ? input.terms.slice(0, 12) : [],
|
||||
Math.min(Math.max(Number(input.limit) || 10, 1), 30),
|
||||
);
|
||||
const posts = await search(String(input.storage_state || ""), Array.isArray(input.terms) ? input.terms.slice(0, 12) : [], Math.min(Math.max(Number(input.limit) || 10, 1), 30));
|
||||
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({ posts }));
|
||||
}
|
||||
} catch (error) {
|
||||
res.writeHead(422, { "content-type": "application/json" }).end(
|
||||
JSON.stringify({ error: error instanceof Error ? error.message : "crawler failed" }),
|
||||
);
|
||||
res.writeHead(422, { "content-type": "application/json" }).end(JSON.stringify({ error: error instanceof Error ? error.message : "crawler failed" }));
|
||||
}
|
||||
}).listen(port, "127.0.0.1");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# Call Home 會議重點與 Action Items
|
||||
|
||||
**日期:** 2026/07/28
|
||||
|
||||
## 會議重點
|
||||
|
||||
### 1. 整合方向
|
||||
|
||||
Call Home 可拆成三個部分:
|
||||
|
||||
1. **Trigger/State**:事件收集、分類、去重與 Recovery。
|
||||
2. **Delivery**:Email、SMS、Phone、ServiceNow、Salesforce。
|
||||
3. **Service Process**:自動開單、Ticket 狀態與結案責任。
|
||||
|
||||
### 2. 可以直接重用的能力
|
||||
|
||||
- **SSM 團隊提供:** Event Mapping、事件白名單、資料收集、去重與 Recovery 邏輯。
|
||||
- **SCC-FLEX 提供:** Policy Engine、統一 Webhook、多通路通知與 Salesforce API。
|
||||
- **Local Administrator:** 沿用 SCC-FLEX 現有 Recipient/Receiver 設定,不另外開發獨立模組。
|
||||
|
||||
### 3. 目前主要缺口
|
||||
|
||||
- 尚未定義統一 Event Payload 與 Correlation Key。
|
||||
- SCC-FLEX 尚缺完整 Deduplication、Soft/Hard State 與 Event Lifecycle。
|
||||
- Service Team 可接受的 Event 白名單尚未確認。
|
||||
- Device Recovered 與 Ticket Closed 的關係尚未定義。
|
||||
- Salesforce Ticket 的 Owner、Close 權限與 Ask-to-Close 流程尚未確認。
|
||||
- Call Home License 模式尚未確認。
|
||||
|
||||
### 4. 初步共識
|
||||
|
||||
- Supermicro Service 情境傾向**自動建立 Ticket**。
|
||||
- 自動開單前必須先完成 Event 白名單、去重及 Idempotency。
|
||||
- 初期先支援 Compute/Supermicro Hardware。
|
||||
- CDU、PDU、Liquid Cooling 等產品後續再評估。
|
||||
|
||||
## Action Items
|
||||
|
||||
| Owner | Action Item | Status |
|
||||
|---|---|---|
|
||||
| Daniel | 依 Trigger、Delivery、Service Process 三部分整理整合需求與責任邊界。 | 待整理 |
|
||||
| Daniel | 建立跨團隊討論群組並發布會議記錄。 | 待處理 |
|
||||
| Daniel/PM | 確認 DCM Single Key、Service Key、每台 Host 計費及 Support Advisor 的關係。 | 待確認 |
|
||||
| PM/Service Team | 提供可自動開單的 Event 白名單、必要 Payload/附件及維護 Owner。 | 待確認 |
|
||||
| SSM 團隊 | 提供 Event Mapping、Trigger List、Deduplication、Recovery 與資料收集規則。 | 待指派 |
|
||||
| SCC-FLEX 團隊 | 定義 Event Payload、Correlation Key、Soft/Hard State、Deduplication 與 Idempotency。 | 待排程 |
|
||||
| Daniel/IT/Service Team | 確認 Salesforce API Owner、Ticket Update、Close 與 Ask-to-Close 規則。 | 待確認 |
|
||||
| PM/產品團隊 | 決定 Event 選擇方式及 Ticket List/Ask-to-Close 是否列入產品範圍。 | 待討論 |
|
||||
|
||||
## 下一步
|
||||
|
||||
下一次會議只需要確認四件事:
|
||||
|
||||
1. Service Team Event 白名單。
|
||||
2. SSM 可提供的資料與介面。
|
||||
3. Salesforce Ticket Lifecycle 與責任人。
|
||||
4. License 與產品範圍。
|
||||
|
|
@ -57,8 +57,6 @@ type (
|
|||
PostCode string `json:"post_code,optional"`
|
||||
PreferredLanguage string `json:"preferred_language,optional"`
|
||||
Currency string `json:"currency,optional"`
|
||||
// skipped | completed;只寫一次,已結束後再送會被忽略
|
||||
OnboardingStatus string `json:"onboarding_status,optional"`
|
||||
CurrentPassword string `json:"current_password,optional"`
|
||||
NewPassword string `json:"new_password,optional"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,6 @@ type MemberPublic {
|
|||
PostCode string `json:"post_code,optional"`
|
||||
PreferredLanguage string `json:"preferred_language,optional"`
|
||||
Currency string `json:"currency,optional"`
|
||||
// pending | skipped | completed;空=尚未寫入(視同 pending,舊帳可後端補 completed)
|
||||
OnboardingStatus string `json:"onboarding_status,optional"`
|
||||
OnboardingDoneAt int64 `json:"onboarding_done_at,optional"`
|
||||
Identities []IdentityPublic `json:"identities,optional"`
|
||||
JoinedAt int64 `json:"joined_at,optional"`
|
||||
CreatedAt int64 `json:"created_at,optional"`
|
||||
|
|
|
|||
|
|
@ -1,285 +0,0 @@
|
|||
syntax = "v1"
|
||||
|
||||
// demand-radar: contacts / touches / follow-ups / conversion / stats
|
||||
// spec: docs/product/demand-radar/spec.md §5.2
|
||||
// 未實作能力一律回 501(crmDomain.ErrNotReady),禁止 102000 空成功。
|
||||
// 命名紅線:不得以 lead 指稱銷售線索。
|
||||
|
||||
type (
|
||||
// ---------- Contact ----------
|
||||
ContactPublic {
|
||||
Id string `json:"id"`
|
||||
SourcePlatform string `json:"source_platform"`
|
||||
AuthorHandle string `json:"author_handle"`
|
||||
DisplayName string `json:"display_name,optional"`
|
||||
Stage string `json:"stage"` // new_found | engaged | dm_sent | replied | quoted | won | lost
|
||||
NeedsFollowUp bool `json:"needs_follow_up"`
|
||||
FollowUpDays int `json:"follow_up_days"`
|
||||
LastTouchAt int64 `json:"last_touch_at,optional"`
|
||||
OpportunityIds []string `json:"opportunity_ids"`
|
||||
OpportunityCount int `json:"opportunity_count"`
|
||||
MergedFrom []string `json:"merged_from,optional"`
|
||||
TopIntentBand string `json:"top_intent_band,optional"`
|
||||
TopIntentScore int `json:"top_intent_score,optional"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
ContactBrief {
|
||||
Id string `json:"id"`
|
||||
SourcePlatform string `json:"source_platform"`
|
||||
AuthorHandle string `json:"author_handle"`
|
||||
DisplayName string `json:"display_name,optional"`
|
||||
Stage string `json:"stage"`
|
||||
}
|
||||
|
||||
// StageCount — 八格視圖用;stage 值另含 needs_follow_up(跨階段檢視,非階段值)
|
||||
StageCount {
|
||||
Stage string `json:"stage"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
ListContactsReq {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=20"`
|
||||
Query string `form:"query,optional"`
|
||||
Stage string `form:"stage,optional"`
|
||||
// FollowUp: true | false(留空=不篩選)
|
||||
FollowUp string `form:"follow_up,optional"`
|
||||
Band string `form:"band,optional"`
|
||||
// Sort: last_touch_at | intent_score(預設 last_touch_at 倒序)
|
||||
Sort string `form:"sort,optional"`
|
||||
}
|
||||
|
||||
ContactListData {
|
||||
List []ContactPublic `json:"list"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
StageCounts []StageCount `json:"stage_counts"`
|
||||
}
|
||||
|
||||
ContactTouchPublic {
|
||||
Id string `json:"id"`
|
||||
ContactId string `json:"contact_id"`
|
||||
Type string `json:"type"` // stage | reply | note | conversion
|
||||
FromStage string `json:"from_stage,optional"`
|
||||
ToStage string `json:"to_stage,optional"`
|
||||
Body string `json:"body,optional"`
|
||||
ActorUid int64 `json:"actor_uid"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
ContactOpportunityBrief {
|
||||
Id string `json:"id"`
|
||||
Permalink string `json:"permalink"`
|
||||
Text string `json:"text"`
|
||||
IntentScore int `json:"intent_score"`
|
||||
IntentBand string `json:"intent_band"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
GetContactReq {
|
||||
Id string `path:"id"`
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=20"`
|
||||
}
|
||||
|
||||
ContactDetailData {
|
||||
Contact ContactPublic `json:"contact"`
|
||||
Touches []ContactTouchPublic `json:"touches"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
Opportunities []ContactOpportunityBrief `json:"opportunities"`
|
||||
}
|
||||
|
||||
ContactIdReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
UpdateContactStageReq {
|
||||
Id string `path:"id"`
|
||||
Stage string `json:"stage"`
|
||||
Note string `json:"note,optional"`
|
||||
}
|
||||
|
||||
SetContactFollowUpReq {
|
||||
Id string `path:"id"`
|
||||
NeedsFollowUp bool `json:"needs_follow_up"`
|
||||
Days int `json:"days,optional"`
|
||||
}
|
||||
|
||||
CreateContactNoteReq {
|
||||
Id string `path:"id"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
MergeContactReq {
|
||||
Id string `path:"id"`
|
||||
SourceContactId string `json:"source_contact_id"`
|
||||
}
|
||||
|
||||
UnmergeContactReq {
|
||||
Id string `path:"id"`
|
||||
MergedContactId string `json:"merged_contact_id"`
|
||||
}
|
||||
|
||||
// ---------- Conversion(寫既有 growth_outcomes,不另建成交帳) ----------
|
||||
CreateCrmConversionReq {
|
||||
Id string `path:"id"`
|
||||
Amount float64 `json:"amount,optional"`
|
||||
Currency string `json:"currency,optional"`
|
||||
Note string `json:"note,optional"`
|
||||
}
|
||||
|
||||
UpdateCrmConversionReq {
|
||||
Id string `path:"id"`
|
||||
Amount float64 `json:"amount,optional"`
|
||||
Currency string `json:"currency,optional"`
|
||||
Note string `json:"note,optional"`
|
||||
}
|
||||
|
||||
DeleteCrmConversionReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
CrmConversionData {
|
||||
ContactId string `json:"contact_id"`
|
||||
OutcomeId string `json:"outcome_id,optional"`
|
||||
Stage string `json:"stage"`
|
||||
Amount float64 `json:"amount,optional"`
|
||||
Currency string `json:"currency,optional"`
|
||||
Note string `json:"note,optional"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ---------- FollowUp ----------
|
||||
FollowUpPublic {
|
||||
Id string `json:"id"`
|
||||
ContactId string `json:"contact_id"`
|
||||
DueAt int64 `json:"due_at"`
|
||||
Status string `json:"status"` // scheduled | notified | done | snoozed | escalated
|
||||
NotifiedCount int `json:"notified_count"`
|
||||
Contact ContactBrief `json:"contact"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
ListFollowUpsReq {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=20"`
|
||||
Status string `form:"status,optional"`
|
||||
}
|
||||
|
||||
FollowUpListData {
|
||||
List []FollowUpPublic `json:"list"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
FollowUpIdReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
SnoozeFollowUpReq {
|
||||
Id string `path:"id"`
|
||||
Days int `json:"days"`
|
||||
}
|
||||
|
||||
GenerateFollowUpMessageReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
GenerateFollowUpMessageData {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// ---------- Stats(樣本 < 5 只給絕對數) ----------
|
||||
TermConversionStat {
|
||||
Term string `json:"term"`
|
||||
Accepted int `json:"accepted"`
|
||||
Replied int `json:"replied"`
|
||||
Won int `json:"won"`
|
||||
ConversionRate float64 `json:"conversion_rate,optional"`
|
||||
InsufficientSample bool `json:"insufficient_sample"`
|
||||
}
|
||||
|
||||
VariantConversionStat {
|
||||
Variant string `json:"variant"`
|
||||
Used int `json:"used"`
|
||||
Replied int `json:"replied"`
|
||||
Won int `json:"won"`
|
||||
SuccessRate float64 `json:"success_rate,optional"`
|
||||
InsufficientSample bool `json:"insufficient_sample"`
|
||||
}
|
||||
|
||||
SourceConversionStat {
|
||||
Source string `json:"source"` // radar | scout | manual_import
|
||||
Won int `json:"won"`
|
||||
InsufficientSample bool `json:"insufficient_sample"`
|
||||
}
|
||||
|
||||
CrmStatsReq {
|
||||
From int64 `form:"from,optional"`
|
||||
To int64 `form:"to,optional"`
|
||||
}
|
||||
|
||||
CrmStatsData {
|
||||
Terms []TermConversionStat `json:"terms"`
|
||||
Variants []VariantConversionStat `json:"variants"`
|
||||
Sources []SourceConversionStat `json:"sources"`
|
||||
// UnavailableDimensions 列出「尚未實作」而非「查無資料」的維度,
|
||||
// 前端才不會把功能缺口誤顯示為使用者還沒有數據。
|
||||
UnavailableDimensions []string `json:"unavailable_dimensions"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
group: crm
|
||||
prefix: /api/v1/crm
|
||||
middleware: AuthJWT
|
||||
)
|
||||
service gateway {
|
||||
@handler ListContacts
|
||||
get /contacts (ListContactsReq) returns (ContactListData)
|
||||
|
||||
@handler GetContact
|
||||
get /contacts/:id (GetContactReq) returns (ContactDetailData)
|
||||
|
||||
@handler DeleteContact
|
||||
delete /contacts/:id (ContactIdReq) returns (OkData)
|
||||
|
||||
@handler UpdateContactStage
|
||||
post /contacts/:id/stage (UpdateContactStageReq) returns (ContactPublic)
|
||||
|
||||
@handler SetContactFollowUp
|
||||
post /contacts/:id/follow-up (SetContactFollowUpReq) returns (ContactPublic)
|
||||
|
||||
@handler CreateContactNote
|
||||
post /contacts/:id/notes (CreateContactNoteReq) returns (ContactTouchPublic)
|
||||
|
||||
@handler MergeContact
|
||||
post /contacts/:id/merge (MergeContactReq) returns (ContactPublic)
|
||||
|
||||
@handler UnmergeContact
|
||||
post /contacts/:id/unmerge (UnmergeContactReq) returns (ContactPublic)
|
||||
|
||||
@handler CreateContactConversion
|
||||
post /contacts/:id/conversion (CreateCrmConversionReq) returns (CrmConversionData)
|
||||
|
||||
@handler UpdateContactConversion
|
||||
put /contacts/:id/conversion (UpdateCrmConversionReq) returns (CrmConversionData)
|
||||
|
||||
@handler DeleteContactConversion
|
||||
delete /contacts/:id/conversion (DeleteCrmConversionReq) returns (OkData)
|
||||
|
||||
@handler ListFollowUps
|
||||
get /followups (ListFollowUpsReq) returns (FollowUpListData)
|
||||
|
||||
@handler DoneFollowUp
|
||||
post /followups/:id/done (FollowUpIdReq) returns (FollowUpPublic)
|
||||
|
||||
@handler SnoozeFollowUp
|
||||
post /followups/:id/snooze (SnoozeFollowUpReq) returns (FollowUpPublic)
|
||||
|
||||
@handler GenerateFollowUpMessage
|
||||
post /followups/:id/message (GenerateFollowUpMessageReq) returns (GenerateFollowUpMessageData)
|
||||
|
||||
@handler GetCrmStats
|
||||
get /stats (CrmStatsReq) returns (CrmStatsData)
|
||||
}
|
||||
|
|
@ -25,5 +25,3 @@ import "m5.api"
|
|||
import "invite.api"
|
||||
import "growth.api"
|
||||
import "growth_p2.api"
|
||||
import "radar.api"
|
||||
import "crm.api"
|
||||
|
|
|
|||
|
|
@ -188,8 +188,6 @@ type (
|
|||
ProductContext string `json:"product_context"`
|
||||
MatchTags []string `json:"match_tags"`
|
||||
PainPoints []string `json:"pain_points"`
|
||||
ProviderCapabilityTerms []string `json:"provider_capability_terms"`
|
||||
ProviderExcludeTerms []string `json:"provider_exclude_terms"`
|
||||
PlacementUrl string `json:"placement_url,optional"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
|
|
@ -204,8 +202,6 @@ type (
|
|||
ProductContext string `json:"product_context,optional"`
|
||||
MatchTags []string `json:"match_tags,optional"`
|
||||
PainPoints []string `json:"pain_points,optional"`
|
||||
ProviderCapabilityTerms []string `json:"provider_capability_terms,optional"`
|
||||
ProviderExcludeTerms []string `json:"provider_exclude_terms,optional"`
|
||||
PlacementUrl string `json:"placement_url,optional"`
|
||||
}
|
||||
ProductIdPath {
|
||||
|
|
@ -247,61 +243,15 @@ type (
|
|||
ThemeKey string `json:"theme_key,optional"`
|
||||
ThemeLabel string `json:"theme_label,optional"`
|
||||
ProductContext string `json:"product_context,optional"`
|
||||
// TargetCount:話題今日目標(則),未指定時預設 20。主路徑不足時會加碼再搜/次路徑補抓,上限 40。
|
||||
TargetCount int `json:"target_count,optional"`
|
||||
}
|
||||
ScoutScanReq {
|
||||
Brief ScoutBriefPublic `json:"brief"`
|
||||
}
|
||||
ScoutScanJobData {
|
||||
Job JobPublic `json:"job"`
|
||||
Run ScoutRunPublic `json:"run"`
|
||||
}
|
||||
ScoutRunPublic {
|
||||
Id string `json:"id"`
|
||||
JobId string `json:"job_id"`
|
||||
ThemeKey string `json:"theme_key"`
|
||||
ThemeLabel string `json:"theme_label"`
|
||||
Intent string `json:"intent"`
|
||||
Mode string `json:"mode"`
|
||||
BrandId string `json:"brand_id,optional"`
|
||||
TargetCount int `json:"target_count"`
|
||||
Status string `json:"status"`
|
||||
SearchedCount int `json:"searched_count"`
|
||||
DuplicateCount int `json:"duplicate_count"`
|
||||
IrrelevantCount int `json:"irrelevant_count"`
|
||||
EligibleCount int `json:"eligible_count"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
ShortfallCount int `json:"shortfall_count"`
|
||||
ShortfallReasons []string `json:"shortfall_reasons"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
StartedAt int64 `json:"started_at,optional"`
|
||||
CompletedAt int64 `json:"completed_at,optional"`
|
||||
Error string `json:"error,optional"`
|
||||
}
|
||||
ScoutRunListReq {
|
||||
Page int `form:"page,optional"`
|
||||
PageSize int `form:"pageSize,optional"`
|
||||
BrandId string `form:"brand_id,optional"`
|
||||
Mode string `form:"mode,optional"`
|
||||
}
|
||||
ScoutRunListData {
|
||||
List []ScoutRunPublic `json:"list"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
ScoutRunPostsReq {
|
||||
RunId string `path:"runId"`
|
||||
Page int `form:"page,optional"`
|
||||
PageSize int `form:"pageSize,optional"`
|
||||
}
|
||||
ScoutRunPostsData {
|
||||
Run ScoutRunPublic `json:"run"`
|
||||
List []ScoutPostPublic `json:"list"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
ScoutPostPublic {
|
||||
Id string `json:"id"`
|
||||
RunId string `json:"run_id,optional"`
|
||||
BrandId string `json:"brand_id,optional"`
|
||||
Author string `json:"author"`
|
||||
Text string `json:"text"`
|
||||
|
|
@ -320,8 +270,6 @@ type (
|
|||
ScanPath string `json:"scan_path,optional"`
|
||||
Classification string `json:"classification,optional"`
|
||||
Permalink string `json:"permalink,optional"`
|
||||
// 原文發文時間(unix nanoseconds);未知時可為 0
|
||||
PostedAt int64 `json:"posted_at,optional"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
ScoutPostListData {
|
||||
|
|
@ -364,14 +312,6 @@ type (
|
|||
ScoutCrawlerSessionReq {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// 升級為商機:複製成 Opportunity,不改 outreach_status
|
||||
PromoteScoutPostData {
|
||||
OpportunityId string `json:"opportunity_id"`
|
||||
Status string `json:"status"`
|
||||
IntentBand string `json:"intent_band,optional"`
|
||||
IntentScore int `json:"intent_score,optional"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
|
|
@ -495,15 +435,6 @@ service gateway {
|
|||
@handler RunScan
|
||||
post /scan (ScoutScanReq) returns (ScoutScanJobData)
|
||||
|
||||
@handler ListScoutRuns
|
||||
get /runs (ScoutRunListReq) returns (ScoutRunListData)
|
||||
|
||||
@handler ListScoutRunPosts
|
||||
get /runs/:runId/posts (ScoutRunPostsReq) returns (ScoutRunPostsData)
|
||||
|
||||
@handler RemoveScoutRun
|
||||
delete /runs/:runId (ScoutRunPostsReq) returns (OkData)
|
||||
|
||||
@handler ListScoutPosts
|
||||
get /posts (ScoutPostListReq) returns (ScoutPostListData)
|
||||
|
||||
|
|
@ -519,9 +450,6 @@ service gateway {
|
|||
@handler SendOutreach
|
||||
post /posts/:id/send (ScoutSendPathReq) returns (ScoutPostPublic)
|
||||
|
||||
@handler PromoteScoutPost
|
||||
post /posts/:id/promote (ScoutPostIdPath) returns (PromoteScoutPostData)
|
||||
|
||||
@handler RemoveScoutPost
|
||||
delete /posts/:id (ScoutPostIdPath) returns (OkData)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,634 +0,0 @@
|
|||
syntax = "v1"
|
||||
|
||||
// demand-radar: service profile / radar watches / sweeps / opportunities / replies
|
||||
// spec: docs/product/demand-radar/spec.md §5.2
|
||||
// 未實作能力一律回 501(radarDomain.ErrNotReady),禁止 102000 空成功。
|
||||
|
||||
type (
|
||||
// ---------- ServiceProfile ----------
|
||||
ServiceItem {
|
||||
Name string `json:"name"`
|
||||
PriceMin float64 `json:"price_min,optional"`
|
||||
PriceMax float64 `json:"price_max,optional"`
|
||||
Currency string `json:"currency,optional"`
|
||||
}
|
||||
|
||||
ServiceCasePublic {
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary,optional"`
|
||||
Link string `json:"link,optional"`
|
||||
}
|
||||
|
||||
FaqItem {
|
||||
Question string `json:"question"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
ServiceProfilePublic {
|
||||
Exists bool `json:"exists"`
|
||||
Services []ServiceItem `json:"services"`
|
||||
Cases []ServiceCasePublic `json:"cases"`
|
||||
Forbidden []string `json:"forbidden"`
|
||||
Faq []FaqItem `json:"faq"`
|
||||
ServiceAreas []string `json:"service_areas"`
|
||||
RemoteOk bool `json:"remote_ok"`
|
||||
Availability string `json:"availability,optional"`
|
||||
ToneNote string `json:"tone_note,optional"`
|
||||
UpdatedAt int64 `json:"updated_at,optional"`
|
||||
}
|
||||
|
||||
UpsertServiceProfileReq {
|
||||
Services []ServiceItem `json:"services"`
|
||||
Cases []ServiceCasePublic `json:"cases,optional"`
|
||||
Forbidden []string `json:"forbidden,optional"`
|
||||
Faq []FaqItem `json:"faq,optional"`
|
||||
ServiceAreas []string `json:"service_areas,optional"`
|
||||
RemoteOk bool `json:"remote_ok,optional"`
|
||||
Availability string `json:"availability,optional"`
|
||||
ToneNote string `json:"tone_note,optional"`
|
||||
}
|
||||
|
||||
// ---------- RadarWatch ----------
|
||||
RadarWatchPublic {
|
||||
Id string `json:"id"`
|
||||
Terms []string `json:"terms"`
|
||||
ExcludeTerms []string `json:"exclude_terms"`
|
||||
Regions []string `json:"regions"`
|
||||
Status string `json:"status"` // active | paused | archived
|
||||
ContextMode string `json:"context_mode"` // generic | product
|
||||
BrandId string `json:"brand_id,optional"`
|
||||
ProductId string `json:"product_id,optional"`
|
||||
BrandNameSnapshot string `json:"brand_name_snapshot,optional"`
|
||||
ProductLabelSnapshot string `json:"product_label_snapshot,optional"`
|
||||
ContextBoundAt int64 `json:"context_bound_at,optional"`
|
||||
PauseReason string `json:"pause_reason,optional"`
|
||||
LastSweptAt int64 `json:"last_swept_at,optional"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
// 只在建立當下這是使用者第一組訂閱、且已排入首巡時為 true;不是持久狀態,僅供前端顯示一次性提示。
|
||||
FirstSweepTriggered bool `json:"first_sweep_triggered,optional"`
|
||||
}
|
||||
|
||||
ListWatchesReq {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=20"`
|
||||
Status string `form:"status,optional"`
|
||||
ContextMode string `form:"context_mode,optional"`
|
||||
BrandId string `form:"brand_id,optional"`
|
||||
ProductId string `form:"product_id,optional"`
|
||||
}
|
||||
|
||||
WatchListData {
|
||||
List []RadarWatchPublic `json:"list"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
ActiveCount int `json:"active_count"`
|
||||
MaxActive int `json:"max_active"`
|
||||
ProfileExists bool `json:"profile_exists"`
|
||||
}
|
||||
|
||||
CreateWatchReq {
|
||||
Terms []string `json:"terms"`
|
||||
ExcludeTerms []string `json:"exclude_terms,optional"`
|
||||
Regions []string `json:"regions,optional"`
|
||||
Enabled bool `json:"enabled,optional"`
|
||||
BrandId string `json:"brand_id,optional"`
|
||||
ProductId string `json:"product_id,optional"`
|
||||
}
|
||||
|
||||
UpdateWatchReq {
|
||||
Id string `path:"id"`
|
||||
Terms []string `json:"terms,optional"`
|
||||
ExcludeTerms []string `json:"exclude_terms,optional"`
|
||||
Regions []string `json:"regions,optional"`
|
||||
}
|
||||
|
||||
WatchIdReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
AssignProductReq {
|
||||
Id string `path:"id"`
|
||||
BrandId string `json:"brand_id"`
|
||||
ProductId string `json:"product_id"`
|
||||
}
|
||||
|
||||
SuggestWatchTermsReq {
|
||||
Limit int `json:"limit,optional"`
|
||||
BrandId string `json:"brand_id,optional"`
|
||||
ProductId string `json:"product_id,optional"`
|
||||
}
|
||||
|
||||
WatchTermSuggestion {
|
||||
Term string `json:"term"`
|
||||
Reason string `json:"reason"`
|
||||
Usage string `json:"usage"` // include | exclude
|
||||
BasisKind string `json:"basis_kind,optional"`
|
||||
BasisText string `json:"basis_text,optional"`
|
||||
}
|
||||
|
||||
WatchSuggestData {
|
||||
List []WatchTermSuggestion `json:"list"`
|
||||
}
|
||||
|
||||
TriggerSweepData {
|
||||
JobId string `json:"job_id"`
|
||||
SweepId string `json:"sweep_id,optional"`
|
||||
}
|
||||
|
||||
// ---------- Opportunity ----------
|
||||
OpportunityReason {
|
||||
Dimension string `json:"dimension"` // authenticity | intent | region | freshness | fit
|
||||
Score int `json:"score"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
OpportunityOverride {
|
||||
FromBand string `json:"from_band,optional"`
|
||||
ToBand string `json:"to_band,optional"`
|
||||
FromStatus string `json:"from_status,optional"`
|
||||
ToStatus string `json:"to_status,optional"`
|
||||
ActorUid int64 `json:"actor_uid"`
|
||||
At int64 `json:"at"`
|
||||
}
|
||||
|
||||
ProductFitReasonPublic {
|
||||
Dimension string `json:"dimension"` // pain | scenario | audience | capability
|
||||
Score int `json:"score"`
|
||||
Reason string `json:"reason"`
|
||||
CandidateExcerpt string `json:"candidate_excerpt,optional"`
|
||||
ProductBasis string `json:"product_basis,optional"`
|
||||
}
|
||||
|
||||
ProductMatchPublic {
|
||||
BrandId string `json:"brand_id"`
|
||||
ProductId string `json:"product_id"`
|
||||
BrandNameSnapshot string `json:"brand_name_snapshot"`
|
||||
ProductLabelSnapshot string `json:"product_label_snapshot"`
|
||||
BrandUpdatedAt int64 `json:"brand_updated_at"`
|
||||
ProductUpdatedAt int64 `json:"product_updated_at"`
|
||||
ProductFitScore int `json:"product_fit_score"`
|
||||
ProductFitBand string `json:"product_fit_band"` // strong | possible | weak
|
||||
Eligible bool `json:"eligible"`
|
||||
Excluded bool `json:"excluded"`
|
||||
ExcludeReason string `json:"exclude_reason,optional"`
|
||||
Reasons []ProductFitReasonPublic `json:"reasons"`
|
||||
Risks []string `json:"risks"`
|
||||
WatchIds []string `json:"watch_ids"`
|
||||
MatchedTerms []string `json:"matched_terms"`
|
||||
MatchedAt int64 `json:"matched_at"`
|
||||
}
|
||||
|
||||
OpportunityPublic {
|
||||
Id string `json:"id"`
|
||||
WatchId string `json:"watch_id,optional"`
|
||||
Source string `json:"source"` // threads | manual | scout_promote
|
||||
SourceScoutPostId string `json:"source_scout_post_id,optional"`
|
||||
ExternalId string `json:"external_id"`
|
||||
Permalink string `json:"permalink"`
|
||||
AuthorHandle string `json:"author_handle"`
|
||||
Text string `json:"text"`
|
||||
PostedAt int64 `json:"posted_at"`
|
||||
Status string `json:"status"` // judging | qualified | rejected | accepted | dismissed
|
||||
IntentScore int `json:"intent_score"`
|
||||
IntentBand string `json:"intent_band"` // high | mid | low
|
||||
Reasons []OpportunityReason `json:"reasons"`
|
||||
RegionDetected string `json:"region_detected,optional"`
|
||||
RegionMatch string `json:"region_match"` // match | mismatch | unknown
|
||||
FreshnessHours int `json:"freshness_hours"`
|
||||
MatchedService string `json:"matched_service,optional"`
|
||||
MatchedTerms []string `json:"matched_terms"`
|
||||
RejectReason string `json:"reject_reason,optional"`
|
||||
Override *OpportunityOverride `json:"override,optional"`
|
||||
ContactId string `json:"contact_id,optional"`
|
||||
DefaultReply *ReplyVariantPublic `json:"default_reply,optional"`
|
||||
PrimaryBrandId string `json:"primary_brand_id,optional"`
|
||||
PrimaryProductId string `json:"primary_product_id,optional"`
|
||||
PrimaryBrandName string `json:"primary_brand_name,optional"`
|
||||
PrimaryProductLabel string `json:"primary_product_label,optional"`
|
||||
PrimaryProductFitScore int `json:"primary_product_fit_score,optional"`
|
||||
PrimaryProductFitBand string `json:"primary_product_fit_band,optional"`
|
||||
PrimaryProductOverridden bool `json:"primary_product_overridden,optional"`
|
||||
ProductMatches []ProductMatchPublic `json:"product_matches"`
|
||||
ReviewState string `json:"review_state"` // pending | completed | removed
|
||||
PreviousReviewState string `json:"previous_review_state,optional"`
|
||||
RemovalReason string `json:"removal_reason,optional"`
|
||||
RemovalNote string `json:"removal_note,optional"`
|
||||
RemovedAt int64 `json:"removed_at,optional"`
|
||||
RemovedBy int64 `json:"removed_by,optional"`
|
||||
LastMatchedAt int64 `json:"last_matched_at,optional"`
|
||||
PriorityScore int `json:"priority_score"`
|
||||
PriorityBand string `json:"priority_band"` // high | review | low
|
||||
PainFitScore int `json:"pain_fit_score,optional"`
|
||||
DemandIntentScore int `json:"demand_intent_score,optional"`
|
||||
EvidenceQualityScore int `json:"evidence_quality_score,optional"`
|
||||
FreshnessScore int `json:"freshness_score,optional"`
|
||||
DemandEvidence []string `json:"demand_evidence,optional"`
|
||||
DemandInputVersion string `json:"demand_input_version,optional"`
|
||||
DemandMapVersion int64 `json:"demand_map_version,optional"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
RadarTodayStats {
|
||||
Total int `json:"total"`
|
||||
High int `json:"high"`
|
||||
Mid int `json:"mid"`
|
||||
Low int `json:"low"`
|
||||
}
|
||||
|
||||
RadarTodayData {
|
||||
Stats RadarTodayStats `json:"stats"`
|
||||
High []OpportunityPublic `json:"high"`
|
||||
Mid []OpportunityPublic `json:"mid"`
|
||||
Low []OpportunityPublic `json:"low"`
|
||||
TruncatedCount int `json:"truncated_count"`
|
||||
LastSweptAt int64 `json:"last_swept_at,optional"`
|
||||
// EmptyReason: not_swept_yet | all_watches_paused | no_watch | no_profile | sweep_failed | no_hit | no_eligible_product_match
|
||||
EmptyReason string `json:"empty_reason,optional"`
|
||||
EmptyHint string `json:"empty_hint,optional"`
|
||||
}
|
||||
|
||||
RadarTodayReq {
|
||||
BrandId string `form:"brand_id,optional"`
|
||||
ProductId string `form:"product_id,optional"`
|
||||
FitBand string `form:"fit_band,optional"`
|
||||
}
|
||||
|
||||
ListOpportunitiesReq {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=20"`
|
||||
Band string `form:"band,optional"`
|
||||
Status string `form:"status,optional"`
|
||||
WatchId string `form:"watch_id,optional"`
|
||||
BrandId string `form:"brand_id,optional"`
|
||||
ProductId string `form:"product_id,optional"`
|
||||
FitBand string `form:"fit_band,optional"`
|
||||
MatchState string `form:"match_state,optional"`
|
||||
ReviewState string `form:"review_state,optional"`
|
||||
TimeScope string `form:"time_scope,optional"` // today | 7d | all
|
||||
PriorityBand string `form:"priority_band,optional"` // high | review | low
|
||||
Sort string `form:"sort,optional"`
|
||||
From int64 `form:"from,optional"`
|
||||
To int64 `form:"to,optional"`
|
||||
}
|
||||
|
||||
OpportunityListData {
|
||||
List []OpportunityPublic `json:"list"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
OpportunityIdReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
AcceptOpportunityReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
AcceptOpportunityData {
|
||||
OpportunityId string `json:"opportunity_id"`
|
||||
ContactId string `json:"contact_id,optional"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
DismissOpportunityReq {
|
||||
Id string `path:"id"`
|
||||
Reason string `json:"reason,optional"`
|
||||
}
|
||||
|
||||
ReviewStateReq {
|
||||
Id string `path:"id"`
|
||||
State string `json:"state"` // pending | completed | removed
|
||||
RemovalReason string `json:"removal_reason,optional"`
|
||||
RemovalNote string `json:"removal_note,optional"`
|
||||
DuplicateOpportunityId string `json:"duplicate_of_opportunity_id,optional"`
|
||||
}
|
||||
|
||||
DemandMapProductReq {
|
||||
ProductId string `path:"productId"`
|
||||
}
|
||||
|
||||
DemandMapPhrase {
|
||||
Text string `json:"text"`
|
||||
Kind string `json:"kind"`
|
||||
BasisKind string `json:"basis_kind,optional"`
|
||||
BasisText string `json:"basis_text,optional"`
|
||||
Origin string `json:"origin"` // product | user | ai
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
DemandMapPublic {
|
||||
ProductId string `json:"product_id"`
|
||||
DemandInputVersion string `json:"demand_input_version"`
|
||||
MapVersion int64 `json:"map_version"`
|
||||
State string `json:"state"` // ready | incomplete | stale
|
||||
PainPhrases []DemandMapPhrase `json:"pain_phrases"`
|
||||
ScenarioPhrases []DemandMapPhrase `json:"scenario_phrases"`
|
||||
DesiredOutcomes []DemandMapPhrase `json:"desired_outcomes"`
|
||||
SolutionSignals []DemandMapPhrase `json:"solution_signals"`
|
||||
ExclusionSignals []DemandMapPhrase `json:"exclusion_signals"`
|
||||
SourceBasis []string `json:"source_basis"`
|
||||
CustomPhrases []DemandMapPhrase `json:"custom_phrases"`
|
||||
AiEnrichedAt int64 `json:"ai_enriched_at,optional"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
UpdateDemandMapReq {
|
||||
ProductId string `path:"productId"`
|
||||
ExpectedMapVersion int64 `json:"expected_map_version"`
|
||||
PainPhrases []DemandMapPhrase `json:"pain_phrases,optional"`
|
||||
ScenarioPhrases []DemandMapPhrase `json:"scenario_phrases,optional"`
|
||||
DesiredOutcomes []DemandMapPhrase `json:"desired_outcomes,optional"`
|
||||
SolutionSignals []DemandMapPhrase `json:"solution_signals,optional"`
|
||||
ExclusionSignals []DemandMapPhrase `json:"exclusion_signals,optional"`
|
||||
CustomPhrases []DemandMapPhrase `json:"custom_phrases,optional"`
|
||||
}
|
||||
|
||||
CostPreviewReq {
|
||||
Action string `form:"action"` // sweep | explore | demand_map_enrich | reply
|
||||
WatchId string `form:"watch_id,optional"`
|
||||
ProductId string `form:"product_id,optional"`
|
||||
CandidateLimit int `form:"candidate_limit,optional"`
|
||||
}
|
||||
|
||||
CostPreviewPublic {
|
||||
PreviewId string `json:"preview_id"`
|
||||
Action string `json:"action"`
|
||||
KeyMode string `json:"key_mode"` // platform | byok
|
||||
FixedCredits int `json:"fixed_credits"`
|
||||
MinCredits int `json:"min_credits"`
|
||||
MaxCredits int `json:"max_credits"`
|
||||
SearchCalls int `json:"search_calls"`
|
||||
MaxAiCandidates int `json:"max_ai_candidates"`
|
||||
RemainingCredits int `json:"remaining_credits"`
|
||||
EstimateBasis string `json:"estimate_basis"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
EnrichDemandMapReq {
|
||||
ProductId string `path:"productId"`
|
||||
PreviewId string `json:"preview_id"`
|
||||
CreditCeiling int `json:"credit_ceiling"`
|
||||
ExpectedMapVersion int64 `json:"expected_map_version"`
|
||||
}
|
||||
|
||||
OverrideOpportunityReq {
|
||||
Id string `path:"id"`
|
||||
Band string `json:"band,optional"`
|
||||
Status string `json:"status,optional"`
|
||||
Note string `json:"note,optional"`
|
||||
}
|
||||
|
||||
SetPrimaryProductReq {
|
||||
Id string `path:"id"`
|
||||
ProductId string `json:"product_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// ---------- ReplyVariant ----------
|
||||
ReplyVariantPublic {
|
||||
Id string `json:"id"`
|
||||
OpportunityId string `json:"opportunity_id"`
|
||||
Variant string `json:"variant"` // public_comment | dm | no_sales | professional | humorous
|
||||
Text string `json:"text"`
|
||||
UsedAt int64 `json:"used_at,optional"`
|
||||
SentChannel string `json:"sent_channel,optional"` // outbox | manual_copy
|
||||
// 只在 sent_channel=outbox 且真的排入既有 Outbox 佇列時才有值。
|
||||
OutboxId string `json:"outbox_id,optional"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
ListRepliesReq {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
ReplyListData {
|
||||
List []ReplyVariantPublic `json:"list"`
|
||||
}
|
||||
|
||||
CreateReplyReq {
|
||||
Id string `path:"id"`
|
||||
Variant string `json:"variant"`
|
||||
}
|
||||
|
||||
// MarkReplyUsedReq: channel=outbox|manual_copy(dm 僅 manual_copy)
|
||||
MarkReplyUsedReq {
|
||||
Id string `path:"id"`
|
||||
ReplyId string `path:"replyId"`
|
||||
Channel string `json:"channel"` // outbox | manual_copy
|
||||
// channel=outbox 時必填:要用哪個 Threads 帳號送出。
|
||||
AccountId string `json:"account_id,optional"`
|
||||
}
|
||||
|
||||
MarkReplyUsedData {
|
||||
Reply ReplyVariantPublic `json:"reply"`
|
||||
HealthAdvice string `json:"health_advice,optional"`
|
||||
}
|
||||
|
||||
// ---------- RadarSweep ----------
|
||||
RadarSweepPublic {
|
||||
Id string `json:"id"`
|
||||
WatchId string `json:"watch_id"`
|
||||
JobId string `json:"job_id,optional"`
|
||||
Path string `json:"path"` // api | crawler
|
||||
HitCount int `json:"hit_count"`
|
||||
JudgedCount int `json:"judged_count"`
|
||||
CreatedCount int `json:"created_count"`
|
||||
TruncatedCount int `json:"truncated_count"`
|
||||
MatchEvaluatedCount int `json:"match_evaluated_count"`
|
||||
MatchMergedCount int `json:"match_merged_count"`
|
||||
FitRejectedCount int `json:"fit_rejected_count"`
|
||||
FailedReason string `json:"failed_reason,optional"`
|
||||
CreditsUsed int `json:"credits_used"`
|
||||
StartedAt int64 `json:"started_at"`
|
||||
EndedAt int64 `json:"ended_at,optional"`
|
||||
DemandInputVersion string `json:"demand_input_version,optional"`
|
||||
DemandMapVersion int64 `json:"demand_map_version,optional"`
|
||||
DedupedCount int `json:"deduped_count"`
|
||||
PrefilterPassCount int `json:"prefilter_pass_count"`
|
||||
PrefilterReviewCount int `json:"prefilter_review_count"`
|
||||
PrefilterRejectedCount int `json:"prefilter_rejected_count"`
|
||||
CachedJudgmentCount int `json:"cached_judgment_count"`
|
||||
TombstoneMatchedCount int `json:"tombstone_matched_count"`
|
||||
BudgetDeferredCount int `json:"budget_deferred_count"`
|
||||
SweepStatus string `json:"status"` // complete | partial_budget | blocked_budget | failed
|
||||
CreditSearch int `json:"credit_search"`
|
||||
CreditDemandMap int `json:"credit_demand_map"`
|
||||
CreditJudge int `json:"credit_judge"`
|
||||
CreditReply int `json:"credit_reply"`
|
||||
}
|
||||
|
||||
ListSweepsReq {
|
||||
Page int `form:"page,default=1"`
|
||||
PageSize int `form:"pageSize,default=20"`
|
||||
WatchId string `form:"watch_id,optional"`
|
||||
From int64 `form:"from,optional"`
|
||||
To int64 `form:"to,optional"`
|
||||
}
|
||||
|
||||
SweepListData {
|
||||
List []RadarSweepPublic `json:"list"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
// ---------- Manual Import(P1:不承諾全平台自動抓取的合規補位) ----------
|
||||
ImportOpportunityItem {
|
||||
Url string `json:"url"`
|
||||
// 貼文內文;沒有官方 API/爬蟲可讀任意網址,判定一律吃使用者貼上的文字。
|
||||
Text string `json:"text"`
|
||||
// 缺省時從 threads 網址猜 @handle,猜不到就留空。
|
||||
Author string `json:"author,optional"`
|
||||
// unix nanoseconds UTC;缺省用匯入當下時間(新鮮度以匯入時間計)。
|
||||
PostedAt int64 `json:"posted_at,optional"`
|
||||
}
|
||||
|
||||
ImportOpportunitiesReq {
|
||||
Items []ImportOpportunityItem `json:"items"`
|
||||
BrandId string `json:"brand_id,optional"`
|
||||
ProductId string `json:"product_id,optional"`
|
||||
}
|
||||
|
||||
ImportedOpportunityResult {
|
||||
Url string `json:"url"`
|
||||
OpportunityId string `json:"opportunity_id,optional"`
|
||||
Status string `json:"status"` // qualified | rejected | skipped | failed
|
||||
IntentBand string `json:"intent_band,optional"`
|
||||
IntentScore int `json:"intent_score,optional"`
|
||||
// skipped/failed 時的人話原因;qualified/rejected 時通常留空。
|
||||
Error string `json:"error,optional"`
|
||||
}
|
||||
|
||||
ImportOpportunitiesData {
|
||||
Results []ImportedOpportunityResult `json:"results"`
|
||||
}
|
||||
|
||||
// ---------- Explore(商機頁立即探索:短詞 fan-out → 五問判定) ----------
|
||||
ExploreOpportunitiesReq {
|
||||
// 1–6 組 Threads 短詞(契約 A);不合規整組擋下,不默默修正。
|
||||
Terms []string `json:"terms"`
|
||||
BrandId string `json:"brand_id,optional"`
|
||||
ProductId string `json:"product_id,optional"`
|
||||
PreviewId string `json:"preview_id,optional"`
|
||||
CreditCeiling int `json:"credit_ceiling,optional"`
|
||||
DemandInputVersion string `json:"demand_input_version,optional"`
|
||||
DemandMapVersion int64 `json:"demand_map_version,optional"`
|
||||
}
|
||||
|
||||
ExploreOpportunitiesData {
|
||||
SweepId string `json:"sweep_id"`
|
||||
HitCount int `json:"hit_count"`
|
||||
JudgedCount int `json:"judged_count"`
|
||||
CreatedCount int `json:"created_count"`
|
||||
MatchedCount int `json:"matched_count"`
|
||||
MergedCount int `json:"merged_count"`
|
||||
TruncatedCount int `json:"truncated_count"`
|
||||
CreditsUsed int `json:"credits_used"`
|
||||
DedupedCount int `json:"deduped_count"`
|
||||
PrefilterPassCount int `json:"prefilter_pass_count"`
|
||||
PrefilterReviewCount int `json:"prefilter_review_count"`
|
||||
PrefilterRejectedCount int `json:"prefilter_rejected_count"`
|
||||
CachedJudgmentCount int `json:"cached_judgment_count"`
|
||||
TombstoneMatchedCount int `json:"tombstone_matched_count"`
|
||||
BudgetDeferredCount int `json:"budget_deferred_count"`
|
||||
SweepStatus string `json:"status"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
group: radar
|
||||
prefix: /api/v1/radar
|
||||
middleware: AuthJWT
|
||||
)
|
||||
service gateway {
|
||||
@handler GetServiceProfile
|
||||
get /service-profile returns (ServiceProfilePublic)
|
||||
|
||||
@handler UpsertServiceProfile
|
||||
put /service-profile (UpsertServiceProfileReq) returns (ServiceProfilePublic)
|
||||
|
||||
@handler ListWatches
|
||||
get /watches (ListWatchesReq) returns (WatchListData)
|
||||
|
||||
@handler CreateWatch
|
||||
post /watches (CreateWatchReq) returns (RadarWatchPublic)
|
||||
|
||||
@handler SuggestWatchTerms
|
||||
post /watches/suggest (SuggestWatchTermsReq) returns (WatchSuggestData)
|
||||
|
||||
@handler AssignWatchProduct
|
||||
post /watches/:id/assign-product (AssignProductReq) returns (RadarWatchPublic)
|
||||
|
||||
@handler GetWatch
|
||||
get /watches/:id (WatchIdReq) returns (RadarWatchPublic)
|
||||
|
||||
@handler UpdateWatch
|
||||
put /watches/:id (UpdateWatchReq) returns (RadarWatchPublic)
|
||||
|
||||
@handler ArchiveWatch
|
||||
delete /watches/:id (WatchIdReq) returns (OkData)
|
||||
|
||||
@handler DeleteArchivedWatch
|
||||
delete /watches/:id/purge (WatchIdReq) returns (OkData)
|
||||
|
||||
@handler PauseWatch
|
||||
post /watches/:id/pause (WatchIdReq) returns (RadarWatchPublic)
|
||||
|
||||
@handler ResumeWatch
|
||||
post /watches/:id/resume (WatchIdReq) returns (RadarWatchPublic)
|
||||
|
||||
@handler TriggerWatchSweep
|
||||
post /watches/:id/sweep (WatchIdReq) returns (TriggerSweepData)
|
||||
|
||||
@handler GetRadarToday
|
||||
get /today (RadarTodayReq) returns (RadarTodayData)
|
||||
|
||||
@handler ListOpportunities
|
||||
get /opportunities (ListOpportunitiesReq) returns (OpportunityListData)
|
||||
|
||||
@handler GetOpportunity
|
||||
get /opportunities/:id (OpportunityIdReq) returns (OpportunityPublic)
|
||||
|
||||
@handler AcceptOpportunity
|
||||
post /opportunities/:id/accept (AcceptOpportunityReq) returns (AcceptOpportunityData)
|
||||
|
||||
@handler DismissOpportunity
|
||||
post /opportunities/:id/dismiss (DismissOpportunityReq) returns (OpportunityPublic)
|
||||
|
||||
@handler UpdateOpportunityReviewState
|
||||
put /opportunities/:id/review-state (ReviewStateReq) returns (OpportunityPublic)
|
||||
|
||||
@handler OverrideOpportunity
|
||||
post /opportunities/:id/override (OverrideOpportunityReq) returns (OpportunityPublic)
|
||||
|
||||
@handler SetPrimaryProduct
|
||||
put /opportunities/:id/primary-product (SetPrimaryProductReq) returns (OpportunityPublic)
|
||||
|
||||
@handler GetDemandMap
|
||||
get /products/:productId/demand-map (DemandMapProductReq) returns (DemandMapPublic)
|
||||
|
||||
@handler UpdateDemandMap
|
||||
put /products/:productId/demand-map (UpdateDemandMapReq) returns (DemandMapPublic)
|
||||
|
||||
@handler EnrichDemandMap
|
||||
post /products/:productId/demand-map/enrich (EnrichDemandMapReq) returns (DemandMapPublic)
|
||||
|
||||
@handler GetRadarCostPreview
|
||||
get /cost-preview (CostPreviewReq) returns (CostPreviewPublic)
|
||||
|
||||
@handler ListOpportunityReplies
|
||||
get /opportunities/:id/replies (ListRepliesReq) returns (ReplyListData)
|
||||
|
||||
@handler CreateOpportunityReply
|
||||
post /opportunities/:id/replies (CreateReplyReq) returns (ReplyVariantPublic)
|
||||
|
||||
@handler MarkOpportunityReplyUsed
|
||||
post /opportunities/:id/replies/:replyId/mark-used (MarkReplyUsedReq) returns (MarkReplyUsedData)
|
||||
|
||||
@handler ListSweeps
|
||||
get /sweeps (ListSweepsReq) returns (SweepListData)
|
||||
|
||||
@handler ImportOpportunities
|
||||
post /import (ImportOpportunitiesReq) returns (ImportOpportunitiesData)
|
||||
|
||||
@handler ExploreOpportunities
|
||||
post /explore (ExploreOpportunitiesReq) returns (ExploreOpportunitiesData)
|
||||
}
|
||||
|
|
@ -285,8 +285,6 @@ type (
|
|||
ScheduleStartAt int64 `json:"schedule_start_at,optional"`
|
||||
// Threads 話題標籤(topic_tag,1~50 字,可不加 #)
|
||||
TopicTag string `json:"topic_tag,optional"`
|
||||
// everyone | accounts_you_follow | mentioned_only | parent_post_author_only | followers_only
|
||||
ReplyControl string `json:"reply_control,optional"`
|
||||
}
|
||||
|
||||
// --- Own posts ---
|
||||
|
|
@ -301,8 +299,6 @@ type (
|
|||
RepliedAt int64 `json:"replied_at,optional"`
|
||||
ParentReplyId string `json:"parent_reply_id,optional"`
|
||||
IsMine bool `json:"is_mine,optional"`
|
||||
// Threads hide_status:NOT_HUSHED / HIDDEN / …
|
||||
HideStatus string `json:"hide_status,optional"`
|
||||
}
|
||||
|
||||
OwnPostPublic {
|
||||
|
|
@ -328,8 +324,6 @@ type (
|
|||
FormulaDetail string `json:"formula_detail,optional"`
|
||||
Replies []OwnPostReplyPublic `json:"replies"`
|
||||
PublishedAt int64 `json:"published_at"`
|
||||
// everyone | accounts_you_follow | mentioned_only | parent_post_author_only | followers_only
|
||||
ReplyControl string `json:"reply_control,optional"`
|
||||
}
|
||||
|
||||
OwnPostListData {
|
||||
|
|
@ -375,17 +369,6 @@ type (
|
|||
PostId string `json:"post_id"`
|
||||
}
|
||||
|
||||
OwnPostManageReplyReq {
|
||||
PostId string `json:"post_id"`
|
||||
ReplyId string `json:"reply_id"`
|
||||
Hide bool `json:"hide"`
|
||||
}
|
||||
|
||||
OwnPostSetReplyControlReq {
|
||||
PostId string `json:"post_id"`
|
||||
ReplyControl string `json:"reply_control"`
|
||||
}
|
||||
|
||||
// --- Mentions ---
|
||||
MentionPublic {
|
||||
Id string `json:"id"`
|
||||
|
|
@ -565,12 +548,6 @@ service gateway {
|
|||
|
||||
@handler OwnPostLoadReplies
|
||||
post /load-replies (OwnPostLoadRepliesReq) returns (OwnPostPublic)
|
||||
|
||||
@handler OwnPostManageReply
|
||||
post /manage-reply (OwnPostManageReplyReq) returns (OwnPostPublic)
|
||||
|
||||
@handler OwnPostSetReplyControl
|
||||
post /reply-control (OwnPostSetReplyControlReq) returns (OwnPostPublic)
|
||||
}
|
||||
|
||||
@server (
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
[
|
||||
{ "dropIndexes": "radar_watches", "index": "owner_watches_status" },
|
||||
{ "dropIndexes": "radar_watches", "index": "owner_watches_created" },
|
||||
{ "dropIndexes": "radar_sweeps", "index": "owner_sweeps_created" },
|
||||
{ "dropIndexes": "radar_sweeps", "index": "watch_sweeps_created" },
|
||||
{ "dropIndexes": "radar_sweeps", "index": "sweep_job" },
|
||||
{ "dropIndexes": "radar_opportunities", "index": "owner_opportunities_created" },
|
||||
{ "dropIndexes": "radar_opportunities", "index": "owner_opportunity_band_status" },
|
||||
{ "dropIndexes": "radar_opportunities", "index": "owner_opportunity_external" },
|
||||
{ "dropIndexes": "radar_replies", "index": "owner_reply_variant" },
|
||||
{ "dropIndexes": "crm_contacts", "index": "owner_contact_identity" },
|
||||
{ "dropIndexes": "crm_contacts", "index": "owner_contacts_stage_touch" },
|
||||
{ "dropIndexes": "crm_contacts", "index": "owner_contacts_follow_up" },
|
||||
{ "dropIndexes": "crm_touches", "index": "owner_touches_created" },
|
||||
{ "dropIndexes": "crm_followups", "index": "followup_due_scan" },
|
||||
{ "dropIndexes": "crm_followups", "index": "owner_contact_followup" }
|
||||
]
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
[
|
||||
{
|
||||
"createIndexes": "radar_watches",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "status": 1 }, "name": "owner_watches_status" },
|
||||
{ "key": { "owner_uid": 1, "created_at": -1 }, "name": "owner_watches_created" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "radar_sweeps",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "created_at": -1 }, "name": "owner_sweeps_created" },
|
||||
{ "key": { "watch_id": 1, "created_at": -1 }, "name": "watch_sweeps_created" },
|
||||
{ "key": { "job_id": 1 }, "name": "sweep_job" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "radar_opportunities",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "created_at": -1 }, "name": "owner_opportunities_created" },
|
||||
{ "key": { "owner_uid": 1, "intent_band": 1, "status": 1 }, "name": "owner_opportunity_band_status" },
|
||||
{ "key": { "owner_uid": 1, "external_id": 1 }, "name": "owner_opportunity_external", "unique": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "radar_replies",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "opportunity_id": 1, "variant": 1 }, "name": "owner_reply_variant" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "crm_contacts",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "source_platform": 1, "author_handle": 1 }, "name": "owner_contact_identity", "unique": true },
|
||||
{ "key": { "owner_uid": 1, "stage": 1, "last_touch_at": -1 }, "name": "owner_contacts_stage_touch" },
|
||||
{ "key": { "owner_uid": 1, "needs_follow_up": 1 }, "name": "owner_contacts_follow_up" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "crm_touches",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "contact_id": 1, "created_at": -1 }, "name": "owner_touches_created" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "crm_followups",
|
||||
"indexes": [
|
||||
{ "key": { "status": 1, "due_at": 1 }, "name": "followup_due_scan" },
|
||||
{ "key": { "owner_uid": 1, "contact_id": 1 }, "name": "owner_contact_followup" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[
|
||||
{ "dropIndexes": "radar_watches", "index": "owner_watches_context_status" },
|
||||
{ "dropIndexes": "radar_watches", "index": "owner_watches_product_created" },
|
||||
{ "dropIndexes": "radar_opportunities", "index": "owner_opportunities_primary_product" },
|
||||
{ "dropIndexes": "radar_opportunities", "index": "owner_opportunities_product_match" }
|
||||
]
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
[
|
||||
{
|
||||
"createIndexes": "radar_watches",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "context_mode": 1, "status": 1 }, "name": "owner_watches_context_status" },
|
||||
{ "key": { "owner_uid": 1, "product_id": 1, "created_at": -1 }, "name": "owner_watches_product_created" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "radar_opportunities",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "primary_product_id": 1, "created_at": -1 }, "name": "owner_opportunities_primary_product" },
|
||||
{ "key": { "owner_uid": 1, "product_matches.product_id": 1, "created_at": -1 }, "name": "owner_opportunities_product_match" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
[
|
||||
{ "dropIndexes": "radar_opportunities", "index": "owner_opportunities_review_posted" },
|
||||
{ "dropIndexes": "radar_opportunities", "index": "owner_opportunities_tombstone" },
|
||||
{ "dropIndexes": "radar_opportunity_review_events", "index": "owner_opportunity_review_events" }
|
||||
]
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
[
|
||||
{
|
||||
"createIndexes": "radar_opportunities",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "review_state": 1, "posted_at": -1 }, "name": "owner_opportunities_review_posted" },
|
||||
{ "key": { "owner_uid": 1, "removal_reason": 1, "removed_at": -1 }, "name": "owner_opportunities_tombstone" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"createIndexes": "radar_opportunity_review_events",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "opportunity_id": 1, "at": -1 }, "name": "owner_opportunity_review_events" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
[
|
||||
{
|
||||
"dropIndexes": "radar_demand_maps",
|
||||
"indexNames": ["owner_product_demand_map", "owner_demand_input_version"]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
[
|
||||
{
|
||||
"createIndexes": "radar_demand_maps",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "product_id": 1 }, "name": "owner_product_demand_map", "unique": true },
|
||||
{ "key": { "owner_uid": 1, "demand_input_version": 1 }, "name": "owner_demand_input_version" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[
|
||||
{ "dropIndexes": "crm_contacts", "index": "owner_contacts_active_stage_touch" }
|
||||
]
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[
|
||||
{
|
||||
"createIndexes": "crm_contacts",
|
||||
"indexes": [
|
||||
{ "key": { "owner_uid": 1, "removed_at": 1, "stage": 1, "last_touch_at": -1 }, "name": "owner_contacts_active_stage_touch" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[
|
||||
{ "dropIndexes": "jobs", "index": "radar_sweep_job_ref_unique" }
|
||||
]
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
[
|
||||
{
|
||||
"createIndexes": "jobs",
|
||||
"indexes": [
|
||||
{
|
||||
"key": { "owner_uid": 1, "template_type": 1, "ref_id": 1 },
|
||||
"name": "radar_sweep_job_ref_unique",
|
||||
"unique": true,
|
||||
"partialFilterExpression": { "template_type": "radar_sweep" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func CreateContactConversionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CreateCrmConversionReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewCreateContactConversionLogic(r.Context(), svcCtx)
|
||||
data, err := l.CreateContactConversion(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func CreateContactNoteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CreateContactNoteReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewCreateContactNoteLogic(r.Context(), svcCtx)
|
||||
data, err := l.CreateContactNote(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DeleteContactConversionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.DeleteCrmConversionReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewDeleteContactConversionLogic(r.Context(), svcCtx)
|
||||
data, err := l.DeleteContactConversion(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DeleteContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ContactIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewDeleteContactLogic(r.Context(), svcCtx)
|
||||
data, err := l.DeleteContact(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DoneFollowUpHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.FollowUpIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewDoneFollowUpLogic(r.Context(), svcCtx)
|
||||
data, err := l.DoneFollowUp(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GenerateFollowUpMessageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.GenerateFollowUpMessageReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewGenerateFollowUpMessageLogic(r.Context(), svcCtx)
|
||||
data, err := l.GenerateFollowUpMessage(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.GetContactReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewGetContactLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetContact(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetCrmStatsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CrmStatsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewGetCrmStatsLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetCrmStats(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListContactsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListContactsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewListContactsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListContacts(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListFollowUpsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListFollowUpsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewListFollowUpsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListFollowUps(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func MergeContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.MergeContactReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewMergeContactLogic(r.Context(), svcCtx)
|
||||
data, err := l.MergeContact(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func SetContactFollowUpHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SetContactFollowUpReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewSetContactFollowUpLogic(r.Context(), svcCtx)
|
||||
data, err := l.SetContactFollowUp(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func SnoozeFollowUpHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SnoozeFollowUpReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewSnoozeFollowUpLogic(r.Context(), svcCtx)
|
||||
data, err := l.SnoozeFollowUp(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UnmergeContactHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UnmergeContactReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewUnmergeContactLogic(r.Context(), svcCtx)
|
||||
data, err := l.UnmergeContact(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpdateContactConversionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpdateCrmConversionReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewUpdateContactConversionLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpdateContactConversion(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package crm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/crm"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpdateContactStageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpdateContactStageReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := crm.NewUpdateContactStageLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpdateContactStage(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package ownposts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/ownposts"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func OwnPostManageReplyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.OwnPostManageReplyReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := ownposts.NewOwnPostManageReplyLogic(r.Context(), svcCtx)
|
||||
data, err := l.OwnPostManageReply(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package ownposts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/ownposts"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func OwnPostSetReplyControlHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.OwnPostSetReplyControlReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := ownposts.NewOwnPostSetReplyControlLogic(r.Context(), svcCtx)
|
||||
data, err := l.OwnPostSetReplyControl(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func AcceptOpportunityHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AcceptOpportunityReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewAcceptOpportunityLogic(r.Context(), svcCtx)
|
||||
data, err := l.AcceptOpportunity(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ArchiveWatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.WatchIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewArchiveWatchLogic(r.Context(), svcCtx)
|
||||
data, err := l.ArchiveWatch(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func AssignWatchProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AssignProductReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewAssignWatchProductLogic(r.Context(), svcCtx)
|
||||
data, err := l.AssignWatchProduct(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func CreateOpportunityReplyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CreateReplyReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewCreateOpportunityReplyLogic(r.Context(), svcCtx)
|
||||
data, err := l.CreateOpportunityReply(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func CreateWatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CreateWatchReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewCreateWatchLogic(r.Context(), svcCtx)
|
||||
data, err := l.CreateWatch(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DeleteArchivedWatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.WatchIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewDeleteArchivedWatchLogic(r.Context(), svcCtx)
|
||||
data, err := l.DeleteArchivedWatch(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func DismissOpportunityHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.DismissOpportunityReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewDismissOpportunityLogic(r.Context(), svcCtx)
|
||||
data, err := l.DismissOpportunity(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func EnrichDemandMapHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.EnrichDemandMapReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewEnrichDemandMapLogic(r.Context(), svcCtx)
|
||||
data, err := l.EnrichDemandMap(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ExploreOpportunitiesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ExploreOpportunitiesReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewExploreOpportunitiesLogic(r.Context(), svcCtx)
|
||||
data, err := l.ExploreOpportunities(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetDemandMapHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.DemandMapProductReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewGetDemandMapLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetDemandMap(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetOpportunityHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.OpportunityIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewGetOpportunityLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetOpportunity(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetRadarCostPreviewHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CostPreviewReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewGetRadarCostPreviewLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetRadarCostPreview(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetRadarTodayHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.RadarTodayReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewGetRadarTodayLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetRadarToday(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
)
|
||||
|
||||
func GetServiceProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
l := radar.NewGetServiceProfileLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetServiceProfile()
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func GetWatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.WatchIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewGetWatchLogic(r.Context(), svcCtx)
|
||||
data, err := l.GetWatch(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ImportOpportunitiesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ImportOpportunitiesReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewImportOpportunitiesLogic(r.Context(), svcCtx)
|
||||
data, err := l.ImportOpportunities(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListOpportunitiesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListOpportunitiesReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewListOpportunitiesLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListOpportunities(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListOpportunityRepliesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListRepliesReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewListOpportunityRepliesLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListOpportunityReplies(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListSweepsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListSweepsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewListSweepsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListSweeps(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListWatchesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ListWatchesReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewListWatchesLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListWatches(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func MarkOpportunityReplyUsedHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.MarkReplyUsedReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewMarkOpportunityReplyUsedLogic(r.Context(), svcCtx)
|
||||
data, err := l.MarkOpportunityReplyUsed(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func OverrideOpportunityHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.OverrideOpportunityReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewOverrideOpportunityLogic(r.Context(), svcCtx)
|
||||
data, err := l.OverrideOpportunity(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func PauseWatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.WatchIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewPauseWatchLogic(r.Context(), svcCtx)
|
||||
data, err := l.PauseWatch(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ResumeWatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.WatchIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewResumeWatchLogic(r.Context(), svcCtx)
|
||||
data, err := l.ResumeWatch(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func SetPrimaryProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SetPrimaryProductReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewSetPrimaryProductLogic(r.Context(), svcCtx)
|
||||
data, err := l.SetPrimaryProduct(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func SuggestWatchTermsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SuggestWatchTermsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewSuggestWatchTermsLogic(r.Context(), svcCtx)
|
||||
data, err := l.SuggestWatchTerms(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func TriggerWatchSweepHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.WatchIdReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewTriggerWatchSweepLogic(r.Context(), svcCtx)
|
||||
data, err := l.TriggerWatchSweep(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpdateDemandMapHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpdateDemandMapReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewUpdateDemandMapLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpdateDemandMap(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpdateOpportunityReviewStateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ReviewStateReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewUpdateOpportunityReviewStateLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpdateOpportunityReviewState(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpdateWatchHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpdateWatchReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewUpdateWatchLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpdateWatch(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package radar
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/radar"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func UpsertServiceProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.UpsertServiceProfileReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := radar.NewUpsertServiceProfileLogic(r.Context(), svcCtx)
|
||||
data, err := l.UpsertServiceProfile(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ import (
|
|||
billing "apps/backend/internal/handler/billing"
|
||||
checkups "apps/backend/internal/handler/checkups"
|
||||
compose "apps/backend/internal/handler/compose"
|
||||
crm "apps/backend/internal/handler/crm"
|
||||
extension "apps/backend/internal/handler/extension"
|
||||
health "apps/backend/internal/handler/health"
|
||||
insightsapi "apps/backend/internal/handler/insightsapi"
|
||||
|
|
@ -32,7 +31,6 @@ import (
|
|||
proxy "apps/backend/internal/handler/proxy"
|
||||
publictools "apps/backend/internal/handler/publictools"
|
||||
publicutm "apps/backend/internal/handler/publicutm"
|
||||
radar "apps/backend/internal/handler/radar"
|
||||
research "apps/backend/internal/handler/research"
|
||||
scout "apps/backend/internal/handler/scout"
|
||||
settings "apps/backend/internal/handler/settings"
|
||||
|
|
@ -317,95 +315,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
rest.WithPrefix("/api/v1/compose"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/contacts",
|
||||
Handler: crm.ListContactsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/contacts/:id",
|
||||
Handler: crm.GetContactHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/contacts/:id",
|
||||
Handler: crm.DeleteContactHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/contacts/:id/conversion",
|
||||
Handler: crm.CreateContactConversionHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/contacts/:id/conversion",
|
||||
Handler: crm.UpdateContactConversionHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/contacts/:id/conversion",
|
||||
Handler: crm.DeleteContactConversionHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/contacts/:id/follow-up",
|
||||
Handler: crm.SetContactFollowUpHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/contacts/:id/merge",
|
||||
Handler: crm.MergeContactHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/contacts/:id/notes",
|
||||
Handler: crm.CreateContactNoteHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/contacts/:id/stage",
|
||||
Handler: crm.UpdateContactStageHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/contacts/:id/unmerge",
|
||||
Handler: crm.UnmergeContactHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/followups",
|
||||
Handler: crm.ListFollowUpsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/followups/:id/done",
|
||||
Handler: crm.DoneFollowUpHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/followups/:id/message",
|
||||
Handler: crm.GenerateFollowUpMessageHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/followups/:id/snooze",
|
||||
Handler: crm.SnoozeFollowUpHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/stats",
|
||||
Handler: crm.GetCrmStatsHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/crm"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
|
|
@ -623,11 +532,12 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/upload",
|
||||
Handler: media.UploadHandler(serverCtx),
|
||||
Path: "/generate-image",
|
||||
Handler: media.GenerateImageHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
|
||||
rest.WithPrefix("/api/v1/media"),
|
||||
)
|
||||
|
||||
|
|
@ -637,12 +547,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/generate-image",
|
||||
Handler: media.GenerateImageHandler(serverCtx),
|
||||
Path: "/upload",
|
||||
Handler: media.UploadHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
|
||||
rest.WithPrefix("/api/v1/media"),
|
||||
)
|
||||
|
||||
|
|
@ -809,16 +718,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
Path: "/load-replies",
|
||||
Handler: ownposts.OwnPostLoadRepliesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/manage-reply",
|
||||
Handler: ownposts.OwnPostManageReplyHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/reply-control",
|
||||
Handler: ownposts.OwnPostSetReplyControlHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/send-reply",
|
||||
|
|
@ -1042,170 +941,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
rest.WithPrefix("/api/v1/public/u"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/cost-preview",
|
||||
Handler: radar.GetRadarCostPreviewHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/explore",
|
||||
Handler: radar.ExploreOpportunitiesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/import",
|
||||
Handler: radar.ImportOpportunitiesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/opportunities",
|
||||
Handler: radar.ListOpportunitiesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/opportunities/:id",
|
||||
Handler: radar.GetOpportunityHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/opportunities/:id/accept",
|
||||
Handler: radar.AcceptOpportunityHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/opportunities/:id/dismiss",
|
||||
Handler: radar.DismissOpportunityHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/opportunities/:id/override",
|
||||
Handler: radar.OverrideOpportunityHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/opportunities/:id/primary-product",
|
||||
Handler: radar.SetPrimaryProductHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/opportunities/:id/replies",
|
||||
Handler: radar.ListOpportunityRepliesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/opportunities/:id/replies",
|
||||
Handler: radar.CreateOpportunityReplyHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/opportunities/:id/replies/:replyId/mark-used",
|
||||
Handler: radar.MarkOpportunityReplyUsedHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/opportunities/:id/review-state",
|
||||
Handler: radar.UpdateOpportunityReviewStateHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/products/:productId/demand-map",
|
||||
Handler: radar.GetDemandMapHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/products/:productId/demand-map",
|
||||
Handler: radar.UpdateDemandMapHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/products/:productId/demand-map/enrich",
|
||||
Handler: radar.EnrichDemandMapHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/service-profile",
|
||||
Handler: radar.GetServiceProfileHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/service-profile",
|
||||
Handler: radar.UpsertServiceProfileHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/sweeps",
|
||||
Handler: radar.ListSweepsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/today",
|
||||
Handler: radar.GetRadarTodayHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/watches",
|
||||
Handler: radar.ListWatchesHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/watches",
|
||||
Handler: radar.CreateWatchHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/watches/:id",
|
||||
Handler: radar.GetWatchHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/watches/:id",
|
||||
Handler: radar.UpdateWatchHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/watches/:id",
|
||||
Handler: radar.ArchiveWatchHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/watches/:id/assign-product",
|
||||
Handler: radar.AssignWatchProductHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/watches/:id/pause",
|
||||
Handler: radar.PauseWatchHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/watches/:id/purge",
|
||||
Handler: radar.DeleteArchivedWatchHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/watches/:id/resume",
|
||||
Handler: radar.ResumeWatchHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/watches/:id/sweep",
|
||||
Handler: radar.TriggerWatchSweepHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/watches/suggest",
|
||||
Handler: radar.SuggestWatchTermsHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/radar"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
|
|
@ -1310,11 +1045,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
Path: "/posts/:id/mark-published",
|
||||
Handler: scout.MarkPublishedHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/posts/:id/promote",
|
||||
Handler: scout.PromoteScoutPostHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/posts/:id/send",
|
||||
|
|
@ -1355,21 +1085,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
Path: "/products/import",
|
||||
Handler: scout.ImportProductHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/runs",
|
||||
Handler: scout.ListScoutRunsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/runs/:runId",
|
||||
Handler: scout.RemoveScoutRunHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/runs/:runId/posts",
|
||||
Handler: scout.ListScoutRunPostsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/scan",
|
||||
|
|
@ -1550,35 +1265,6 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
rest.WithPrefix("/api/v1/utm"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/:id/branding",
|
||||
Handler: workspaces.UpdateWorkspaceBrandingHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:id/members",
|
||||
Handler: workspaces.ListWorkspaceMembersHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/:id/members",
|
||||
Handler: workspaces.AddWorkspaceMemberHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/:id/members/:uid",
|
||||
Handler: workspaces.RemoveWorkspaceMemberHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/workspaces"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
|
|
@ -1637,4 +1323,33 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
|||
),
|
||||
rest.WithPrefix("/api/v1/workspaces"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.AuthJWT},
|
||||
[]rest.Route{
|
||||
{
|
||||
Method: http.MethodPut,
|
||||
Path: "/:id/branding",
|
||||
Handler: workspaces.UpdateWorkspaceBrandingHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/:id/members",
|
||||
Handler: workspaces.ListWorkspaceMembersHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/:id/members",
|
||||
Handler: workspaces.AddWorkspaceMemberHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodDelete,
|
||||
Path: "/:id/members/:uid",
|
||||
Handler: workspaces.RemoveWorkspaceMemberHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/workspaces"),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package scout
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/scout"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListScoutRunPostsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ScoutRunPostsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := scout.NewListScoutRunPostsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListScoutRunPosts(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package scout
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/scout"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func ListScoutRunsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ScoutRunListReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := scout.NewListScoutRunsLogic(r.Context(), svcCtx)
|
||||
data, err := l.ListScoutRuns(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package scout
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/scout"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func PromoteScoutPostHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ScoutPostIdPath
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := scout.NewPromoteScoutPostLogic(r.Context(), svcCtx)
|
||||
data, err := l.PromoteScoutPost(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// Code generated by goctl. DO NOT EDIT.
|
||||
// goctl <no value>
|
||||
|
||||
package scout
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"apps/backend/internal/logic/scout"
|
||||
"apps/backend/internal/response"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
)
|
||||
|
||||
func RemoveScoutRunHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.ScoutRunPostsReq
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
response.Write(r.Context(), w, nil, response.WrapRequestError(err))
|
||||
return
|
||||
}
|
||||
|
||||
l := scout.NewRemoveScoutRunLogic(r.Context(), svcCtx)
|
||||
data, err := l.RemoveScoutRun(&req)
|
||||
response.Write(r.Context(), w, data, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,6 @@ func (l *LoginLogic) Login(req *types.AuthLoginReq) (resp *types.AuthSessionData
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m = ensureOnboarding(l.ctx, l.svcCtx, m)
|
||||
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
|
||||
return &types.AuthSessionData{
|
||||
Tokens: types.TokenPairFromAuth(pair.AccessToken, pair.RefreshToken, pair.TokenType, pair.ExpiresIn),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ func (l *MeLogic) Me() (resp *types.MemberPublic, err error) {
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
m = ensureOnboarding(l.ctx, l.svcCtx, m)
|
||||
ids, _ := l.svcCtx.Auth.ListIdentities(l.ctx, m.UID)
|
||||
return types.MemberFromModelWithIdentities(m, ids), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
memberDomain "apps/backend/internal/module/member/domain"
|
||||
radarDomain "apps/backend/internal/module/radar/domain"
|
||||
"apps/backend/internal/svc"
|
||||
)
|
||||
|
||||
// ensureOnboarding fills completed for legacy members with an empty status who
|
||||
// already have a brand, product, or demand watch. An explicit pending stays
|
||||
// pending so a reset (for retest) is not immediately overwritten.
|
||||
func ensureOnboarding(ctx context.Context, svcCtx *svc.ServiceContext, m *memberDomain.Member) *memberDomain.Member {
|
||||
if m == nil || m.OnboardingStatus != "" {
|
||||
return m
|
||||
}
|
||||
if !hasExistingSetup(ctx, svcCtx, m.UID) {
|
||||
return m
|
||||
}
|
||||
status := memberDomain.OnboardingCompleted
|
||||
updated, err := svcCtx.Auth.UpdateUserInfo(ctx, m.UID, &memberDomain.UpdateUserInfoPatch{
|
||||
OnboardingStatus: &status,
|
||||
})
|
||||
if err != nil || updated == nil {
|
||||
return m
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func hasExistingSetup(ctx context.Context, svcCtx *svc.ServiceContext, uid int64) bool {
|
||||
if svcCtx == nil || uid <= 0 {
|
||||
return false
|
||||
}
|
||||
if svcCtx.Scout != nil {
|
||||
if brands, err := svcCtx.Scout.ListBrands(ctx, uid); err == nil && len(brands) > 0 {
|
||||
return true
|
||||
}
|
||||
if products, err := svcCtx.Scout.ListAllProducts(ctx, uid); err == nil && len(products) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if svcCtx.Radar != nil {
|
||||
if _, total, err := svcCtx.Radar.ListWatches(ctx, uid, radarDomain.WatchListFilter{Page: 1, PageSize: 1}); err == nil && total > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ func (l *ComposePublishSingleLogic) ComposePublishSingle(req *types.ComposePubli
|
|||
if !ok {
|
||||
return nil, response.Biz(401, 401001, "missing authorization")
|
||||
}
|
||||
b, err := l.svcCtx.Studio.PublishSingle(l.ctx, uid, req.AccountId, req.Text, req.Title, req.ImageUrls, req.ScheduleStartAt, req.TopicTag, req.ReplyControl)
|
||||
b, err := l.svcCtx.Studio.PublishSingle(l.ctx, uid, req.AccountId, req.Text, req.Title, req.ImageUrls, req.ScheduleStartAt, req.TopicTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type CreateContactConversionLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCreateContactConversionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateContactConversionLogic {
|
||||
return &CreateContactConversionLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *CreateContactConversionLogic) CreateContactConversion(req *types.CreateCrmConversionReq) (*types.CrmConversionData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, outcomeID, err := l.svcCtx.Crm.ReportConversion(l.ctx, uid, req.Id, req.Amount, req.Currency, req.Note)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.CrmConversionData{
|
||||
ContactId: c.ID, OutcomeId: outcomeID, Stage: c.Stage,
|
||||
Amount: req.Amount, Currency: req.Currency, Note: req.Note, UpdatedAt: c.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/logic/crmmap"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type CreateContactNoteLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCreateContactNoteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateContactNoteLogic {
|
||||
return &CreateContactNoteLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *CreateContactNoteLogic) CreateContactNote(req *types.CreateContactNoteReq) (*types.ContactTouchPublic, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err := l.svcCtx.Crm.AddNote(l.ctx, uid, req.Id, req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crmmap.Touch(t), nil
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type DeleteContactConversionLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewDeleteContactConversionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteContactConversionLogic {
|
||||
return &DeleteContactConversionLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *DeleteContactConversionLogic) DeleteContactConversion(req *types.DeleteCrmConversionReq) (*types.OkData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := l.svcCtx.Crm.DeleteConversion(l.ctx, uid, req.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.OkData{Ok: true}, nil
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type DeleteContactLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewDeleteContactLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteContactLogic {
|
||||
return &DeleteContactLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
// DeleteContact removes the contact from the working list while retaining
|
||||
// opportunity, touch, and conversion history for audit and attribution.
|
||||
func (l *DeleteContactLogic) DeleteContact(req *types.ContactIdReq) (*types.OkData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := l.svcCtx.Crm.RemoveContact(l.ctx, uid, req.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.OkData{Ok: true, Message: "contact removed from list"}, nil
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/logic/crmmap"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type DoneFollowUpLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewDoneFollowUpLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DoneFollowUpLogic {
|
||||
return &DoneFollowUpLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *DoneFollowUpLogic) DoneFollowUp(req *types.FollowUpIdReq) (*types.FollowUpPublic, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := l.svcCtx.Crm.DoneFollowUp(l.ctx, uid, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crmmap.FollowUp(f), nil
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type GenerateFollowUpMessageLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGenerateFollowUpMessageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateFollowUpMessageLogic {
|
||||
return &GenerateFollowUpMessageLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *GenerateFollowUpMessageLogic) GenerateFollowUpMessage(req *types.GenerateFollowUpMessageReq) (*types.GenerateFollowUpMessageData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text, err := l.svcCtx.Crm.GenerateFollowUpMessage(l.ctx, uid, req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.GenerateFollowUpMessageData{Text: text}, nil
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/logic/crmmap"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type GetContactLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetContactLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetContactLogic {
|
||||
return &GetContactLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *GetContactLogic) GetContact(req *types.GetContactReq) (*types.ContactDetailData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, touches, total, err := l.svcCtx.Crm.GetContact(l.ctx, uid, int64(req.Page), int64(req.PageSize), req.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
briefs := make([]types.ContactOpportunityBrief, 0)
|
||||
for _, raw := range l.svcCtx.Crm.OpportunityBriefs(l.ctx, uid, c.OpportunityIDs) {
|
||||
b := types.ContactOpportunityBrief{}
|
||||
if v, ok := raw["id"].(string); ok {
|
||||
b.Id = v
|
||||
}
|
||||
if v, ok := raw["permalink"].(string); ok {
|
||||
b.Permalink = v
|
||||
}
|
||||
if v, ok := raw["text"].(string); ok {
|
||||
b.Text = v
|
||||
}
|
||||
if v, ok := raw["intent_score"].(int); ok {
|
||||
b.IntentScore = v
|
||||
}
|
||||
if v, ok := raw["intent_band"].(string); ok {
|
||||
b.IntentBand = v
|
||||
}
|
||||
if v, ok := raw["created_at"].(int64); ok {
|
||||
b.CreatedAt = v
|
||||
}
|
||||
briefs = append(briefs, b)
|
||||
}
|
||||
return &types.ContactDetailData{
|
||||
Contact: *crmmap.Contact(c),
|
||||
Touches: crmmap.TouchList(touches),
|
||||
Pagination: crmmap.Pagination(req.Page, req.PageSize, total),
|
||||
Opportunities: briefs,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
crmUC "apps/backend/internal/module/crm/usecase"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type GetCrmStatsLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetCrmStatsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCrmStatsLogic {
|
||||
return &GetCrmStatsLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *GetCrmStatsLogic) GetCrmStats(req *types.CrmStatsReq) (*types.CrmStatsData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats, err := l.svcCtx.Crm.Stats(l.ctx, uid, req.From, req.To)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
terms := make([]types.TermConversionStat, 0, len(stats.Terms))
|
||||
for _, s := range stats.Terms {
|
||||
row := types.TermConversionStat{
|
||||
Term: s.Key, Accepted: s.Accepted, Replied: s.Replied, Won: s.Won,
|
||||
InsufficientSample: s.Accepted < crmUC.StatsMinSample,
|
||||
}
|
||||
// 樣本不足只給絕對數,不給會被過度解讀的比率(spec §9.5)
|
||||
if !row.InsufficientSample {
|
||||
row.ConversionRate = float64(s.Won) / float64(s.Accepted)
|
||||
}
|
||||
terms = append(terms, row)
|
||||
}
|
||||
sources := make([]types.SourceConversionStat, 0, len(stats.Sources))
|
||||
for _, s := range stats.Sources {
|
||||
sources = append(sources, types.SourceConversionStat{
|
||||
Source: s.Key, Won: s.Won, InsufficientSample: s.Won < crmUC.StatsMinSample,
|
||||
})
|
||||
}
|
||||
return &types.CrmStatsData{
|
||||
Terms: terms,
|
||||
Variants: []types.VariantConversionStat{},
|
||||
Sources: sources,
|
||||
UnavailableDimensions: stats.UnavailableDimensions,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"apps/backend/internal/logic/crmmap"
|
||||
"apps/backend/internal/module/crm/domain"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type ListContactsLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListContactsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListContactsLogic {
|
||||
return &ListContactsLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListContactsLogic) ListContacts(req *types.ListContactsReq) (*types.ContactListData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := domain.ContactListFilter{Query: req.Query, Stage: req.Stage, Band: req.Band, Sort: req.Sort, Page: req.Page, PageSize: req.PageSize}
|
||||
switch strings.ToLower(req.FollowUp) {
|
||||
case "true", "1":
|
||||
v := true
|
||||
f.FollowUp = &v
|
||||
case "false", "0":
|
||||
v := false
|
||||
f.FollowUp = &v
|
||||
}
|
||||
list, total, counts, err := l.svcCtx.Crm.ListContacts(l.ctx, uid, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &types.ContactListData{
|
||||
List: crmmap.ContactList(list), Pagination: crmmap.Pagination(req.Page, req.PageSize, total),
|
||||
StageCounts: crmmap.StageCounts(counts),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/logic/crmmap"
|
||||
"apps/backend/internal/module/crm/domain"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type ListFollowUpsLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewListFollowUpsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListFollowUpsLogic {
|
||||
return &ListFollowUpsLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *ListFollowUpsLogic) ListFollowUps(req *types.ListFollowUpsReq) (*types.FollowUpListData, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, total, err := l.svcCtx.Crm.ListFollowUps(l.ctx, uid, domain.FollowUpListFilter{
|
||||
Status: req.Status, Page: req.Page, PageSize: req.PageSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]types.FollowUpPublic, 0, len(list))
|
||||
// 同一聯絡人常有多筆追蹤;快取避免整頁重複查同一筆
|
||||
briefs := make(map[string]types.ContactBrief, len(list))
|
||||
for _, f := range list {
|
||||
p := crmmap.FollowUp(f)
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
brief, cached := briefs[f.ContactID]
|
||||
if !cached {
|
||||
if c, cerr := l.svcCtx.Crm.GetContactOnly(l.ctx, uid, f.ContactID); cerr == nil && c != nil {
|
||||
brief = types.ContactBrief{
|
||||
Id: c.ID, SourcePlatform: c.SourcePlatform, AuthorHandle: c.AuthorHandle,
|
||||
DisplayName: c.DisplayName, Stage: c.Stage,
|
||||
}
|
||||
}
|
||||
briefs[f.ContactID] = brief
|
||||
}
|
||||
p.Contact = brief
|
||||
out = append(out, *p)
|
||||
}
|
||||
return &types.FollowUpListData{List: out, Pagination: crmmap.Pagination(req.Page, req.PageSize, total)}, nil
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/logic/crmmap"
|
||||
"apps/backend/internal/svc"
|
||||
"apps/backend/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type MergeContactLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewMergeContactLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MergeContactLogic {
|
||||
return &MergeContactLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||||
}
|
||||
|
||||
func (l *MergeContactLogic) MergeContact(req *types.MergeContactReq) (*types.ContactPublic, error) {
|
||||
uid, err := ownerUID(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := l.svcCtx.Crm.Merge(l.ctx, uid, req.Id, req.SourceContactId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return crmmap.Contact(c), nil
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
crmDomain "apps/backend/internal/module/crm/domain"
|
||||
)
|
||||
|
||||
// notReady is returned by every crm capability that is routed but not implemented yet.
|
||||
// It maps to HTTP 501 / code 501010 in internal/response, so a caller can never mistake
|
||||
// a missing implementation for a successful empty result.
|
||||
func notReady(capability string) error {
|
||||
return fmt.Errorf("%w: %s", crmDomain.ErrNotReady, capability)
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"apps/backend/internal/domain"
|
||||
crmDomain "apps/backend/internal/module/crm/domain"
|
||||
"apps/backend/internal/response"
|
||||
)
|
||||
|
||||
// notReady envelope 契約:殘留未實作 capability 須 501,不可 102000 空成功。
|
||||
func TestNotReadyEnvelope(t *testing.T) {
|
||||
err := notReady("example.capability")
|
||||
if !errors.Is(err, crmDomain.ErrNotReady) {
|
||||
t.Fatalf("expected ErrNotReady, got %v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
response.Write(context.Background(), rec, nil, err)
|
||||
if rec.Code != http.StatusNotImplemented {
|
||||
t.Fatalf("expected HTTP 501, got %d", rec.Code)
|
||||
}
|
||||
var env response.Envelope
|
||||
if decErr := json.NewDecoder(rec.Body).Decode(&env); decErr != nil {
|
||||
t.Fatalf("decode envelope: %v", decErr)
|
||||
}
|
||||
if env.Code == domain.SuccessCode {
|
||||
t.Fatal("not-ready must not use success code")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package crm
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"apps/backend/internal/middleware"
|
||||
"apps/backend/internal/response"
|
||||
)
|
||||
|
||||
func ownerUID(ctx context.Context) (int64, error) {
|
||||
uid, ok := middleware.UIDFrom(ctx)
|
||||
if !ok || uid <= 0 {
|
||||
return 0, response.Biz(401, 401001, "missing authorization")
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue