docs: add miniprogram phone quick login design.

Capture the approved WeChat getPhoneNumber login flow for shanxia mini program, including WECHAT_MP_* config, sys_user openid columns, and parity with password login JWT.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-18 11:00:42 +08:00
1 parent 86694f739f
commit 7f3f569722
1 file changed
+222
@@ -0,0 +1,222 @@
# 山峡小程序「手机号快捷登录」设计
> 日期:2026-09-18
> 适用范围:`miniprogram` 登录页 + `backend` 微信换票 / 绑号
> 参考实现:`/Users/zlwu/work/00code/zkxw`(`mobile-h5` + `backend` mini auth)
> 视觉:登录页协议/隐私下方新增按钮,样式对齐 zkxw「手机号快捷登录」
---
## 1. 目标与已确认决策
### 1.1 目标
1. 登录页在协议、隐私下方增加「手机号快捷登录」按钮;点击后可完成登录并进入工作台。
2. 账号密码登录保持现有行为,与快捷登录互不影响。
3. 快捷登录用微信手机号匹配已有 `sys_user.phone_no`;唯一命中则登录并绑定 `wx_openid`;未找到 / 多账号失败(不自动注册)。
### 1.2 决策摘要
| 项 | 约定 |
|----|------|
| 身份策略 | 对齐 zkxw:`phone_no` 匹配 + 绑定 `wx_openid` |
| 自动注册 | **不做** |
| 密码登录后静默绑微信 | **不做**(仅快捷登录路径绑号) |
| 仅 openid 静默换票接口 | **不做**(本期不做 `wechat-bind` / silent login) |
| 密钥配置 | **新增** `WECHAT_MP_APPID` + `WECHAT_MP_SECRET`;`WXPAY_*` 不动 |
| 前端交互 | 协议下方按钮;`open-type="getPhoneNumber"` + 同次 `uni.login`;未勾选协议时确认键同次拉授权 |
| JWT | 与密码登录同一套 access / refresh / userInfo |
### 1.3 非目标
- 密码登录成功后静默绑定 openid
- 仅 openid 静默登录 / OA 深链换票
- 自动注册新用户
- web-admin 微信登录
- 改微信支付 / 模板消息现有 `WXPAY_*` 配置用途
- 客户端 / 供应商 / 配送员多端分 App 登录
---
## 2. 端到端流程
```
登录页「手机号快捷登录」
→(未勾选协议)确认弹窗「同意并登录」确认键 open-type=getPhoneNumber
→(已勾选)button open-type=getPhoneNumber
→ e.detail.code → phoneCode
→ uni.login → loginCode
→ POST /api/mini/auth/wechat-login { loginCode, phoneCode }
→ 后端 jscode2session → openid
→ 后端 getuserphonenumber → 规范化手机号
→ 按 phone_no 唯一匹配 sys_user(status 正常、del_flag=0)
→ 首次写 wx_openid;已绑则校验一致
→ 签发 JWT(同密码登录)
→ 前端存会话 → 拉 menu-codes → 进工作台
```
密码登录:现有 `POST /api/web/auth/login`(RSA)不变;**不**调用微信绑号。
---
## 3. 配置
### 3.1 后端环境变量(新增)
| 环境变量 | 用途 | 说明 |
|----------|------|------|
| `WECHAT_MP_APPID` | 小程序 AppID | 与门户正式号一致:`wx7db1ddb2a90c9425` |
| `WECHAT_MP_SECRET` | 小程序 AppSecret | 仅后端;禁止写入前端源码 |
`WXPAY_MINI_APPID` / `WXPAY_SECRET` **保持不动**(支付/模板消息)。
另需:
| 项 | 说明 |
|----|------|
| `API_ENCRYPT_EXCLUDE_PATHS` | 加入 `/api/mini/auth/wechat-login` |
| 前端 `manifest.json` → `mp-weixin.appid` | 已是 `wx7db1ddb2a90c9425`,须与 `WECHAT_MP_APPID` 一致 |
实现完成后由人工将真实 `WECHAT_MP_SECRET` 填入 `backend/.env` / `.env.production`。
### 3.2 微信公众平台前置条件
| 要求 | 说明 |
|------|------|
| 主体 | 非个人主体,且已完成微信认证 |
| 能力 | 开通「手机号快速验证」(可能按次计费) |
| 合法域名 | request 域名含现有 API 主机(如 `w.shanxiachem.com`) |
| 基础库 | 使用新版 `e.detail.code`(约 ≥ 2.21.2);不用旧版 encryptedData 解密 |
| 验收 | 以真机为准(开发者工具对 getPhoneNumber 不可靠) |
官方参考:
- [手机号快速验证组件](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber)
- [getuserphonenumber](https://developers.weixin.qq.com/miniprogram/dev/server/API/user-info/phone-number/api_getphonenumber)
---
## 4. 数据模型
### 4.1 `sys_user` 增列
| 列名 | 类型 | 说明 |
|------|------|------|
| `wx_openid` | `VARCHAR(64) NULL` | 本小程序下 openid;未绑定为 NULL |
| `wx_unionid` | `VARCHAR(64) NULL` | 有则写入;本期不做多端打通依赖 |
约束:
- 唯一索引建议:`uk_sys_user_wx_openid`(`wx_openid`);MySQL 5.7 下多 NULL 可共存;应用层保证同一 openid 不绑两人。
- 匹配手机号:`phone_no` trim / 去 `+86`/`86` 前缀规范化;要求 `status=1`、`del_flag=0`;发 token 前角色校验与密码登录一致。
- 同一规范化手机号命中多条启用账号:明确错误,不自动选人。
SQL 落在 `docs/sql/` 增量脚本,并同步 ORM `SysUser`。
---
## 5. 后端 API
### 5.1 `POST /api/mini/auth/wechat-login`
- 鉴权:无需 Bearer(登录入口)
- 请求体:
```json
{
"loginCode": "<uni.login 返回的 code>",
"phoneCode": "<getPhoneNumber 返回的 code>"
}
```
本期登录页场景 **始终传** `phoneCode`;后端无 `phoneCode` 时返回明确错误(不做 openid-only 静默登录)。
- 成功 `data`(与密码登录对齐):
```json
{
"token": "<access JWT>",
"refreshToken": "<refresh JWT>",
"expiresIn": <秒>,
"userInfo": { }
}
```
### 5.2 业务分支
| 情况 | 行为 |
|------|------|
| `openid` 已绑定用户 | 必须与本次授权手机号对应同一用户(`phones_match`);不一致 → 失败 |
| 未绑定 | 按规范化手机号查启用用户 |
| 0 条 | 失败:未找到员工账号 |
| >1 条 | 失败:手机号对应多个账号 |
| 唯一命中 | 写入 `wx_openid`(及可选 `wx_unionid`)并 commit → 发 JWT |
| openid 已被其他用户占用 | 冲突失败 |
### 5.3 微信 HTTP API(自建后端,非云开发)
1. `GET https://api.weixin.qq.com/sns/jscode2session`(`WECHAT_MP_APPID` + `WECHAT_MP_SECRET` + `loginCode`)
2. `GET https://api.weixin.qq.com/cgi-bin/token` → `access_token`
3. `POST https://api.weixin.qq.com/wxa/business/getuserphonenumber`(body: `{ "code": phoneCode }`)
### 5.4 失败提示(前端 Toast / 接口 message)
| 场景 | 提示方向 |
|------|----------|
| 用户拒绝授权 / 无 code | 需要授权手机号,或请使用账号密码登录 |
| 未找到匹配员工 | 未找到对应员工账号(请确认手机号已在系统维护) |
| 同一手机号多账号 | 手机号对应多个账号,请联系管理员 |
| openid 已绑他人 / 与手机号不一致 | 微信与账号不匹配,请联系管理员 |
| 账号停用 / 无角色 | 与密码登录相同的拒绝原因 |
---
## 6. 前端改动
| 位置 | 改动 |
|------|------|
| `miniprogram/pages/login/login.vue` | 协议/隐私下方:「或」分隔 +「手机号快捷登录」;样式对齐 zkxw;协议门禁 |
| 协议确认弹窗 | 快捷登录入口打开时,确认键带 `open-type="getPhoneNumber"`,同次点击完成同意+授权 |
| `miniprogram/config/api.js` | 增加 wechat-login 路径常量 |
| `miniprogram/stores/user.js`(或等价 auth 工具) | `wechatLogin({ loginCode, phoneCode })`;成功后复用现有存 token、menu-codes、进工作台 |
密码登录按钮与表单逻辑保持不变。
---
## 7. 后端改动清单
| 位置 | 改动 |
|------|------|
| `app/api/mini_auth.py`(新) | `wechat-login` 路由 |
| `app/services/wechat_mini_auth.py`(新) | code2session、get_phone_number、手机号规范化 |
| `app/services/wechat_login_service.py`(新) | 匹配 / 绑定 / 冲突校验 |
| 现有发 JWT 能力 | 复用密码登录签发(如 `issue_tokens` / public 登录同路径逻辑) |
| `app/models/models.py` | `SysUser.wx_openid` / `wx_unionid` |
| `app/crud/user.py` | `get_by_wx_openid`、`list_active_by_phone_no` 等 |
| `app/core/config.py` | `WECHAT_MP_*` + 加密排除 |
| `app/api/__init__.py` + `app/main.py` | 注册路由 |
| `docs/sql/` | 增量 DDL |
实现时可对照移植 zkxw 对应文件,按山峡现有命名与 JWT 签发方式适配,不引入密码登录后的 `wechat-bind`。
---
## 8. 验收标准
1. 账号密码登录仍可用,行为与现网一致。
2. 已勾选协议时,点「手机号快捷登录」→ 授权 → 匹配成功 → 进入工作台。
3. 未勾选协议时,确认「同意并登录」可同次拉起手机号授权并完成登录。
4. `phone_no` 为空 / 不匹配 / 多账号 → 失败且不创建用户。
5. 首次成功写入 `wx_openid`;再次快捷登录同号同微信可进。
6. openid 与手机号对应关系冲突时拒绝登录。
7. 真机验证通过(开发者工具仅作联调参考)。
---
## 9. 风险与依赖
- **Secret 未配置**:接口应返回可理解错误,避免空串调微信。
- **AppID 不一致**:`manifest` / `WECHAT_MP_APPID` / 微信后台必须同一正式号。
- **手机号未维护**:管理员需保证员工 `sys_user.phone_no` 正确且唯一。
- **计费**:手机号快速验证可能按次收费,需在公众平台关注额度。