文档目录
账号绑定机制

简介

本文件面向 DouPHP 框架的社交账号绑定机制,重点解释:

  • 新注册用户自动登录与已有用户账号绑定的处理流程
  • resolve() 方法的参数含义与完整处理路径
  • unionid 与 openid 两种登录 ID 模式的选择策略
  • 账号冲突、重复绑定检测、用户合并等关键业务逻辑
  • 账号绑定的最佳实践与安全注意事项

项目结构

社交账号绑定能力由“服务层 + 控制器”协同实现:

  • 服务层:统一封装第三方登录/绑定的通用流程,负责查找关联用户、判断是否已绑定、自动注册、写入 user_sns 表并返回跳转 URL。
  • 控制器层:以微信小程序登录为例,完成 code 换 session、unionid/openid 校验、手机号回填、账户锁定检查、事务性绑定/注册、签发 API Token 并记录审计日志。
graph TB
Client["客户端<br/>小程序/浏览器"] --> WXCtrl["微信登录控制器<br/>WeixinController"]
WXCtrl --> Svc["SNS 登录服务<br/>SnsLoginService.resolve()"]
Svc --> DB["数据库<br/>user / user_sns"]
Svc --> Auth["认证服务<br/>UserAuthService"]
WXCtrl --> Token["API Token 服务<br/>ApiTokenService"]
WXCtrl --> Audit["审计日志<br/>audit()"]

核心组件

  • SNS 登录服务(SnsLoginService)
    • 职责:根据传入的 sns 数据与当前登录态,决定“直接登录/绑定/自动注册/引导绑定页”四种结果之一,并返回最终落地 URL。
    • 关键点:支持 openid/unionid 双模式;已登录时做重复绑定检测;未登录且允许时自动注册并写 user_sns;否则将 sns 写入 Session 并跳转到绑定页面。
  • 微信登录控制器(WeixinController)
    • 职责:小程序端 code 换 session,校验 unionid/openid,处理手机号回填、账户锁定检查、事务性绑定/注册、签发 API Token、记录审计日志。
    • 关键点:当配置为 unionid 模式且缺失 unionid 时拒绝;支持按手机号关联既有账号;在 nobind=true 时直接自动注册。
  • 用户服务(UserService)
    • 职责:提供推广关系登记、资料查询等能力;在自动注册后用于建立分销关系树。

架构总览

下图展示从客户端到后端的核心调用链与数据落点:

sequenceDiagram
participant C as "客户端"
participant W as "WeixinController"
participant S as "SnsLoginService"
participant D as "数据库(user/user_sns)"
participant A as "UserAuthService"
participant T as "ApiTokenService"
C->>W : "POST /user/weixin/login(code, phone?)"
W->>W : "code 换 session_key/openid/unionid"
W->>D : "按 openid/unionid 查 user_sns.user_id"
alt 找到已绑定用户
W->>A : "loginSuccess(userId)"
W-->>C : "返回 token + 用户信息"
else 未绑定
opt unionid 模式且缺失
W-->>C : "错误:无法获取 unionid"
end
opt 允许按手机号关联
W->>D : "按手机号查用户"
alt 找到用户
W->>D : "插入 user_sns(绑定)"
W->>A : "loginSuccess(userId)"
W-->>C : "返回 token"
else 未找到
opt nobind=true
W->>D : "创建新用户(user)+user_sns"
W->>T : "签发 token"
W-->>C : "返回 token"
else 需要引导绑定
W-->>C : "返回 sns 供前端引导"
end
end
end
end

详细组件分析

SnsLoginService::resolve() 方法详解

该方法是社交登录/绑定的中枢决策器,输入与行为如下:

  • 参数说明

    • sns:必须包含 group、apptype、openid、unionid、nickname、avatar、sex。用于标识第三方身份与基础资料。
    • userProfile:当前已登录会员对象或 null。若不为空表示“已登录用户正在绑定”。
    • loginIdMode:'openid' 或 'unionid'。决定优先按哪个字段查找已存在用户。
    • nobind:布尔值。true 表示未登录时也直接自动注册,不走“引导绑定页”。
  • 处理流程

    1. 规范化 sns 数据,读取系统登录模式(mobile/email),确定后续登录字段。
    2. 先按 openid 查找 user_sns 中的 user_id;若未命中且 loginIdMode='unionid' 且 unionid 非空,再按 unionid 查找。
    3. 若找到已绑定用户:
      • 如果当前有登录态,则切换登录至该用户,清理推广标记,跳转用户中心。
    4. 若当前已登录(userProfile 有效):
      • 检测该用户是否已绑定同一 group+apptype 的第三方账号;
      • 已绑定则更新 openid/unionid;未绑定则插入 user_sns;
      • 返回“我的绑定”页面路由。
    5. 若未登录且 nobind=true:
      • 自动注册新用户,写入 user_sns,登录并跳转用户中心。
    6. 其他情况:
      • 生成随机 sns_token,将 sns 写入 Session,返回“绑定确认页”路由。
flowchart TD
Start(["进入 resolve()"]) --> Normalize["规范化 sns 数据<br/>读取登录模式"]
Normalize --> FindByOpenid["按 openid 查 user_sns.user_id"]
FindByOpenid --> Found{"找到用户?"}
Found --> |是| CheckUnionid{"mode=unionid 且 unionid 非空?"}
CheckUnionid --> |是| FindByUnionid["按 unionid 再查 user_id"]
CheckUnionid --> |否| UseOpenid["使用 openid 结果"]
FindByUnionid --> UnionFound{"找到用户?"}
UnionFound --> |是| LoginOrBind["已登录? 是则切换登录并跳转<br/>否则继续"]
UnionFound --> |否| NextStep["继续"]
UseOpenid --> LoginOrBind
LoginOrBind --> IsLoggedIn{"userProfile 有效?"}
IsLoggedIn --> |是| DupCheck["检测重复绑定(group+apptype)"]
DupCheck --> UpdateOrInsert{"已绑定?"}
UpdateOrInsert --> |是| Update["更新 openid/unionid"]
UpdateOrInsert --> |否| Insert["插入 user_sns"]
UpdateOrInsert --> ReturnBind["返回绑定页路由"]
IsLoggedIn --> |否| Nobind{"nobind=true?"}
Nobind --> |是| AutoReg["自动注册新用户+user_sns<br/>登录并跳转"]
Nobind --> |否| ToSession["sns 写入 Session<br/>返回绑定确认页路由"]
ToSession --> End(["结束"])
ReturnBind --> End
AutoReg --> End

WeixinController 小程序登录流程

  • 入口:POST /user/weixin/login
  • 关键步骤
    • 通过 code 换取 session_key/openid/unionid;
    • 若配置为 unionid 模式但缺失 unionid,直接拒绝;
    • 先按 openid 查找用户;若存在,检查账户锁定状态,必要时回填手机号,签发 Token 并记录审计日志;
    • 若不存在,且 unionid 模式开启,则尝试按 unionid 关联既有用户;
    • 若仍无匹配,且客户端提供了有效手机号,则尝试按手机号关联既有用户;
    • 若 nobind=true,则自动注册新用户,写入 user_sns,签发 Token 并记录审计日志;
    • 否则返回 sns 数据,交由前端引导绑定。
sequenceDiagram
participant App as "小程序"
participant Ctrl as "WeixinController"
participant DB as "数据库"
participant Auth as "UserAuthService"
participant Token as "ApiTokenService"
App->>Ctrl : "code, phone?"
Ctrl->>Ctrl : "code 换 session_key/openid/unionid"
alt unionid 模式且缺失
Ctrl-->>App : "错误:无法获取 unionid"
else 找到 openid 用户
Ctrl->>DB : "检查账户锁定"
Ctrl->>DB : "可选回填手机号"
Ctrl->>Auth : "loginSuccess(userId)"
Ctrl->>Token : "签发 token"
Ctrl-->>App : "成功响应"
else 未找到
opt unionid 模式
Ctrl->>DB : "按 unionid 查用户"
alt 找到
Ctrl->>DB : "插入 user_sns(绑定)"
Ctrl->>Auth : "loginSuccess(userId)"
Ctrl->>Token : "签发 token"
Ctrl-->>App : "成功响应"
end
end
opt 手机号关联
Ctrl->>DB : "按手机号查用户"
alt 找到
Ctrl->>DB : "插入 user_sns(绑定)"
Ctrl->>Auth : "loginSuccess(userId)"
Ctrl->>Token : "签发 token"
Ctrl-->>App : "成功响应"
end
end
opt nobind=true
Ctrl->>DB : "创建新用户+user_sns"
Ctrl->>Token : "签发 token"
Ctrl-->>App : "成功响应"
else
Ctrl-->>App : "返回 sns 供前端引导"
end
end

自动注册与分销关系登记

  • 自动注册触发条件:
    • 在 SnsLoginService 中,当未登录且 nobind=true;
    • 或在 WeixinController 中,当 nobind=true 且未匹配到任何既有用户。
  • 注册要点:
    • 生成唯一 user_sn,邮箱采用临时后缀;
    • 密码使用安全哈希;
    • 写入 user_sns 关联;
    • 登记推广关系树(直推及上级链),返利规则保持硬封顶 2 级。

账号冲突处理与重复绑定检测

  • 重复绑定检测:
    • 当用户已登录时,按 user_id + group + apptype 检测是否已绑定同一第三方账号;
    • 若已绑定,仅更新 openid/unionid,避免重复记录。
  • 账号冲突场景:
    • 同一 openid/unionid 可能对应不同历史账号;
    • 若当前已登录用户与第三方账号已绑定到其他用户,应提示冲突并阻止覆盖;
    • 建议在前端或控制器层增加“目标用户与当前用户不一致”时的二次确认或拒绝逻辑。

登录 ID 模式选择策略(openid vs unionid)

  • 选择依据:
    • 控制器侧根据配置 param.loginid_mode 决定使用 unionid 还是 openid;
    • 若选择 unionid 但未获取到 unionid,应拒绝登录并提示管理员配置问题;
    • 若选择 openid,则仅按 openid 进行匹配。
  • 影响范围:
    • 影响“已绑定用户查找”、“unionid 模式下的跨应用统一识别”以及“绑定/注册分支”。

用户合并(概念性建议)

  • 当前代码未提供“合并两个既有用户”的通用能力;
  • 若出现同一用户拥有多个分散账号的情况,建议在管理后台提供“合并账号”工具:
    • 将订单、资产、联系人等迁移至主账号;
    • 将子账号的 user_sns 记录迁移至主账号;
    • 保留审计日志,确保可追溯。

依赖关系分析

  • SnsLoginService 依赖:
    • UserAuthService:用于切换登录态;
    • DB:读写 user、user_sns;
    • Config:读取登录模式;
    • Str:生成随机 token。
  • WeixinController 依赖:
    • UserAuthService:登录成功事件;
    • ApiTokenService:签发 API Token;
    • UserService:推广关系登记;
    • audit():审计日志。
classDiagram
class SnsLoginService {
+resolve(sns, userProfile, loginIdMode, nobind) string
-insertUserSns(userId, sns) void
-autoRegister(sns) int
}
class WeixinController {
+login(request) Response
+getPhone(request) Response
}
class UserAuthService
class ApiTokenService
class UserService
class DB
SnsLoginService --> UserAuthService : "登录"
SnsLoginService --> DB : "读写 user/user_sns"
WeixinController --> UserAuthService : "登录成功"
WeixinController --> ApiTokenService : "签发 token"
WeixinController --> UserService : "推广关系"
WeixinController --> DB : "读写 user/user_sns"

性能与并发考虑

  • 数据库查询
    • 对 user_sns 的查找应按 openid/unionid 建立索引,减少全表扫描;
    • 重复绑定检测需加唯一约束(user_id, group, apptype)。
  • 事务与一致性
    • 绑定/注册涉及多表写入,应使用事务保证原子性;
    • 失败时回滚并记录审计日志。
  • 并发安全
    • 高并发下可能出现“同时注册同名邮箱”或“重复绑定”,需在唯一键层面兜底;
    • 对敏感操作(绑定、合并)增加幂等键或分布式锁。
  • 速率限制
    • 登录接口应实施 IP/用户维度限流,防止暴力破解与资源滥用。

故障排查指南

  • 常见问题
    • 无法获取 unionid:当配置为 unionid 模式但平台未返回 unionid,会直接报错;
    • 账户被锁定:检测到账户锁定时间大于 0 时,拒绝登录并提示重试时间;
    • 绑定失败:事务异常时回滚并记录审计日志,返回友好提示。
  • 定位方法
    • 查看审计日志中的登录成功/失败明细;
    • 核对 user_sns 表中 group/apptype/openid/unionid 的唯一性与完整性;
    • 检查配置项 param.login_mode、param.loginid_mode 是否符合预期。

结论

  • SnsLoginService 提供统一的社交登录/绑定决策流程,支持 openid/unionid 双模式、已登录绑定、未登录自动注册与引导绑定四种路径。
  • WeixinController 在小程端实现了完整的 code 换 session、unionid/openid 校验、手机号回填、账户锁定检查、事务性绑定/注册、Token 签发与审计。
  • 为保证一致性与安全性,建议:
    • 在数据库层对 user_sns 添加必要唯一约束;
    • 在高并发场景引入幂等与锁机制;
    • 对 unionid 缺失、账户锁定、绑定冲突等边界情况给出明确提示;
    • 结合审计日志持续追踪异常与风险。

附录

  • 文案与国际化
    • 账号绑定相关文案位于语言包中,便于多语言扩展。
添加日期:2026-10-05