docs: add mobile-h5 WeChat login design (openid + phone)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-10 16:14:42 +08:00
1 parent 17a1f88626
commit 3533c42291
1 file changed
+184
@@ -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": "<wx.login code>",
"phoneCode": "<getPhoneNumber code,可选>"
}
```
处理顺序:
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": "<wx.login code>" }
```
处理:`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」;不新增第二套除非你要求改名 |