From 3533c4229194a09701939a80c8ea996c72024ee2 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 10 Aug 2026 16:14:42 +0800 Subject: [PATCH] docs: add mobile-h5 WeChat login design (openid + phone) Co-authored-by: Cursor --- ...026-08-10-mobile-h5-wechat-login-design.md | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-10-mobile-h5-wechat-login-design.md diff --git a/docs/superpowers/specs/2026-08-10-mobile-h5-wechat-login-design.md b/docs/superpowers/specs/2026-08-10-mobile-h5-wechat-login-design.md new file mode 100644 index 00000000..958dfe3d --- /dev/null +++ b/docs/superpowers/specs/2026-08-10-mobile-h5-wechat-login-design.md @@ -0,0 +1,184 @@ +# mobile-h5 微信授权登录设计 + +> 日期:2026-08-10 +> 适用范围:`mobile-h5` 员工登录页微信授权 + `backend` 换票 / 绑定 +> 关联:`docs/superpowers/specs/2026-08-10-mobile-h5-password-login-api-design.md` +> 视觉:沿用现有登录页「微信授权登录」按钮(启用,不再置灰) + +--- + +## 1. 目标与已确认决策 + +### 1.1 目标 + +1. 已绑定微信的员工:点「微信授权登录」→ 用 `openid` 换本系统 JWT,进入工作台。 +2. 未绑定:同一次点击走手机号授权 → 匹配 `sys_user.phone_no` → 登录并绑定 `openid`。 +3. 手机号拒绝 / 失败 / 匹配不到:提示改用账号密码登录;密码登录成功后补绑当前微信。 +4. 冷启动不做静默微信登录:有本地 JWT 则进首页,否则停登录页。 + +### 1.2 决策摘要 + +| 项 | 约定 | +|----|------| +| 身份策略 | **C**:openid 绑定 + 手机号匹配 | +| 未绑定 UX | **B**:直接手机号授权;失败再引导密码登录 | +| 冷启动 | **A**:仅本地 JWT,不静默 `wx.login` | +| 前端交互 | **方案 1**:单按钮 `open-type="getPhoneNumber"` + 同次 `wx.login` | +| 密钥配置 | **仅后端 `.env`**;前端 JS 不存 AppSecret;`manifest.json` 配 AppID | +| 自动注册 | **不做**;无匹配员工则失败 | + +### 1.3 非目标 + +- unionid 多端打通(本期不做,字段可预留为空) +- 刷新 token 策略变更、全局 401 拦截(沿用密码登录期约定) +- web-admin 微信登录 +- 改支付逻辑(可复用已有小程序 AppID/Secret 配置项) + +--- + +## 2. 配置(后端 `.env`) + +**实现完成后由人工填写真实值。** 本期代码优先复用已有配置项,避免重复密钥: + +| 环境变量 | 是否已有 | 用途 | 你需要提供 | +|----------|----------|------|------------| +| `WXPAY_MINI_APPID` | 是(`config.py`) | 小程序 AppID;`code2session`、换手机号 | 小程序 AppID(`wx…`) | +| `WXPAY_SECRET` | 是(注释为小程序 Secret) | AppSecret | 小程序 AppSecret | + +前端另需(非 `.env`): + +| 位置 | 项 | 说明 | +|------|-----|------| +| `mobile-h5/manifest.json` → `mp-weixin.appid` | 与上表 AppID **一致** | 开发者工具 / 真机运行必需 | +| `mobile-h5/utils/config.js` → `API_BASE_URL` | 已有 | 指向可达后端 | + +**禁止**:把 `WXPAY_SECRET` / AppSecret 写入任何 `mobile-h5` 源码或仓库明文文档示例以外的真实值。 + +实现结束时的交付物:在回复中再次罗列上表,等你填入 `backend/.env`(及确认 `manifest.json` appid)。 + +--- + +## 3. 数据模型 + +### 3.1 `sys_user` 增列 + +| 列名 | 类型 | 说明 | +|------|------|------| +| `wx_openid` | `VARCHAR(64) NULL` | 本小程序下 openid;未绑定为 NULL | +| (可选)`wx_bind_time` | `DATETIME NULL` | 首次绑定时间,便于审计;可省略以减小范围 | + +约束建议: + +- 唯一索引:`uk_sys_user_wx_openid`(`wx_openid`),**仅对非空值唯一**(MySQL 5.7 下多 NULL 可共存;应用层保证同一 openid 不绑两人)。 +- 匹配手机号:沿用现有 `phone_no`;匹配时 trim,且要求 `status=1`、`del_flag=0`、已分配角色(与密码登录一致)。 +- 若同一 `phone_no` 命中多条启用账号:返回明确错误「手机号对应多个账号,请使用账号密码登录」,不自动选人。 + +SQL 落在 `docs/sql/`(增量脚本),并同步 ORM `SysUser`。 + +--- + +## 4. 后端 API + +路径前缀建议:`/api/web/auth/...`(与现员工 JWT 体系统一)或 `/api/mini/auth/...`。 +**推荐**:`/api/mini/auth/*`,语义清晰;发的仍是与 web 相同结构的 access/refresh JWT + `userInfo`。 +登录相关路径加入 `API_ENCRYPT_EXCLUDE_PATHS`(若启用对称加密)。 + +### 4.1 `POST /api/mini/auth/wechat-login` + +**未登录可调。** Body: + +```json +{ + "loginCode": "", + "phoneCode": "" +} +``` + +处理顺序: + +1. 用 `loginCode` 调微信 `jscode2session` → `openid`(`session_key` 仅存服务端内存/本次,不下发)。 +2. 若 `sys_user.wx_openid = openid` 命中合法用户 → 发 JWT(同密码登录会话逻辑)→ 返回成功。 + (此时可忽略 `phoneCode`。) +3. 若未绑定: + - 无 `phoneCode` → `code=400`,`message` 提示需授权手机号或改用密码登录。 + - 有 `phoneCode` → 调微信「手机号快速验证」换手机号 → 按 `phone_no` 查用户: + - 命中唯一合法用户 → 写入 `wx_openid`(若已被其他用户占用则冲突错误)→ 发 JWT。 + - 未命中 / 多命中 / 停用 / 无角色 → 对应业务错误文案。 +4. 微信 API 失败:返回可读错误(不泄露 secret)。 + +成功响应 `data` 与 `/web/auth/login` 对齐:`token`、`refreshToken`、`expiresIn`、`userInfo`。 + +### 4.2 `POST /api/mini/auth/wechat-bind` + +**需 Bearer JWT(密码登录后)。** Body: + +```json +{ "loginCode": "" } +``` + +处理:`code2session` → 将 `openid` 绑定到当前用户;若 openid 已绑定其他账号则失败;若已绑定当前用户则幂等成功。 + +### 4.3 微信 HTTP 封装 + +新建薄服务(如 `app/services/wechat_mini_auth.py`): + +- `code2session(login_code) -> { openid, session_key, unionid? }` +- `get_phone_number(phone_code) -> phone_number`(新版 phonenumber.getPhoneNumber) + +使用 `WXPAY_MINI_APPID` + `WXPAY_SECRET`。缺配置时接口返回明确「未配置小程序凭证」。 + +--- + +## 5. 前端流程 + +### 5.1 微信按钮 + +- 去掉 `is-disabled`;使用原生/uni `button`:`open-type="getPhoneNumber"`,样式对齐现有 `.login-wechat`。 +- 协议门禁:未同意则先 `ConfirmDialog`(与密码登录一致);同意后再进入授权(可用 `v-if` 切换普通 view / 授权 button,或点击时先 ensureAgree)。 +- `@getphonenumber` 回调中: + 1. 若 `e.detail.errMsg` 表示拒绝/失败 → Toast,文案引导账号密码登录。 + 2. 否则 `uni.login` 取 `loginCode`,与 `phoneCode`(`e.detail.code`)一并 `wechatLogin`。 + 3. 成功则存会话(复用 `auth.js` 的 save 逻辑)→ 进首页。 + 4. 业务错误 Toast 展示后端 `message`。 + +**已绑定用户**:仍会走 `getPhoneNumber` 弹层(方案 1 已知代价);后端优先 openid,可不消费手机号。 + +### 5.2 密码登录后补绑 + +`passwordLogin` 成功后(不阻塞进首页的体验): + +- 异步 `uni.login` → 带 token 调 `wechat-bind`;失败仅 `console`/静默,不打断「登录成功」。 + +### 5.3 会话 + +继续使用 `zkxw_mobile_token` / `refresh_token` / `user`;与密码登录共用 `auth.js`。 + +--- + +## 6. 安全与校验 + +- AppSecret 仅服务端。 +- `phone_no` 匹配前规范化(去空格;若库中无 +86 前缀则去掉微信返回的国家码再比)。 +- 绑定 openid 必须登录态或「手机号匹配成功的同一事务」,禁止匿名只传 openid 绑任意账号。 +- 与密码登录相同的停用 / 无角色拒绝策略。 + +--- + +## 7. 验收标准 + +1. 已绑定用户:点微信登录可进首页,Storage 为真实 JWT。 +2. 未绑定且 `phone_no` 正确:授权手机号后登录成功,且 `wx_openid` 已写入。 +3. 拒绝手机号 / 无匹配用户:Toast 明确,可改用密码登录;密码成功后再次微信可走 openid。 +4. 未配置 AppID/Secret:后端返回可读错误,前端 Toast,不崩溃。 +5. `.env` 清单在实现完成后再次发给你填写。 + +--- + +## 8. 风险 + +| 风险 | 缓解 | +|------|------| +| 已绑定仍弹手机号框 | 方案 1 接受;若体验不可接受下期改方案 2 | +| 手机号不准 / 重复 | 明确错误;要求主数据维护 | +| 开发者工具与真机差异 | 真机验证 getPhoneNumber;开发期可用已绑定账号测 openid 路径 | +| `WXPAY_SECRET` 命名易误解 | 文档写清「小程序 AppSecret」;不新增第二套除非你要求改名 |