docs: add mobile-h5 WeChat login design (openid + phone)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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」;不新增第二套除非你要求改名 |
|
||||
Reference in new issue
Block a user