recall_frontend.md
旧文件/recall_frontend.md · 22.3 KB · 2026-09-22 15:07:00
原始文件 下载

2.0 召集令「召回你的 1.0 团队」前端对接 & 运营配置文档

概述

上级的直属下级(inviter_fans[1] 指向该上级的人)在活动期内首次选定 1.0 资产解锁方案时,上级获得硬币补贴。

规则
首选加速方案 每人 +5 硬币,不封顶
首选普通方案 每满 10 人 +5 硬币;计入人数封顶 50 人(即最多 25 硬币)
首选口径 严格:首次选择必须完整落在活动期内
领取 增量领取(总补贴 − 已领 = 待领),活动结束后仍可领,不设截止

两条关键口径,前端和客服都会被问到:

设计稿 → 接口字段对照

稿面元素 取哪个字段
倒计时 6天 10时 00分 20秒 ended_atserver_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

通用约定

类型 字段 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,无需签名

活动未开启不是错误。此时接口正常返回 200enabled=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 永远是 REGULARschemeACCELERATED。两者不一致是预期行为,不要当成数据错误上报。

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_subsidypending_subsidyclaimed_subsidyaccelerated_countregular_countdirect_legacy_member_countuser_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 未登录 / 无权限 跳登录

边界行为:


接口一览

接入方 方法 路径 作用
用户端 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 参与用户列表 + 汇总