Sfoglia il codice sorgente

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>
wuzilong 1 mese fa
parent
commit
7f3f569722

+ 222 - 0
docs/superpowers/specs/2026-09-18-miniprogram-phone-quick-login-design.md

@@ -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` 正确且唯一。
+- **计费**:手机号快速验证可能按次收费,需在公众平台关注额度。