日期:2026-09-18
适用范围:miniprogram登录页 +backend微信换票 / 绑号
参考实现:/Users/zlwu/work/00code/zkxw(mobile-h5+backendmini auth)
视觉:登录页协议/隐私下方新增按钮,样式对齐 zkxw「手机号快捷登录」
sys_user.phone_no;唯一命中则登录并绑定 wx_openid;未找到 / 多账号失败(不自动注册)。| 项 | 约定 |
|---|---|
| 身份策略 | 对齐 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 |
WXPAY_* 配置用途登录页「手机号快捷登录」
→(未勾选协议)确认弹窗「同意并登录」确认键 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)不变;不调用微信绑号。
| 环境变量 | 用途 | 说明 |
|---|---|---|
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。
| 要求 | 说明 |
|---|---|
| 主体 | 非个人主体,且已完成微信认证 |
| 能力 | 开通「手机号快速验证」(可能按次计费) |
| 合法域名 | request 域名含现有 API 主机(如 w.shanxiachem.com) |
| 基础库 | 使用新版 e.detail.code(约 ≥ 2.21.2);不用旧版 encryptedData 解密 |
| 验收 | 以真机为准(开发者工具对 getPhoneNumber 不可靠) |
官方参考:
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。
POST /api/mini/auth/wechat-login{
"loginCode": "<uni.login 返回的 code>",
"phoneCode": "<getPhoneNumber 返回的 code>"
}
本期登录页场景 始终传 phoneCode;后端无 phoneCode 时返回明确错误(不做 openid-only 静默登录)。
data(与密码登录对齐):{
"token": "<access JWT>",
"refreshToken": "<refresh JWT>",
"expiresIn": <秒>,
"userInfo": { }
}
| 情况 | 行为 |
|---|---|
openid 已绑定用户 |
必须与本次授权手机号对应同一用户(phones_match);不一致 → 失败 |
| 未绑定 | 按规范化手机号查启用用户 |
| 0 条 | 失败:未找到员工账号 |
| >1 条 | 失败:手机号对应多个账号 |
| 唯一命中 | 写入 wx_openid(及可选 wx_unionid)并 commit → 发 JWT |
| openid 已被其他用户占用 | 冲突失败 |
GET https://api.weixin.qq.com/sns/jscode2session(WECHAT_MP_APPID + WECHAT_MP_SECRET + loginCode)GET https://api.weixin.qq.com/cgi-bin/token → access_tokenPOST https://api.weixin.qq.com/wxa/business/getuserphonenumber(body: { "code": phoneCode })| 场景 | 提示方向 |
|---|---|
| 用户拒绝授权 / 无 code | 需要授权手机号,或请使用账号密码登录 |
| 未找到匹配员工 | 未找到对应员工账号(请确认手机号已在系统维护) |
| 同一手机号多账号 | 手机号对应多个账号,请联系管理员 |
| openid 已绑他人 / 与手机号不一致 | 微信与账号不匹配,请联系管理员 |
| 账号停用 / 无角色 | 与密码登录相同的拒绝原因 |
| 位置 | 改动 |
|---|---|
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、进工作台 |
密码登录按钮与表单逻辑保持不变。
| 位置 | 改动 |
|---|---|
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。
phone_no 为空 / 不匹配 / 多账号 → 失败且不创建用户。wx_openid;再次快捷登录同号同微信可进。manifest / WECHAT_MP_APPID / 微信后台必须同一正式号。sys_user.phone_no 正确且唯一。