diff --git a/docs/superpowers/specs/2026-09-18-miniprogram-phone-quick-login-design.md b/docs/superpowers/specs/2026-09-18-miniprogram-phone-quick-login-design.md new file mode 100644 index 000000000..90d365039 --- /dev/null +++ b/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": "", + "phoneCode": "" +} +``` + +本期登录页场景 **始终传** `phoneCode`;后端无 `phoneCode` 时返回明确错误(不做 openid-only 静默登录)。 + +- 成功 `data`(与密码登录对齐): + +```json +{ + "token": "", + "refreshToken": "", + "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` 正确且唯一。 +- **计费**:手机号快速验证可能按次收费,需在公众平台关注额度。