feat: unify planKey handling for lead management
- Added a new entry for the 存客宝计划 Key in the documentation, ensuring consistency between the displayed planKey and the apiKey used for retries. - Implemented a caching mechanism for the JWT token in the ckbOpenGetToken function to reduce redundant API calls. - Refactored the lead management logic to ensure that the same planKey is used for both displaying leads and retrying failed pushes, enhancing data integrity and reducing discrepancies. This update aims to streamline lead management processes and improve the reliability of API interactions.
This commit is contained in:
13
.cursor/agent/后端工程师/evolution/2026-05-08-存客宝工作台资源对接接口测试.md
Normal file
13
.cursor/agent/后端工程师/evolution/2026-05-08-存客宝工作台资源对接接口测试.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 存客宝工作台「join·资源对接」接口测试误报失败
|
||||
|
||||
## 现象
|
||||
|
||||
管理端 `CKBConfigPanel` 接口测试对 `POST /api/ckb/join` 传 `type=investor`、`userId=admin_test`。`CKBJoin` 对资源对接在非空 `userId` 时校验 `userHasContentPurchase`,`admin_test` 非真实用户且无订单 → 返回 `success:false`、`CONTENT_PURCHASE_REQUIRED`,与其它 join 子项不一致。
|
||||
|
||||
## 处理
|
||||
|
||||
在 `internal/handler/ckb.go` 中:**当 `userId` 去空格后为 `admin_test` 时跳过购课校验**(该串仅工作台连通性测试使用)。真实用户 ID 仍为 UUID,不受影响。
|
||||
|
||||
## 注意
|
||||
|
||||
勿将真实用户 ID 设为 `admin_test`;若后续收紧,可改为管理端专用 Header 或仅 `/api/db` 代理测试接口。
|
||||
37
.cursor/agent/后端工程师/evolution/2026-05-08-存客宝计划Key列表与重推一致.md
Normal file
37
.cursor/agent/后端工程师/evolution/2026-05-08-存客宝计划Key列表与重推一致.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 存客宝:计划 Key 列表展示与重推实发 apiKey 一致
|
||||
|
||||
## 问题现象
|
||||
|
||||
- 管理端「推广中心 / 获客列表」**计划Key** 列显示为某条计划(如 `2y4v5-…`)。
|
||||
- 点 **重推** 时,实际请求 `ckbapi…/v1/api/scenarios` 的 `apiKey` 却是全局 `getCkbLeadApiKey()`(如 `IPQ9s-…`),存客宝返回 `无效的apiKey` / 401。
|
||||
|
||||
## 根因
|
||||
|
||||
1. **两套解析链**:`GET /api/db/ckb-leads?mode=contact` 对 `planApiKey` 做多层兜底(`plan_api_key` → action 默认 → `target_person_id`/`persons.ckb_api_key` → index_link 全局人物);旧 **`retryOneLeadRecord`** 在 `plan_api_key` 为空时**直接** `getCkbLeadApiKey()`,且曾用 `params.targetUserId` 覆盖却**未读表字段 `target_person_id`**,甚至曾在非空快照上再被人物 key 覆盖。
|
||||
2. **展示与写请求分叉**:运营按界面计划 Key 排障,与抓包不一致,误判为「存客宝挂了」。
|
||||
|
||||
## 修复要点(已实现)
|
||||
|
||||
- 抽 **`resolveLeadPlanAPIKeyForRetry`**(命名保留历史;语义为「本条线索应采信的计划 apiKey」),顺序与列表原注释一致。
|
||||
- **`retryOneLeadRecord`** 仅通过该函数取 key;`target_person_id` 与 `params.targetUserId` 并用。
|
||||
- **`DBCKBLeadList` mode=contact** 循环内 **`planApiKey` 改为调用同一函数**,与重推、定时重试(同走 `retryOneLeadRecord`)长期同源,避免再分叉。
|
||||
- 调试用的 **`DBCKBLeadRetry` 终端打印、`pushLeadToCKB` URL 打印** 已按上线要求移除。
|
||||
|
||||
## 排查同类问题时的检查清单
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| 是否存在「列表/详情一套、再请求另一套」 | 搜索同一业务字段在两处分别拼接 |
|
||||
| `plan_api_key` 空时行为 | 是否与人物、`action/source` 一致 |
|
||||
| 人物标识 | 表字段 `target_person_id` 与 JSON `targetUserId` 是否同时考虑 |
|
||||
| 重推与首次提交 | 重推当前为 **GET scenarios 简化参数**;若与首次 **POST JSON** 不一致,可能仍有个案失败(产品另行评估) |
|
||||
|
||||
## 关联代码
|
||||
|
||||
- `soul-api/internal/handler/ckb.go`:`resolveLeadPlanAPIKeyForRetry`、`retryOneLeadRecord`
|
||||
- `soul-api/internal/handler/db_ckb_leads.go`:`DBCKBLeadList` mode=contact 中 `planApiKey`
|
||||
|
||||
## 关联文档 / Skill
|
||||
|
||||
- `.cursor/skills/ckb-leads-api-test/SKILL.md`(Gotchas:planKey 与重推一致性)
|
||||
- `.cursor/skills/api-dev/SKILL.md`(Gotchas:展示与下游请求字段同源)
|
||||
@@ -12,3 +12,5 @@
|
||||
| 2026-03-24 | router 缺失四 handler:BookRanking、DBPersonPinnedToken、CKBPinnedPerson、AdminDashboardLeads | [2026-03-24.md](./2026-03-24.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档:接口/迁移文档化与 P0/P1 标注 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 吸收沉淀:接口与需求表同步、BACKEND-P0 行 | [2026-04-14.md](./2026-04-14.md) |
|
||||
| 2026-05-08 | 存客宝获客列表计划Key与重推 apiKey:`resolveLeadPlanAPIKeyForRetry` 与列表同源,避免展示与抓包分叉 | [2026-05-08-存客宝计划Key列表与重推一致.md](./2026-05-08-存客宝计划Key列表与重推一致.md) |
|
||||
| 2026-05-08 | 存客宝工作台 join·资源对接测试:`userId=admin_test` 跳过购课校验,避免 CONTENT_PURCHASE_REQUIRED 误报 | [2026-05-08-存客宝工作台资源对接接口测试.md](./2026-05-08-存客宝工作台资源对接接口测试.md) |
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
- 2026-03-10:迁移 Mycontent-temp(主导航收敛/Settings 承载)
|
||||
- 2026-03-05:文章详情@某人(编辑页插入 @ 用户)
|
||||
- 后端工程师:`agent/后端工程师/evolution/索引.md`
|
||||
- 2026-05-08:存客宝计划 Key 列表与重推 apiKey 同源(resolveLeadPlanAPIKeyForRetry)
|
||||
- 2026-04-14:吸收沉淀(接口与需求表、BACKEND-P0)
|
||||
- 2026-04-13:按功能同步开发文档(接口/迁移文档化、P0/P1)
|
||||
- 2026-03-24:router 缺失四 handler 补齐(BookRanking 等)
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
| 2026-03-16 | 测试用例归档规则:混合→process、纯端→miniapp/web;需求变更时更新 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) |
|
||||
| 2026-04-13 | 按功能同步开发文档:以文档与索引为真源、契约缺失可阻塞 | [2026-04-13.md](./2026-04-13.md) |
|
||||
| 2026-04-14 | 吸收沉淀:LOOP 需求行与逆推清单回归 | [2026-04-14.md](./2026-04-14.md) |
|
||||
| 2026-05-08 | 获客列表计划Key与重推 apiKey 同源后可验收:同 id 下列展示与推送 key 一致 | [后端 2026-05-08](../../后端工程师/evolution/2026-05-08-存客宝计划Key列表与重推一致.md) |
|
||||
| 2026-05-07 | 存客宝工作台线索类型:join=加入、match=匹配、lead=留资;开发文档与 UI 中文映射 | [存客宝-工作台线索类型](../../../开发文档/6、后端/存客宝-工作台线索类型与中英文说明.md) |
|
||||
|
||||
@@ -112,6 +112,7 @@ description: Trigger when 编辑 soul-api、后端、API、Go、Gin、GORM、新
|
||||
| 新增表/字段后未执行迁移 | 线上报错、字段缺失 | 编写 scripts/add-xxx.sql → 执行 `node .cursor/scripts/db-exec/run.js -f soul-api/scripts/add-xxx.sql` |
|
||||
| 新增 model 未 AutoMigrate | 表不存在、启动失败 | 在 `internal/database/database.go` 中 `AutoMigrate(&model.Xxx{})` |
|
||||
| 业务逻辑直接 `os.Getenv` | 配置分散、难维护 | 仅通过 `internal/config` 的 `Load()` 读环境变量 |
|
||||
| 管理端展示的「计划 Key / 状态」与再次调用第三方用的密钥 **分两处手写解析** | 线上列表对的、重推错的;排障永远对不上抓包 | **单一真源函数**:如存客宝线索 `resolveLeadPlanAPIKeyForRetry` 同时服务 `GET .../ckb-leads?mode=contact` 的 `planApiKey` 与 `retryOneLeadRecord`,禁止列表与重推各写一套顺序 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ description: JWT 驱动的存客宝获客列表 API 回归。用户粘贴管理
|
||||
| 把 JWT 写进会议纪要/需求文档 | **账号泄露** | 仅口头/私聊粘贴;复盘里写「已用临时 token 测过」不写原文 |
|
||||
| 脚本失败就认为一定是小程序 Bug | **误判** | 先区分「接口未返回字段」vs「库里真的无手机号」——前者是后端/部署,后者才可能追 C 端 |
|
||||
| 工作台「类型」列 join/match | **混淆** | join=**加入**(ckb/join),match=**匹配**(ckb/match);留资为 lead,见 `开发文档/6、后端/存客宝-工作台线索类型与中英文说明.md` |
|
||||
| **获客列表「计划Key」与重推 URL 里 `apiKey` 不一致** | 界面是人物/快照兜底后的展示,重推曾误用全局 `CKB_LEAD_API_KEY`,报「无效的apiKey」 | 后端已统一:`resolveLeadPlanAPIKeyForRetry` 同时用于 `mode=contact` 的 `planApiKey` 与 `POST .../ckb-leads/retry` 内部推送;验收时对比**同一条 id** 下列 key 与(临时)服务端日志/抓包是否同源 |
|
||||
| 控制台中文乱码 | 难以读输出 | Windows 可先 `chcp 65001` 或设 `PYTHONIOENCODING=utf-8` |
|
||||
|
||||
---
|
||||
|
||||
@@ -182,7 +182,7 @@ export function FindPartnerPage() {
|
||||
关闭
|
||||
</Button>
|
||||
</header>
|
||||
<div className="flex-1 min-h-0 overflow-hidden px-4 pb-4 pt-3">
|
||||
<div className="flex flex-1 min-h-0 flex-col overflow-hidden px-4 pb-4 pt-3">
|
||||
<CKBConfigPanel fullBleed />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -386,11 +386,11 @@ export function CKBConfigPanel({ initialTab = 'overview', fullBleed }: { initial
|
||||
<Card
|
||||
className={
|
||||
fullBleed
|
||||
? 'bg-[#0f2137] border-orange-500/30 mb-0 flex flex-col min-h-0 flex-1 rounded-none border-x-0 border-b-0 shadow-none'
|
||||
? 'bg-[#0f2137] border-orange-500/30 mb-0 flex h-full min-h-0 flex-1 flex-col rounded-none border-x-0 border-b-0 shadow-none'
|
||||
: 'bg-[#0f2137] border-orange-500/30 mb-6'
|
||||
}
|
||||
>
|
||||
<CardContent className={`p-5 flex flex-col min-h-0 ${fullBleed ? 'flex-1 overflow-hidden' : ''}`}>
|
||||
<CardContent className={`p-5 flex flex-col min-h-0 ${fullBleed ? 'h-full flex-1 overflow-hidden' : ''}`}>
|
||||
<div className="flex items-center justify-between mb-4 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="text-white font-semibold">存客宝工作台</h3>
|
||||
@@ -444,7 +444,7 @@ export function CKBConfigPanel({ initialTab = 'overview', fullBleed }: { initial
|
||||
|
||||
<div className={`min-h-0 flex flex-col ${fullBleed ? 'flex-1 overflow-hidden' : ''}`}>
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6 overflow-auto">
|
||||
<div className={`space-y-6 overflow-y-auto ${fullBleed ? 'min-h-0 flex-1' : ''}`}>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="bg-[#0a1628] border border-gray-700/30 rounded-xl p-5">
|
||||
<p className="text-gray-400 text-xs mb-2">加入/匹配提交</p>
|
||||
@@ -631,8 +631,8 @@ export function CKBConfigPanel({ initialTab = 'overview', fullBleed }: { initial
|
||||
)}
|
||||
|
||||
{activeTab === 'config' && (
|
||||
<div className="space-y-4 overflow-auto flex-1 min-h-0 pb-4">
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="flex shrink-0 flex-wrap items-end gap-2 pb-3">
|
||||
<div className="flex-1 min-w-[200px] max-w-md">
|
||||
<Label className="text-gray-500 text-xs">筛选场景</Label>
|
||||
<Input
|
||||
@@ -662,6 +662,7 @@ export function CKBConfigPanel({ initialTab = 'overview', fullBleed }: { initial
|
||||
</Button>
|
||||
<span className="text-xs text-gray-500">{visibleRouteDefs.length} / {routeDefs.length} 项</span>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto overflow-x-hidden pr-1 pb-2">
|
||||
{visibleRouteDefs.map((item) => (
|
||||
<div key={item.key} className="bg-[#0a1628] border border-gray-700/30 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
@@ -720,6 +721,7 @@ export function CKBConfigPanel({ initialTab = 'overview', fullBleed }: { initial
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -287,7 +287,6 @@ func pushLeadToCKB(name, phone, wechatId, leadKey string) (ckbLeadPushResult, er
|
||||
}
|
||||
q.Set("sign", params["sign"].(string))
|
||||
reqURL := ckbAPIURL + "?" + q.Encode()
|
||||
fmt.Printf("[存客宝 scenarios] 请求 URL(GET): %s\n", reqURL)
|
||||
resp, err := http.Get(reqURL)
|
||||
if err != nil {
|
||||
return ckbLeadPushResult{}, err
|
||||
@@ -328,7 +327,8 @@ func resolvePersonForLead(db *gorm.DB, targetUserID string) (model.Person, bool)
|
||||
return model.Person{}, false
|
||||
}
|
||||
|
||||
// resolveLeadPlanAPIKeyForRetry 与 GET /api/db/ckb-leads?mode=contact 中 planApiKey 展示顺序一致:
|
||||
// resolveLeadPlanAPIKeyForRetry 解析本条线索应采信的存客宝计划 apiKey(GET /api/db/ckb-leads mode=contact 的 planApiKey、
|
||||
// 以及 retryOneLeadRecord 重推 pushLeadToCKB 共用本函数,禁止两处手写两套顺序):
|
||||
// 1) ckb_lead_records.plan_api_key
|
||||
// 2) action/source 默认(join/match → 内置 ckbAPIKey;lead+index_link_button → getCkbLeadApiKey)
|
||||
// 3) 仍为空:按 target_person_id 或 params.targetUserId 查人物 persons.ckb_api_key
|
||||
@@ -571,8 +571,10 @@ func CKBJoin(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的加入类型"})
|
||||
return
|
||||
}
|
||||
// 资源对接:真实用户须已购章节;管理端存客宝工作台「接口测试」固定 userId=admin_test,仅测连通性不做购课校验
|
||||
if body.Type == "investor" && body.UserID != "" {
|
||||
if !userHasContentPurchase(database.DB(), body.UserID) {
|
||||
uid := strings.TrimSpace(body.UserID)
|
||||
if uid != "admin_test" && !userHasContentPurchase(database.DB(), body.UserID) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"error": "请先购买任意章节后再申请资源对接",
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -38,12 +39,30 @@ func getCkbOpenConfig() (apiKey, account string) {
|
||||
return
|
||||
}
|
||||
|
||||
// ckbOpenGetToken 获取开放 API JWT
|
||||
var (
|
||||
ckbOpenTokenMu sync.Mutex
|
||||
ckbOpenCachedToken string
|
||||
ckbOpenCachedExpiry time.Time
|
||||
)
|
||||
|
||||
// ckbOpenTokenCacheTTL 开放 API JWT 内存缓存时长(避免 GET /api/db/persons 等高频路径对存客宝重复 /v1/open/auth/token)
|
||||
const ckbOpenTokenCacheTTL = 45 * time.Minute
|
||||
|
||||
// ckbOpenGetToken 获取开放 API JWT(带进程内短期缓存)
|
||||
func ckbOpenGetToken() (string, error) {
|
||||
apiKey, account := getCkbOpenConfig()
|
||||
if apiKey == "" || account == "" {
|
||||
return "", fmt.Errorf("无效的apiKey:CKB_OPEN_API_KEY 或 CKB_OPEN_ACCOUNT 未配置,请在后端 .env 中配置后重试")
|
||||
}
|
||||
|
||||
ckbOpenTokenMu.Lock()
|
||||
if ckbOpenCachedToken != "" && time.Now().Before(ckbOpenCachedExpiry) {
|
||||
tok := ckbOpenCachedToken
|
||||
ckbOpenTokenMu.Unlock()
|
||||
return tok, nil
|
||||
}
|
||||
ckbOpenTokenMu.Unlock()
|
||||
|
||||
ts := time.Now().Unix()
|
||||
sign := ckbOpenSign(account, ts, apiKey)
|
||||
authBody := map[string]interface{}{
|
||||
@@ -74,7 +93,13 @@ func ckbOpenGetToken() (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("%s", msg)
|
||||
}
|
||||
return authResult.Data.Token, nil
|
||||
|
||||
ckbOpenTokenMu.Lock()
|
||||
ckbOpenCachedToken = authResult.Data.Token
|
||||
ckbOpenCachedExpiry = time.Now().Add(ckbOpenTokenCacheTTL)
|
||||
out := ckbOpenCachedToken
|
||||
ckbOpenTokenMu.Unlock()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ckbOpenCreatePlan 调用 /v1/plan/create 创建获客计划,返回 planId、存客宝原始 data、以及完整响应(失败时便于排查)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -151,31 +149,18 @@ func DBCKBLeadList(c *gin.Context) {
|
||||
}
|
||||
out := make([]gin.H, 0, len(records))
|
||||
for _, r := range records {
|
||||
// 兜底:历史数据 plan_api_key 可能为空(已迁移但仍有漏网),按 action 回填展示,不改库
|
||||
planKey := strings.TrimSpace(r.PlanAPIKey)
|
||||
if planKey == "" {
|
||||
if strings.TrimSpace(r.Action) == "join" || strings.TrimSpace(r.Action) == "match" {
|
||||
planKey = ckbAPIKey
|
||||
} else if strings.TrimSpace(r.Action) == "lead" && r.Source == "index_link_button" {
|
||||
planKey = getCkbLeadApiKey()
|
||||
}
|
||||
}
|
||||
// 与重推 retryOneLeadRecord 共用 resolveLeadPlanAPIKeyForRetry,避免列表「计划Key」与实发 apiKey 再分叉
|
||||
var paramsMap map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(r.Params), ¶msMap)
|
||||
planKey := resolveLeadPlanAPIKeyForRetry(db, r, paramsMap)
|
||||
personName := ""
|
||||
ckbPlanId := int64(0)
|
||||
if p := personMap[r.TargetPersonID]; p != nil {
|
||||
personName = p.Name
|
||||
ckbPlanId = p.CkbPlanID
|
||||
// 兜底:迁移后计划 key 多写在 persons.ckb_api_key,但 lead_records.plan_api_key 可能仍为空
|
||||
// 此处仅用于管理端回显(前端会做掩码展示),不改库
|
||||
if planKey == "" && strings.TrimSpace(p.CkbApiKey) != "" {
|
||||
planKey = strings.TrimSpace(p.CkbApiKey)
|
||||
}
|
||||
} else if strings.TrimSpace(r.TargetPersonID) == "" && r.Source == "index_link_button" && indexLinkFallback != nil {
|
||||
personName = indexLinkFallback.Name
|
||||
ckbPlanId = indexLinkFallback.CkbPlanID
|
||||
if planKey == "" && strings.TrimSpace(indexLinkFallback.CkbApiKey) != "" {
|
||||
planKey = strings.TrimSpace(indexLinkFallback.CkbApiKey)
|
||||
}
|
||||
}
|
||||
displayNick := r.Nickname
|
||||
userAvatar := ""
|
||||
@@ -315,48 +300,13 @@ func DBCKBLeadList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
func logCkbLeadRetryResponse(resp gin.H) {
|
||||
b, err := json.MarshalIndent(resp, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("[DBCKBLeadRetry] response(marshal err=%v): %+v\n", err, resp)
|
||||
return
|
||||
}
|
||||
fmt.Printf("[DBCKBLeadRetry] response JSON:\n%s\n", string(b))
|
||||
}
|
||||
|
||||
func logCkbLeadRetryRequest(raw []byte, readErr error) {
|
||||
if readErr != nil {
|
||||
fmt.Printf("[DBCKBLeadRetry] request body read error: %v\n", readErr)
|
||||
return
|
||||
}
|
||||
s := strings.TrimSpace(string(raw))
|
||||
if s == "" {
|
||||
fmt.Printf("[DBCKBLeadRetry] request body: (empty)\n")
|
||||
return
|
||||
}
|
||||
if json.Valid([]byte(s)) {
|
||||
var buf bytes.Buffer
|
||||
if err := json.Indent(&buf, []byte(s), "", " "); err == nil {
|
||||
fmt.Printf("[DBCKBLeadRetry] request JSON:\n%s\n", buf.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Printf("[DBCKBLeadRetry] request body: %s\n", s)
|
||||
}
|
||||
|
||||
// DBCKBLeadRetry POST /api/db/ckb-leads/retry 管理端-手动重推单条失败线索
|
||||
func DBCKBLeadRetry(c *gin.Context) {
|
||||
raw, readErr := io.ReadAll(c.Request.Body)
|
||||
c.Request.Body = io.NopCloser(bytes.NewBuffer(raw))
|
||||
logCkbLeadRetryRequest(raw, readErr)
|
||||
|
||||
var body struct {
|
||||
ID int64 `json:"id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID <= 0 {
|
||||
resp := gin.H{"success": false, "error": "缺少有效 id"}
|
||||
logCkbLeadRetryResponse(resp)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少有效 id"})
|
||||
return
|
||||
}
|
||||
ok, err := RetryCkbLeadByID(c.Request.Context(), body.ID)
|
||||
@@ -365,20 +315,16 @@ func DBCKBLeadRetry(c *gin.Context) {
|
||||
if msg == "" {
|
||||
msg = "重推失败"
|
||||
}
|
||||
resp := gin.H{"success": false, "error": msg}
|
||||
logCkbLeadRetryResponse(resp)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": msg})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var r model.CkbLeadRecord
|
||||
if err := db.Where("id = ?", body.ID).First(&r).Error; err != nil {
|
||||
resp := gin.H{"success": true, "pushed": ok}
|
||||
logCkbLeadRetryResponse(resp)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "pushed": ok})
|
||||
return
|
||||
}
|
||||
resp := gin.H{
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"pushed": ok,
|
||||
"record": gin.H{
|
||||
@@ -392,9 +338,7 @@ func DBCKBLeadRetry(c *gin.Context) {
|
||||
"lastPushAt": r.LastPushAt,
|
||||
"nextRetryAt": r.NextRetryAt,
|
||||
},
|
||||
}
|
||||
logCkbLeadRetryResponse(resp)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
}
|
||||
|
||||
// DBCKBLeadDelete POST /api/db/ckb-leads/delete 管理端-删除一条留资记录(运营清理误报/测试数据)
|
||||
|
||||
@@ -129,6 +129,15 @@ func syncMissingSuperIndividualsAsPersons(db *gorm.DB) {
|
||||
for _, b := range bound {
|
||||
boundSet[strings.TrimSpace(b)] = struct{}{}
|
||||
}
|
||||
// 未配置「超级个体统一获客计划」时,创建 Person 走 createPersonMinimal,必须先过存客宝开放鉴权。
|
||||
// 鉴权失败若仍逐用户 ensurePersonForUser,会对每个超级个体各打一条相同错误,刷屏且拖慢 /api/db/persons。
|
||||
if _, sharedOk := loadSuperIndividualSharedPlanConfig(db); !sharedOk {
|
||||
if _, err := ckbOpenGetToken(); err != nil {
|
||||
log.Printf("[persons] sync super-individual skipped (need CKB open API or shared plan): %v — configure CKB_OPEN_API_KEY + CKB_OPEN_ACCOUNT, or system_config key %s",
|
||||
err, SuperIndividualSharedPlanConfigKey)
|
||||
return
|
||||
}
|
||||
}
|
||||
any := false
|
||||
for _, uid := range ids {
|
||||
uid = strings.TrimSpace(uid)
|
||||
|
||||
Reference in New Issue
Block a user