← 返回文档导航

🐝 WhatsApp 登录与 OTP 技术设计

App/H5 自生成登录码 · 用户主动发 WA · 回到 App 后消费登录码 · Meta Cloud API 收发链路
通道 Meta Cloud API v25.0 测试服务 api-id-test 正式服务 api-id core.whatsapp 登录 回 App 后消费 去重 DB 唯一键幂等
BeeX 当前主登录方式是 App/H5 自己生成登录码(例如 SX-9LEBRQ),拼出 wa.me 链接打开 WhatsApp,用户把 LOGIN SX-9LEBRQ 发给 BeeX 官方号。 Meta 将消息推送到 /webhooks/whatsapp,服务端解析后把登录码标记为 VERIFIED。用户回到 App 后,H5 才调用 POST /api/v1/auth/whatsapp/login-codes/consume 换取 accessToken / refreshToken重点变化:不再在页面后台每 2 秒轮询;只有从 WhatsApp 回到 App / 页面恢复时才消费登录码。测试环境使用 +62 851-7842-6342,正式环境使用 +62 851-6815-5794
目录 0 整体架构· 1 App/H5 登录主流程· 2 入站(用户发 WA)· 3 四层命令路由· 4 出站(系统发 WA)· 5 手机 OTP 兜底· 6 数据表· 7 配置项· 8 现状 vs 待办

0 · 整体架构

flowchart LR
  U(["📱 用户 WhatsApp"]):::trunk
  META["☁️ Meta Cloud API
graph.facebook.com/v25.0"]:::meta subgraph CORE["core service · com.seahub.x.core.whatsapp"] direction TB WH["WhatsappWebhookController
GET 验签 · POST 入站"]:::issue DP["WhatsappInboundDispatcher
幂等 · 四层路由(同步)"]:::decision CA["WhatsappCloudApiClient
sendText / sendTemplate"]:::a OTP["WhatsappFirstPhoneOtpSender
WA 优先 + 短信兜底"]:::b end U -- "① 发消息" --> META -- "② webhook POST" --> WH --> DP DP -- "③ 即时回复" --> CA -- "出站" --> META -- "④ 推达" --> U OTP -- "OTP 认证模板" --> CA classDef trunk fill:#ffffff,stroke:#e8d9b8,color:#1a1410; classDef meta fill:#eff6ff,stroke:#3b82f6,color:#1e40af; classDef issue fill:#fff7e6,stroke:#f59e0b,color:#b45309; classDef decision fill:#fef3c7,stroke:#e07c00,color:#7a4f24,stroke-width:2px; classDef a fill:#f5f3ff,stroke:#8b5cf6,color:#5b21b6; classDef b fill:#fff7ed,stroke:#fb923c,color:#9a3412;
📌 两个方向入站=用户主动发消息给官号(登录码 / 邀请码 / 带货链接 / 命令);出站=系统主动或被动发消息给用户(Bot 回复 / OTP 验证码)。两侧都经 WhatsappCloudApiClient 与 Meta 交互,所有收发记录分别落 wa_inbound_messages / wa_outbound_messages

1 · App/H5 WhatsApp 登录主流程(当前主流程)

sequenceDiagram
  autonumber
  participant U as 用户
  participant H5 as BeeX H5/App
  participant WA as WhatsApp
  participant META as Meta Cloud API
  participant API as BeeX API
  participant DB as MySQL

  U->>H5: 点击“WhatsApp 登录”
  H5->>H5: 本地生成登录码 SX-XXXXXX
  H5->>WA: 打开 wa.me/{officialNumber}?text=LOGIN SX-XXXXXX
  U->>WA: 点击发送消息
  WA->>META: 消息进入 BeeX 官方号
  META->>API: POST /webhooks/whatsapp
  API->>DB: 保存入站消息,按 wa_message_id 幂等
  API->>DB: 查找/创建 login intent,状态置为 VERIFIED
  API->>WA: 回复“登录成功,请返回 BeeX”
  U->>H5: 从 WhatsApp 回到 App/H5
  H5->>API: POST /api/v1/auth/whatsapp/login-codes/consume
  API->>DB: 校验 code 已 VERIFIED、未过期、未消费
  API-->>H5: accessToken / refreshToken / userId / referralCode
  H5->>H5: 保存登录态,进入业务页面
步骤说明关键约束
1H5/App生成登录码,例如 SX-9LEBRQ,并写入本地 pending 状态。登录码 TTL 默认 5 分钟;生成在端上完成,减少一次创建意图接口依赖。
2H5/App打开 WhatsApp:https://wa.me/{officialNumber}?text=LOGIN+SX-9LEBRQ测试环境 officialNumber=6285178426342;正式环境 officialNumber=6285168155794
3Webhook收到用户发来的 LOGIN SX-9LEBRQ,解析登录码并置为 VERIFIED。中间多个空格、换行、大小写差异都应容错;只认有效期内的码。
4H5/App用户回到 BeeX 后才调用 consume 接口换 token。不在后台持续轮询;页面恢复、App foreground、深链回跳时触发一次或短窗口重试。
5APIconsume 成功后一次性消费登录码。同一个 code 只能消费一次;已消费/过期需要重新登录。
⚠️ 体验约束:用户点击允许跳转 WhatsApp 后,BeeX 不应该一直卡在等待页。回到 App 才开始 consume;如果 code 未发送或未验证,提示“验证码未确认,请先在 WhatsApp 发送登录消息或重新登录”。如果 code 过期,提示“登录码已过期,请重新开始登录”。

2 · 入站 INBOUND(用户发 WA 给系统)

2.1 Webhook 入口 · WhatsappWebhookController/webhooks/whatsapp

方法用途逻辑
GETMeta 订阅验证校验 hub.verify_token == seahub.whatsapp.verify-token,回 hub.challenge
POST接收消息事件WhatsappInboundDispatcher同步处理完才返回 WebhookResult(含 messageCount / verifiedLoginCount / joinedCount / botReplyCount / duplicateMessageCount)

2.2 处理主链 · WhatsappInboundDispatcher.dispatch()

flowchart TD
  IN["POST /webhooks/whatsapp"]:::issue --> PARSE["WhatsappWebhookPayloadParser
解析 entry[].changes[].value.messages[]"]:::trunk PARSE --> LOOP{{"逐条消息"}}:::decision LOOP --> DUP["① 幂等落库 wa_inbound_messages
INSERT IGNORE on wa_message_id"]:::trunk DUP -->|"已存在"| SKIP["重复推送 → duplicateCount++ 跳过"]:::b DUP -->|"新消息"| TXT{"② 是文本吗?"}:::decision TXT -->|"否(图片/语音…)"| NT["回「只读文本」双语提示"]:::a TXT -->|"是"| R1{"③ 登录码 SX-XXXXX?"}:::decision R1 -->|"命中"| L["verifyByInboundMessage
意图→VERIFIED · 回登录成功链接"]:::key R1 -->|"否"| R2{"④ 邀请码 JOIN BXxxxx?"}:::decision R2 -->|"命中"| J["joinByInboundMessage
建用户 + 绑定邀请人 · 回欢迎语"]:::key R2 -->|"否"| R3["⑤ WhatsappBotService.handleMessage
带货链接 / 通用命令"]:::a L --> REPLY["sendText 回复 + 落 wa_outbound_messages"]:::a J --> REPLY R3 --> REPLY classDef trunk fill:#ffffff,stroke:#e8d9b8,color:#1a1410; classDef issue fill:#fff7e6,stroke:#f59e0b,color:#b45309; classDef decision fill:#fef3c7,stroke:#e07c00,color:#7a4f24,stroke-width:2px; classDef a fill:#f5f3ff,stroke:#8b5cf6,color:#5b21b6; classDef b fill:#fff7ed,stroke:#fb923c,color:#9a3412; classDef key fill:#fff5f5,stroke:#dc2626,color:#991b1b,stroke-width:2px;
🔒 幂等:靠 wa_inbound_messages 的唯一键 uk_wa_message_id + INSERT IGNORE,Meta 重推同一条直接跳过(createInbound() 返回 false)。
👤 建用户收敛:三类命令首次见到一个 WA 号都经 WhatsappAuthService.ensureWhatsappUser()users + user_identities + whatsapp_users;JOIN 额外 bindInviterIfAbsent()——邀请人永久锁定,只认第一次

3 · 四层命令路由(命中即停)

优先级命令解析器识别动作回复
1登录WhatsappLoginMessageParserLOGIN SX-XXXXX 或消息内包含 SX-XXXXX校验/创建登录意图 → 标记 VERIFIED登录成功 + 引导返回 BeeX(双语)
2邀请WhatsappJoinMessageParserJOIN BXxxxx建用户 + 绑定邀请人(永久锁定)欢迎语 4 态:BOUND/ALREADY_BOUND/SELF_INVITE/INVALID_CODE
3带货链接WhatsappProductLinkExtractorTikTok/Shopee URL建用户 + 生成联盟链接返回 affiliate 链接
4通用命令WhatsappCommandParserHELP/ORDERS/WALLET/ACCOUNT/WITHDRAWWhatsappBotService 查数据订单统计 / 钱包余额 / 账户信息 / 菜单
🤖 Bot 开关 seahub.whatsapp.bot.enabled(默认 true)。所有回复均为中文 + 印尼语双语。带货链接平台由 ProductLinkParser 识别(当前 TikTok Shop / Shopee)。

4 · 出站 OUTBOUND(系统发 WA 给用户)

统一经 WhatsappCloudApiClientPOST https://graph.facebook.com/v25.0/{phoneNumberId}/messagesAuthorization: Bearer {accessToken}

类型方法场景限制
textsendText()Bot 回复(登录成功 / JOIN / 命令 / 联盟链接)仅限用户来消息后的 24h 服务窗口内(Meta 规则)
templatesendAuthenticationTemplate()OTP 验证码模板需 Meta 预审;可主动推,不受 24h 限制
📤 出站结果封装为 WhatsappSendMessageResult(success / waMessageId / rawResponse / errorMessage),落 wa_outbound_messages(status=SENT/FAILED),失败接 OpsMonitor 告警wa_outbound_failed)。认证模板带 Meta 原生「复制验证码」按钮,body + button 各带同一码。

5 · 手机 OTP 兜底(验证码由 WA 或短信下发)

这一节不是当前主登录链路,而是手机号验证码兜底:现有「手机号 OTP 登录」全链路(建意图→生成码→校验→建/认手机号账号→签 token)已实现。它与上面的 WA 主登录方向相反:主登录是用户把登录码发给 BeeX 官号;手机 OTP 是 BeeX 把验证码发给用户,用户再填回 App。
flowchart TD
  S([用户在 App / H5 登录页]):::trunk --> IN["输入手机号
选「用 WhatsApp 收验证码」"]:::trunk IN --> REQ["① POST /api/v1/auth/phone/otp-intents
phone + deviceId + purpose"]:::issue REQ --> GEN["② 生成 6 位码
codeHash(SHA-256) + TTL 5min + attempts=0
返回 otpIntentId + maskedPhone"]:::trunk GEN --> SEND{"③ WhatsappFirstPhoneOtpSender 选路"}:::decision SEND -->|"WA 优先(配齐+有模板)"| WA["WhatsappCloudApiClient
认证模板(含复制按钮)"]:::a SEND -->|"WA 失败/未配/ mock"| SMS["SmsGatewayService
短信兜底"]:::b WA --> RECV["④ 用户收到验证码"]:::a SMS --> RECV RECV --> TYPE["⑤ 读码 → 填回 App"]:::trunk TYPE --> VER["⑥ POST /api/v1/auth/phone/otp-intents/{id}/verify"]:::issue VER --> CHK{"⑦ codeHash 对? · 未过期? · attempts < 上限?"}:::decision CHK -->|"✅"| OK["⑧ 认/建账号(provider=PHONE,键=手机号)
🔐 签 token · 消费意图(一次性)"]:::key CHK -->|"❌ 码错"| RT["attempts+1 → 重填(到上限锁定)"]:::issue CHK -->|"⌛ 过期/超限"| EXP["意图失效 → 重新获取"]:::issue RT --> TYPE OK --> DONE(["✅ 登录成功 · 携裂变归因码 → 绑定上线"]):::money classDef trunk fill:#ffffff,stroke:#e8d9b8,color:#1a1410; classDef issue fill:#fff7e6,stroke:#f59e0b,color:#b45309; classDef decision fill:#fef3c7,stroke:#e07c00,color:#7a4f24,stroke-width:2px; classDef a fill:#f5f3ff,stroke:#8b5cf6,color:#5b21b6; classDef b fill:#fff7ed,stroke:#fb923c,color:#9a3412; classDef key fill:#fff5f5,stroke:#dc2626,color:#991b1b,stroke-width:2px; classDef money fill:#f0fdf4,stroke:#15803d,color:#15803d;

登录方式关系

方式验证码方向现状用在哪
WhatsApp 主登录App/H5 生成码 → 用户把码发给官号 → 回 App 后 consume✅ 已实现BeeX 当前主登录方式
手机 OTP · 短信下发后端发码 → 用户填回✅ 已实现有手机号、短信可达
手机 OTP · WhatsApp 下发后端发码经 WA → 用户填回✅ 代码就绪 · 依赖模板配置设置/修改登录密码、支付密码、手机号验证等
三方登录 Google/Apple/TikTokOAuth✅ 代码在⛔ 裂变绑定流程禁用(会断绑定)

6 · 数据表

用途关键点
wa_inbound_messages入站消息唯一键 uk_wa_message_id 做幂等;存 type/content/raw_payload
wa_outbound_messages出站消息status SENT/FAILED、template_name;接 OpsMonitor 告警
whatsapp_usersWA 用户记 24h 服务窗口、绑定的 BeeX user_id、opt-in 状态
whatsapp_login_intentWA 登录意图SX-XXXXXX 码,状态 CREATED/VERIFIED/CONSUMED/EXPIRED,5min 过期。当前支持 H5/App 先生成 code,Webhook 收到后反查或创建并置 VERIFIED。
phone_otp_intents手机 OTP 意图哈希不存明文purpose 区分 LOGIN/SET_LOGIN_PASSWORD/SET_PAY_PASSWORD/SET_WITHDRAW_PASSWORD 等。

7 · 配置项

7.1 WhatsApp 账号环境

环境Display Phone Numberwa.me 号码WABA IDPhone Number ID用途
id-test+62 851-7842-6342628517842634220106631398543041158162724037776印尼测试环境登录 / Bot / 验证
id-prod+62 851-6815-5794628516815579427228260348012821204657329390185印尼正式环境登录 / Bot / 验证
id-test 客服+62 851-6815-5850628516815585032327759504445941227019117161727测试环境客服入口,不用于登录
id-prod 客服+62 851-9966-4342628519966434224430268962081141189780530883484正式环境客服入口,不用于登录

7.2 服务端与 H5 配置

配置默认含义
SEAHUB_WHATSAPP_OFFICIAL_NUMBER环境指定服务端生成 WA 登录 intent / 回复链接时使用的官方号。id-test=6285178426342,id-prod=6285168155794
NUXT_PUBLIC_WHATSAPP_OFFICIAL_NUMBER构建脚本指定H5 自生成登录码并打开 WA 时使用的官方号。prod H5 必须是 6285168155794
NUXT_PUBLIC_WHATSAPP_CUSTOMER_SUPPORT_NUMBER6285199664342联系客服入口,必须跳客服号,不要跳登录号。
seahub.whatsapp.verify-tokenchange-meMeta webhook 订阅验证令牌(GET 校验)
seahub.whatsapp.phone-number-idWA 官号电话号码 ID(Cloud API 必需)
seahub.whatsapp.access-tokenMeta Cloud API Bearer Token
seahub.whatsapp.meta-app-secretMeta App Secret,用于校验 POST webhook 的 X-Hub-Signature-256。测试、正式环境都必须通过密钥变量配置,不能留空。
seahub.whatsapp.graph-api-versionv25.0Graph API 版本
seahub.whatsapp.otp-template-namelogin_code_id登录/手机号验证使用的 WA OTP 模板名;模板不可用时才按发送器配置降级短信。
seahub.whatsapp.pay-password-template-nameedit_password_id设置或修改支付密码时使用的 WA OTP 模板名。
seahub.whatsapp.otp-template-languageidOTP 模板语言(id-test)
seahub.whatsapp.bot.enabledtrueBot 入站处理开关
seahub.auth.phone.mock-enabledfalse真实发送开关。测试和正式部署都必须保持 false;Mock 只允许本地自动化测试显式开启。
seahub.auth.phone.ttl-seconds300OTP 有效期;max-attempts=5、code-length=6

8 · 现状 vs 待办

组件 / 能力状态说明
入站 webhook + 四层路由✅ 已实现并部署登录码 / 邀请码 / 带货链接 / 命令全通,幂等去重就绪
出站 Cloud API(text/template)✅ 已实现WhatsappCloudApiClient 收发就绪
App/H5 自生成登录码✅ 已实现H5 生成 SX-XXXXXX,打开 wa.me;用户回到 App 后调用 consume。
正式/测试 WA 号区分✅ 已修复id-test 使用 6285178426342;id-prod 使用 6285168155794。H5 构建脚本会显式传 NUXT_PUBLIC_WHATSAPP_OFFICIAL_NUMBER
回 App 后消费登录码✅ 已实现只有 App foreground / 页面恢复时 consume;不再常驻后台 2 秒轮询。
手机 OTP 全链路 + WA 优先发送器✅ 已实现WhatsappFirstPhoneOtpSender(@Primary):WA 优先 + 短信兜底 + 认证模板复制按钮
WA OTP 真实发送✅ 代码已接入WhatsappFirstPhoneOtpSender 优先使用 WA 模板;部署环境必须具备对应号码的 phone-number-idaccess-token 和已审核模板。模板或通道失败时才按配置降级短信。
POST webhook HMAC 签名校验✅ 已实现WhatsappWebhookController 使用 Meta App Secret 校验 X-Hub-Signature-256。为兼容旧环境,Secret 缺失时不会执行验签,因此测试和正式部署必须配置 Secret,并监控验签失败。
异步队列(outbox + worker)⏳ 可选优化当前 webhook 同步处理,量大时响应慢 / Meta 可能重推(幂等已兜底);后续可改异步
🔑 不变的铁律
  ① 绑定键永远是手机号——无论码走 WA 还是短信,验证通过都按 provider=PHONE + 手机号 认/建同一账号
  ② 登录那刻 App 手里的裂变归因码并入绑定上线,与裂变底座对齐
  ③ 码只存 hash、TTL 5min、attempts 上限、一次性消费
  ④ 入站幂等wa_message_id 唯一键——Meta 重推不会重复建号/发奖。
⚠️ 部署基线:测试和正式环境都必须配置 meta-app-secret,使 POST webhook 强制执行 X-Hub-Signature-256(HMAC-SHA256)校验;不得依赖 Secret 为空时的兼容放行。