|
|
@@ -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」;不新增第二套除非你要求改名 |
|