2.0 召集令「召回你的 1.0 团队」前端对接 & 运营配置文档
概述
上级的直属下级(inviter_fans[1] 指向该上级的人)在活动期内首次选定 1.0 资产解锁方案时,上级获得硬币补贴。
| 规则 | 值 |
|---|---|
| 首选加速方案 | 每人 +5 硬币,不封顶 |
| 首选普通方案 | 每满 10 人 +5 硬币;计入人数封顶 50 人(即最多 25 硬币) |
| 首选口径 | 严格:首次选择必须完整落在活动期内 |
| 领取 | 增量领取(总补贴 − 已领 = 待领),活动结束后仍可领,不设截止 |
两条关键口径,前端和客服都会被问到:
- 活动期开始前选过任何方案的下级,永远不计补贴 —— 哪怕他在活动期内又升级到了加速方案。
- 只认第一次选择。活动期内先选普通、之后升级到加速的下级,按普通计(
status = REGULAR,但scheme = ACCELERATED)。这不是 bug,见下面「状态枚举」一节。
设计稿 → 接口字段对照
| 稿面元素 | 取哪个字段 |
|---|---|
倒计时 6天 10时 00分 20秒 |
ended_at − server_now |
| 「活动已结束」态 | ended |
直属成员总数 38 |
direct_member_total |
加速方案 9 |
accelerated_count |
普通方案 14 |
regular_count |
待召回 15 |
pending_count + before_activity_count(见接口 1 的说明) |
| 三段堆叠进度条 | 上面四个数 |
| WhatsApp「提醒 N 个成员」 | pending_count |
| 「加速方案 +5 硬币」 | accelerated_subsidy |
| 「普通方案 +5 硬币」 | regular_tier_subsidy |
档位进度条 4 / 10 |
regular_tier_progress / regular_tier_size |
总补贴 50 硬币 |
total_subsidy |
待领取 35 硬币 |
pending_subsidy |
| 领取按钮可点 | can_claim |
| 细则「最多计 50 人」 | regular_counted_cap |
通用约定
- 所有接口 POST + JSON,无入参的接口传
{}。 - 外层响应固定为
{"code":200,"message":"success","data":{...}},下文只描述data内容。 - 业务错误同样返回 HTTP 200,形如
{"code":24028,"message":"..."},靠code判断。 int64在 JSON 里是字符串,计数字段是int32裸数字。 这是最容易出 bug 的点:
| 类型 | 字段 | JSON 形态 |
|---|---|---|
| int64 → 字符串 | started_at ended_at server_now last_claimed_at chosen_at user_id total |
"1786032000" |
| int32 → 数字 | direct_member_total accelerated_count regular_count before_activity_count pending_count regular_tier_size regular_counted_cap regular_counted_count regular_completed_tiers regular_tier_progress |
38 |
| 金额 → 字符串小数 | total_subsidy claimed_subsidy pending_subsidy accelerated_subsidy regular_tier_subsidy claimed available_point |
"50" |
所有要参与算术(百分比、取模、进度条宽度)的数字都是 int32,不用 parseInt;时间戳只需转换一次。
- 零值必然出现,不会缺字段,不用判 undefined。
- 枚举出参是名字符串(如 "LEGACY_RECALL_MEMBER_STATUS_PENDING"),入参传名或数字都可以。
- 鉴权头:
| 接入方 | 路径前缀 | 鉴权 |
|---|---|---|
| 用户端 | /v1/legacy_recall/... |
Authorization: Bearer <token> + X-Meta-Sign / X-Meta-Device-Id / X-Meta-Timestamp 签名头 |
| 管理后台 | /v1/man/legacy_recall/... |
仅 Authorization,无需签名 |
活动未开启不是错误。此时接口正常返回
200,enabled=false。前端据enabled/started/ended三态渲染,不要靠错误码判断。
补贴怎么算
计入的普通人数 = min(普通方案人数, regular_counted_cap)
总补贴 = 加速方案人数 × accelerated_subsidy
+ floor(计入的普通人数 / regular_tier_size) × regular_tier_subsidy
待领取 = max(总补贴 − 已领取, 0)
设计稿那组数字:加速 9 人、普通 14 人 →
9 × 5 + floor(min(14, 50) / 10) × 5 = 45 + 5 = 50 硬币
档位进度 = 14 % 10 = 4,即 4 / 10
前端不要自己算金额和进度,服务端全算好了。
total_subsidy/pending_subsidy/regular_tier_progress/regular_completed_tiers直接渲染即可。回传
accelerated_subsidy/regular_tier_subsidy/regular_tier_size/regular_counted_cap是给细则文案做插值用的,请把 H5 里的var SUB_ACCEL = 5; var SUB_REG = 5; var REG_CAP = 50;这三个硬编码删掉,改读接口——运营改配置时文案要跟着变。
封顶后进度条显示满格:普通人数达到或超过 50 时,regular_tier_progress 返回的是 regular_tier_size(即 10 / 10),而不是 50 % 10 = 0。直接渲染即可,不要自己取模。
接口 1:活动状态 — POST /v1/legacy_recall/state/get
请求:空,传 {}。
响应 data:
{
"enabled": true,
"started": true,
"ended": false,
"started_at": "1786032000",
"ended_at": "1786636799",
"server_now": "1786204820",
"direct_member_total": 38,
"accelerated_count": 9,
"regular_count": 14,
"before_activity_count": 0,
"pending_count": 15,
"accelerated_subsidy": "5",
"regular_tier_subsidy": "5",
"regular_tier_size": 10,
"regular_counted_cap": 50,
"regular_counted_count": 14,
"regular_completed_tiers": 1,
"regular_tier_progress": 4,
"total_subsidy": "50",
"claimed_subsidy": "15",
"pending_subsidy": "35",
"can_claim": true,
"last_claimed_at": "1786190400"
}
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
enabled |
bool | 活动总开关。false 时整个活动入口不展示 |
started |
bool | 是否已开始。enabled && server_now >= started_at |
ended |
bool | 是否已结束。true 时渲染「活动已结束」并置灰 WhatsApp 按钮;领取按钮不受影响 |
started_at |
int64 | 活动开始时间 unix 秒,未配置为 0 |
ended_at |
int64 | 活动结束时间 unix 秒,未配置为 0。倒计时目标 |
server_now |
int64 | 服务端当前时间 unix 秒 |
direct_member_total |
int32 | 直属 1.0 老用户总数(只算有金库、能选方案的下级) |
accelerated_count |
int32 | 活动期内首选加速方案的人数(计补贴) |
regular_count |
int32 | 活动期内首选普通方案的人数(封顶前的原始值) |
before_activity_count |
int32 | 活动期开始前已选过方案的人数(永不计补贴) |
pending_count |
int32 | 至今仍未选方案的人数(真正可召回的人) |
accelerated_subsidy |
string | 每个加速方案成员的补贴硬币数 |
regular_tier_subsidy |
string | 普通方案每满一档的补贴硬币数 |
regular_tier_size |
int32 | 普通方案每档人数 |
regular_counted_cap |
int32 | 普通方案计入人数上限 |
regular_counted_count |
int32 | 实际计入的普通人数 = min(regular_count, regular_counted_cap) |
regular_completed_tiers |
int32 | 已满档数 |
regular_tier_progress |
int32 | 当前档进度分子;已封顶时等于 regular_tier_size |
total_subsidy |
string | 截至当前应得总补贴(硬币) |
claimed_subsidy |
string | 累计已领取(硬币) |
pending_subsidy |
string | 待领取 = max(总补贴 − 已领, 0)(硬币) |
can_claim |
bool | 领取按钮是否可点。已含「待领取 > 0」判断 |
last_claimed_at |
int64 | 最近一次领取时间 unix 秒,未领过为 0 |
设计稿的「待召回 15」=
pending_count + before_activity_count。 稿面算式待召回 = 总数 − 加速 − 普通把「还没选」和「活动期前已选」混在了一起,后端把它们拆成了两个字段。WhatsApp 提醒人数建议只用
pending_count—— 活动期前已选过的成员再提醒也不会产生补贴,纯属骚扰。如果产品坚持要和稿面数字一致,前端自行相加。倒计时请用
server_now打底,不要用Date.now()。 用户本地时钟不准会导致倒计时和实际结束时间对不上。做法:进页面时记下offset = server_now - Date.now()/1000,之后按ended_at - (Date.now()/1000 + offset)走。四个分桶之和可能略小于
direct_member_total,差额是「活动结束后才选方案」的人。堆叠进度条请按四个分桶画,direct_member_total只作数字展示;如果自己算残差,记得夹到 0 以上。本接口的分桶计数有 60 秒缓存,新下级选完方案后最多 1 分钟才会反映到数字上。金额(
claimed_subsidy/pending_subsidy)不走缓存,领取后立即准确。
接口 2:直属成员明细 — POST /v1/legacy_recall/member/list
请求:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
offset |
int32 | 否 | 分页偏移,从 0 开始 |
limit |
int32 | 否 | 每页条数,0 按 20 处理,最大 100 |
statuses |
枚举数组 | 否 | 状态筛选,空数组 = 全部,可多选 |
常用取值:
// 「谁还没选」列表 —— 召回页主列表 + WhatsApp 提醒名单
{"offset": 0, "limit": 20, "statuses": ["LEGACY_RECALL_MEMBER_STATUS_PENDING"]}
// 与设计稿「待召回」口径一致
{"offset": 0, "limit": 20,
"statuses": ["LEGACY_RECALL_MEMBER_STATUS_PENDING", "LEGACY_RECALL_MEMBER_STATUS_BEFORE_ACTIVITY"]}
// 全部直属成员
{"offset": 0, "limit": 20}
响应 data:
{
"total": "38",
"items": [
{
"user_id": "10000123",
"nickname": "Budi",
"avatar": "https://cdn.example.com/a/123.png",
"status": "LEGACY_RECALL_MEMBER_STATUS_PENDING",
"scheme": "LegacyUnlockScheme_NONE",
"chosen_at": "0"
},
{
"user_id": "10000456",
"nickname": "Siti",
"avatar": "",
"status": "LEGACY_RECALL_MEMBER_STATUS_REGULAR",
"scheme": "LegacyUnlockScheme_ACCELERATED",
"chosen_at": "1786120000"
}
]
}
字段说明(LegacyRecallMember):
| 字段 | 类型 | 说明 |
|---|---|---|
user_id |
int64 | 成员用户 ID |
nickname |
string | 昵称,可能为空串,需要占位兜底 |
avatar |
string | 头像 URL,可能为空串,需要默认头像兜底 |
status |
枚举 | 在本活动中的状态,按首选判定 |
scheme |
枚举 | 当前生效的方案(会随升级变化) |
chosen_at |
int64 | 首次选择方案的时间 unix 秒,未选为 0 |
排序(服务端固定,不支持自定义):未选方案的排最前 → 其后按选择时间倒序 → user_id 升序兜底。
上面示例中的第二条就是那个容易被当成 bug 的情况:Siti 活动期内先选了普通方案、之后升级到加速,所以
status = REGULAR(按首选计补贴)而scheme = ACCELERATED(当前生效方案)。成员的手机号、邮箱、1.0 资产金额一律不下发。WhatsApp 按钮走通用分享链接
https://wa.me/?text=...,不需要收件人号码。若后续要做定向提醒,正确做法是新增服务端代发 + 限流的接口,而不是把手机号返给客户端。
接口 3:领取补贴 — POST /v1/legacy_recall/subsidy/claim
请求:空,传 {}。一次领走当前全部待领取金额。
响应 data:
{
"claimed": "35",
"total_subsidy": "50",
"claimed_subsidy": "50",
"pending_subsidy": "0",
"available_point": "1285"
}
| 字段 | 类型 | 说明 |
|---|---|---|
claimed |
string | 本次实际到账硬币数,用于「+35 硬币」的成功提示 |
total_subsidy |
string | 领取后的应得总补贴 |
claimed_subsidy |
string | 领取后的累计已领 |
pending_subsidy |
string | 领取后的待领取,正常为 "0" |
available_point |
string | 领取后用户的通用可用硬币余额 |
领取成功后直接用返回值刷新页面数字,不要再调一次
state/get—— 状态接口的分桶计数有 60 秒缓存,重复调用没有意义。按钮必须做防重点击(请求期间置灰)。重复点击会返回
24028,这不是异常,静默刷新即可。活动结束后领取入口保持开放,不设截止时间。 倒计时归零时只需置灰 WhatsApp 按钮,领取按钮照常可点。
前端交互流程示意
进入活动页
└─ POST /v1/legacy_recall/state/get
├─ enabled = false → 不展示活动入口(整页隐藏或跳走)
├─ enabled && !started → 展示预热态,倒计时到 started_at
├─ enabled && started && !ended → 进行中(主态)
└─ ended → 「活动已结束」,WhatsApp 按钮置灰
领取区照常展示
进行中主态渲染
├─ 倒计时 ← ended_at − (本地时间 + server_now 校正量)
├─ 成员总数 ← direct_member_total
├─ 三段堆叠条 ← accelerated_count / regular_count / (pending_count + before_activity_count)
├─ 档位进度条 ← regular_tier_progress / regular_tier_size
├─ 细则文案插值 ← accelerated_subsidy / regular_tier_subsidy / regular_tier_size / regular_counted_cap
├─ 总补贴 / 待领取 ← total_subsidy / pending_subsidy
└─ 领取按钮 ← can_claim
点「WhatsApp 提醒 N 个成员」
├─ N = pending_count
└─(可选)先拉 member/list?statuses=[PENDING] 展示名单,再唤起分享
点「领取」
├─ 按钮置灰
├─ POST /v1/legacy_recall/subsidy/claim
├─ 成功 → 用返回值就地更新 总补贴/已领/待领/余额,弹「+claimed 硬币」
├─ 24028 → 静默把待领取置 0、按钮置灰(多为重复点击)
└─ 其他错误 → 提示后恢复按钮
状态枚举
LegacyRecallMemberStatus(成员在本活动中的状态)
| 名 | 值 | 说明 |
|---|---|---|
LEGACY_RECALL_MEMBER_STATUS_UNSPECIFIED |
0 | 未指定。请求中不传或传空数组表示不过滤 |
LEGACY_RECALL_MEMBER_STATUS_PENDING |
1 | 至今未选任何方案(待召回,仍可为上级带来补贴) |
LEGACY_RECALL_MEMBER_STATUS_ACCELERATED |
2 | 活动期内首选加速方案(计补贴,不封顶) |
LEGACY_RECALL_MEMBER_STATUS_REGULAR |
3 | 活动期内首选普通方案(计补贴,满档给币) |
LEGACY_RECALL_MEMBER_STATUS_BEFORE_ACTIVITY |
4 | 活动期开始前已选过方案(永不计补贴) |
这个字段表示「首选」,不是「当前方案」。 活动期内先选普通、之后升级到加速的成员,
status永远是REGULAR,scheme是ACCELERATED。两者不一致是预期行为,不要当成数据错误上报。
LegacyUnlockScheme(当前生效的解锁方案,复用 basepb.User.LegacyUnlockScheme)
| 名 | 值 | 说明 |
|---|---|---|
LegacyUnlockScheme_NONE |
0 | 未选择 |
LegacyUnlockScheme_REGULAR |
1 | 普通方案 |
LegacyUnlockScheme_ACCELERATED |
2 | 加速方案 |
管理后台接口
参与用户列表 — POST /v1/man/legacy_recall/user/list
按上级维度聚合,只读。
请求:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
offset |
int32 | 否 | 分页偏移 |
limit |
int32 | 否 | 每页条数,0 按 25 处理,最大 100 |
sort |
string | 否 | 排序字段,见下方白名单,默认 total_subsidy |
order |
string | 否 | ASC / DESC,默认 DESC |
user_id |
int64 | 否 | 按上级用户 ID 精确筛选,0 = 不筛选 |
nickname |
string | 否 | 按上级昵称模糊筛选,空 = 不筛选 |
started_at |
int64 | 否 | 行筛选起点 unix 秒,0 = 不筛选 |
ended_at |
int64 | 否 | 行筛选终点 unix 秒,0 = 不筛选 |
sort 白名单(传其他值按默认处理):total_subsidy、pending_subsidy、claimed_subsidy、accelerated_count、regular_count、direct_legacy_member_count、user_id。
响应 data:
{
"total": "128",
"items": [
{
"user_id": "10000001",
"nickname": "Agen Jakarta",
"direct_legacy_member_count": 38,
"accelerated_count": 9,
"regular_count": 14,
"regular_counted_count": 14,
"total_subsidy": "50",
"claimed_subsidy": "15",
"pending_subsidy": "35",
"last_claimed_at": "1786190400"
}
],
"total_direct_legacy_member_count": 4210,
"total_accelerated_count": 312,
"total_regular_count": 1580,
"total_regular_counted_count": 1402,
"total_subsidy": "2260",
"total_claimed_subsidy": "980",
"total_pending_subsidy": "1280"
}
total_* 七个字段是命中筛选条件的全量汇总,不随分页变化,用于财务页脚。total_regular_counted_count 是逐上级封顶后再求和。
时间筛选只做行过滤,不改变任何统计列的口径。
started_at/ended_at的含义是「该区间内至少有 1 个计入的下级首选」,各统计列始终按完整活动期计算。原因:待领金额天然无法按时间切片(一次领取是针对整个活动期的一笔),如果统计列跟着切,同一行里的「应得总补贴」和「待领」就会回答两个不同的问题。
非管理员账号(业务员 / 代理)只能看到自己团队内的上级(含自己那行);管理员看全量。
运营配置(本期无管理后台配置页,走数据库 KV)
配置存在 configs 表,key = legacy_recall.activity,值是一段 JSON。
配置字段
| 字段 | 类型 | 说明 | 默认 |
|---|---|---|---|
enabled |
bool | 活动总开关。只控制前端入口可见性,不影响已产生补贴的领取 | false |
started_at |
int64 | 活动开始时间 unix 秒 | 必填 |
ended_at |
int64 | 活动结束时间 unix 秒 | 必填 |
accel_point |
int64 | 每个首选加速方案的直属下级给多少硬币 | 5 |
regular_group |
int64 | 普通方案每满多少人给一次 | 10 |
regular_point |
int64 | 普通方案每满一档给多少硬币 | 5 |
regular_cap |
int64 | 普通方案计入人数上限 | 50 |
accel_cap |
int64 | 加速方案计入人数上限,0 = 不限。财务兜底开关 |
0 |
开启 / 更新活动(SQL)
完整脚本见 contrib/legacy_recall_init.sql,核心是一条 upsert:
INSERT INTO configs (key, value, updated_at)
VALUES (
'legacy_recall.activity',
json_build_object(
'enabled', true,
'started_at', extract(epoch FROM timestamptz '2026-08-11 00:00:00+07')::bigint,
'ended_at', extract(epoch FROM timestamptz '2026-08-17 23:59:59+07')::bigint,
'accel_point', 5,
'regular_group', 10,
'regular_point', 5,
'regular_cap', 50,
'accel_cap', 0
)::text,
now()
)
ON CONFLICT (key) DO UPDATE
SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at;
改库后必须删缓存,否则最多 10 分钟才生效:
redis-cli DEL legacy_recall:cfg部署顺序有硬性约束:带首选快照逻辑的服务端版本必须在
started_at之前部署完成。补贴的唯一数据来源是首选快照表,代码上线前发生的选择无法追认。建议流程:先以enabled=false落库 → 用真实上级验一遍state/get的数字 → 活动开始时改enabled=true并删缓存。
关闭活动
UPDATE configs
SET value = jsonb_set(value::jsonb, '{enabled}', 'false')::text, updated_at = now()
WHERE key = 'legacy_recall.activity';
再 redis-cli DEL legacy_recall:cfg。注意 enabled=false 只隐藏前端入口,已产生的补贴仍可领取(刻意设计,避免用户因为没来得及点「领取」而丢奖励)。
排查 / QA 查询
-- 某个上级的下级首选明细
SELECT user_id, first_scheme, first_chosen_at, owner_fans_code
FROM legacy_recall_records WHERE owner_user_id = 10000001 ORDER BY first_chosen_at;
-- 某个上级的领取记录
SELECT amount, claimed_from, claimed_to, ledger_id, claimed_at
FROM legacy_recall_claim_records WHERE user_id = 10000001 ORDER BY id DESC;
-- 对账 1: 首选时间必须与金库首选时间一致, 应为 0 行
SELECT COUNT(*) FROM legacy_recall_records r
JOIN legacy_point_vaults v ON v.user_id = r.user_id
WHERE r.first_chosen_at <> v.scheme_chosen_at;
-- 对账 2: 已领总额必须等于流水总额 (type=70 即 TYPE_LEGACY_RECALL_SUBSIDY)
SELECT (SELECT COALESCE(SUM(claimed_point), 0) FROM legacy_recall_claims) AS claims_total,
(SELECT COALESCE(SUM(point), 0) FROM point_ledgers WHERE type = 70) AS ledger_total;
活动细则文案与 WhatsApp 提醒话术全部在 H5 内,服务端不提供文案配置接口,请不要新建。 服务端只回传细则里要用到的数字(费率、档位人数、封顶人数)。
错误码 / 边界
| code | 含义 | 前端处理 |
|---|---|---|
200 |
成功 | — |
24027 |
召集令活动未开启或未配置 | 隐藏活动入口 |
24028 |
暂无可领取补贴 | 置灰领取按钮并把待领取置 0(多为重复点击,静默处理) |
11007 |
限流(领取并发) | 提示稍后重试 |
11009 |
服务端数据库错误 | 通用错误提示 |
14xxx |
未登录 / 无权限 | 跳登录 |
边界行为:
- 活动结束后领取入口仍然开放,只有 WhatsApp 提醒按钮置灰。赚取侧在
ended_at冻结(之后再选方案的下级不再产生补贴)。 - 四个分桶之和可能略小于
direct_member_total,差额是活动结束后才选方案的人。自己算残差时夹到 0 以上。 - 状态接口的分桶计数有 60 秒缓存,金额不缓存。领取后立刻调
state/get也能看到正确的pending_subsidy = 0。 - 没有 1.0 金库的直属下级不计入任何分桶。这类用户(割接时资产为 0)在系统层面就无法选择方案,把他们算进「待召回」会导致进度条永远填不满。
接口一览
| 接入方 | 方法 | 路径 | 作用 |
|---|---|---|---|
| 用户端 | POST | /v1/legacy_recall/state/get |
活动状态 + 成员分桶 + 补贴概览 |
| 用户端 | POST | /v1/legacy_recall/member/list |
直属成员明细(可按状态筛选) |
| 用户端 | POST | /v1/legacy_recall/subsidy/claim |
增量领取待领补贴 |
| 管理后台 | POST | /v1/man/legacy_recall/user/list |
参与用户列表 + 汇总 |