生产用 .env.production 与 pnpm build;测试用 .env.nanafa 与 pnpm build:test;本地 pnpm dev / dev:ideship 可切换。 Co-authored-by: Cursor <cursoragent@cursor.com>
15 KiB
OMS 微信扫码登录
版本:v1.1
日期:2026-07-01
范围:PC 端客户门户微信扫码登录(/登录页 → 微信 Tab)
1. 概述
OMS 客户门户通过微信公众号临时二维码 + 扫码事件回调 + 缓存桥接 + 前端轮询完成登录。
核心思路:
- PC 端向后端申请带 loginKey 的二维码;
- 用户微信扫码后,公众号向 OMS 推送
SCAN/subscribe事件; - 后端根据扫码微信的 openId 查找已绑定客户,将账号信息写入缓存(key = loginKey,TTL 10 分钟);
- PC 端轮询该 key,有值则调用登录接口签发 JWT。
前提: 微信 openId 须已在 t_customer.WechatOpenid 中绑定(通过 H5 注册/绑定页完成,本文不展开)。
门户限制: 仅客户门户 / 提供微信扫码;管理端 /admin 不提供。
2. 扫码登录流程
2.1 时序图
sequenceDiagram
participant PC as 客户浏览器
participant FE as OMS 前端
participant API as OMS API
participant Cache as 分布式缓存
participant WX as 微信公众号
participant DB as MySQL
PC->>FE: 打开登录页 → 切换微信登录
FE->>FE: 生成 loginKey(32 位无横线 UUID)
FE->>API: GET /admin/wechat/mp/login-qrcode?loginUuid={loginKey}
API->>WX: POST qrcode/create(scene_str = loginKey,600s)
API-->>FE: { loginKey, qrCodeUrl, expiresIn: 600 }
FE->>FE: 展示二维码,启动轮询(每 3s)
Note over WX: 用户微信扫码
WX->>API: POST /api/apiwechat/verification(SCAN 或 subscribe)
API->>DB: GetByWechatOpenidAsync(openId)
alt 未绑定微信
API-->>WX: 文本「未绑定账号,请注册或绑定」
else 已绑定
API->>Cache: SET oms:wechat:login:{loginKey} = 账号 JSON,TTL 600s
API-->>WX: 文本「扫码成功,请返回电脑端完成登录」
end
loop 每 3 秒
FE->>API: GET /admin/rbac/auth/wx-login/status?loginUuid={loginKey}
API->>Cache: GET session
API-->>FE: { ready: false } 或 { ready: true, account: "C001" }
end
FE->>API: POST /admin/rbac/auth/wx-login { loginUuid: loginKey }
API->>Cache: GET + DELETE session
API->>DB: 校验客户状态、更新最后登录
API->>API: 签发 JWT(OmsLoginType.Customer)
API-->>FE: LoginResultDto
FE->>PC: 存 Token,跳转首页
2.2 前端轮询流程
flowchart TD
A[onMounted] --> B[生成 loginKey]
B --> C[GET login-qrcode]
C --> D{qrCodeUrl 有效?}
D -- 否 --> E[提示加载失败]
D -- 是 --> F[展示二维码]
F --> G[启动 3s 轮询 + 10min 过期定时器]
G --> H[GET wx-login/status]
H --> I{ready === true?}
I -- 否 --> J[等待 3s]
J --> H
I -- 是 --> K[POST wx-login]
K --> L{accessToken 有效?}
L -- 否 --> J
L -- 是 --> M[loginByWxResult + 跳转首页]
G --> N[10 分钟到] --> O[停止轮询]
P[点击刷新] --> B
2.3 后端扫码事件流程
flowchart TD
A[POST /apiwechat/verification] --> B[验签 signature]
B -- 失败 --> C[返回 error]
B -- 成功 --> D[解析 XML 消息]
D --> E{MsgType = event?}
E -- 否 --> F[空 XML 响应]
E -- 是 --> G{Event = SCAN 或 subscribe?}
G -- 否 --> F
G -- 是 --> H[WeChatQrSceneParser 解析 EventKey → loginKey]
H --> I{loginKey 存在?}
I -- 否 --> J[subscribe 回复「感谢关注」]
I -- 是 --> K[openId 查 t_customer]
K --> L{客户已绑定?}
L -- 否 --> M[回复「未绑定账号…」]
L -- 是 --> N[写入缓存 session JSON]
N --> O[回复「扫码成功,请返回电脑端」]
2.4 模块关系
┌──────────────────────────────────────────────────────────┐
│ OMS 前端 │
│ src/views/login/components/wechat-login.vue │
│ src/api/wechat/index.ts │
└────────────────────────────┬─────────────────────────────┘
│ HTTPS
┌────────────────────────────▼─────────────────────────────┐
│ SimpleAdmin.Api │
│ AuthService(AuthService.WeChatMp.cs + AuthService.cs)│
│ · GET /wechat/mp/login-qrcode 获取二维码 │
│ · GET /rbac/auth/wx-login/status 轮询 key 状态 │
│ · POST /rbac/auth/wx-login 完成登录 │
│ · GET/POST /apiwechat/verification 公众号回调 │
│ WeChatEventHandler 处理 SCAN / subscribe │
│ WeChatLoginSessionStore loginKey → 账号缓存 │
│ WeChatMpApiClient 调微信 qrcode/create │
└────────────┬───────────────────────────────┬─────────────┘
│ │
┌────────▼────────┐ ┌────────▼────────┐
│ IDistributedCache│ │ MySQL │
│ (Redis 推荐) │ │ t_customer │
└─────────────────┘ └─────────────────┘
▲
│ 微信服务器推送
┌──────┴──────┐
│ 微信公众号 │
└─────────────┘
3. 关键概念
3.1 loginKey(扫码会话标识)
| 项 | 说明 |
|---|---|
| 别名 | 接口参数名 loginUuid,语义上即扫码 key |
| 生成 | 前端优先生成 32 位 UUID;也可不传,由后端 Guid.NewGuid().ToString("N") 生成 |
| 用途 | 写入二维码 scene_str;轮询与登录时作为查询 key |
| 有效期 | 与二维码一致,600 秒(10 分钟) |
3.2 二维码 scene
调用微信 cgi-bin/qrcode/create,参数:
{
"expire_seconds": 600,
"action_name": "QR_STR_SCENE",
"action_info": {
"scene": { "scene_str": "{loginKey}" }
}
}
scene_str 直接使用 loginKey 字符串(≤64 字符)。
EventKey 解析(WeChatQrSceneParser)支持:
| 场景 | EventKey 示例 | 解析结果 |
|---|---|---|
| 已关注扫码 | abc123... |
loginKey |
| 未关注扫码后关注 | qrscene_abc123... |
去掉 qrscene_ 前缀 |
| JSON 格式(兼容) | {"type":"login","loginName":"..."} |
取 loginName 或 key 字段 |
3.3 缓存 session
| 项 | 值 |
|---|---|
| Key | oms:wechat:login:{loginKey} |
| TTL | 600 秒 |
| Value(JSON) | 见下表 |
{
"customerId": 42,
"customerCode": "C001",
"userId": 100,
"openId": "oXXXX"
}
扫码成功后写入;wx-login 成功后 删除,防止重复使用。
4. 接口说明
API 前缀:https://api.ideship.com/admin(ASPNETCORE_PATHBASE=/admin)
生产三套服务:
| 服务 | Swagger / PATHBASE |
|---|---|
| Admin | https://api.ideship.com/admin/index.html |
| Itfx | https://api.ideship.com/itfx/index.html |
| Track | https://api.ideship.com/track/index.html |
4.1 获取登录二维码
GET /wechat/mp/login-qrcode?loginUuid={loginKey}
| 参数 | 必填 | 说明 |
|---|---|---|
loginUuid |
否 | 不传则后端自动生成 loginKey |
Response:
{
"loginKey": "a1b2c3d4e5f6...",
"qrCodeUrl": "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=xxx",
"expiresIn": 600
}
实现: AuthService.GetWeChatLoginQrcodeAsync → WeChatMpApiClient.CreateLoginQrCodeUrlAsync
4.2 轮询扫码状态
GET /rbac/auth/wx-login/status?loginUuid={loginKey}
Response(未扫码):
{ "ready": false }
Response(已扫码且已绑定):
{ "ready": true, "account": "C001" }
此接口不抛「请扫码登录」异常,专供前端静默轮询。
实现: AuthService.WxLoginStatusAsync
4.3 完成登录
POST /rbac/auth/wx-login
Content-Type: application/json
{ "loginUuid": "{loginKey}" }
Response(与密码登录一致 LoginResultDto):
{
"accessToken": "eyJ...",
"refreshToken": "eyJ...",
"tokenType": "Bearer",
"expiresIn": 3600,
"userId": "guid"
}
错误 message:
| 场景 | message |
|---|---|
| 缓存无值 | 请扫码登录 |
| 客户不存在 | 未绑定微信,请先注册或绑定账号 |
| 子客户 | 子客户不支持微信扫码登录 |
| 禁用/冻结 | 客户已禁用 / 客户已冻结 |
| 未关联 RBAC | 客户未关联用户 |
核心步骤(AuthService.WxLoginAsync):
- 从缓存读取 session;
- 按
customerId或openId加载客户; - 校验主客户、状态正常;
- 更新
LastLoginIp、LastLoginTime; GetRbacUserGuidByMapIdAsync(customer.UserId)取 RBAC 用户;BuildClaimsAsync(userGuid, OmsLoginType.Customer)签发 JWT;- 删除缓存 session。
4.4 公众号回调(扫码事件入口)
现网配置 URL:
https://api.ideship.com/admin/apiwechat/verification
(旧站曾用 https://oms.nanafa.com/api/apiwechat/verification 反代到 Admin。)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /apiwechat/verification |
验签,返回 echostr |
| HEAD | /apiwechat/verification |
探活,返回 200 |
| POST | /apiwechat/verification |
接收扫码事件 XML |
备用路径(等价):
GET/POST /wechat/mp/{appId}
POST 处理链:
WeChatMpCallbackSupport.HandleMessageAsync验签;WeChatXmlMessageParser解析 XML;WeChatEventHandler.HandleAsync处理SCAN/subscribe;- 返回 XML 文本回复或空响应。
当前限制: 仅支持明文模式;若公众平台开启 AES 加密,需后续扩展。
5. 后端代码结构
src/SimpleAdmin.Api/
├── Auth/
│ ├── AuthService.cs # WxLoginStatusAsync、WxLoginAsync
│ └── AuthService.WeChatMp.cs # 回调验签、login-qrcode
└── WeChat/
├── Dtos/WeChatDtos.cs # WxLoginQrcodeOutput、WxLoginStatusOutput
├── WeChatMpCallbackSupport.cs # 验签、消息分发
└── Services/
├── WeChatMpApiClient.cs # qrcode/create、OAuth
├── WeChatEventHandler.cs # SCAN / subscribe 扫码登录
├── WeChatQrSceneParser.cs # EventKey → loginKey
├── WeChatLoginSessionPayload.cs
├── IWeChatLoginSessionStore.cs
└── WeChatLoginSessionStore.cs
6. 前端实现
6.1 相关文件
| 文件 | 职责 |
|---|---|
src/views/login/components/wechat-login.vue |
二维码展示、轮询、登录跳转 |
src/api/wechat/index.ts |
getLoginQrcode、getWxLoginStatus、wxLogin |
src/api/wechat/types.ts |
类型定义 |
src/store/modules/rbac-user.ts |
loginByWxResult 存 Token 并拉用户信息 |
6.2 轮询参数
| 常量 | 值 |
|---|---|
| 轮询间隔 | 3000 ms |
| 二维码有效期 | 600000 ms(10 分钟) |
6.3 前端逻辑摘要
1. onMounted → loadQrCode()
2. loginKey = crypto.randomUUID().replace(/-/g, '')
3. GET login-qrcode(loginKey) → 展示 qrCodeUrl,使用返回的 loginKey
4. setInterval 3s:
a. GET wx-login/status → ready=false 则继续
b. ready=true → POST wx-login → loginByWxResult → 跳转首页
5. setTimeout 600s → 停止轮询
6. 刷新按钮 → 重新 loadQrCode()
7. 数据与配置
7.1 客户表 t_customer
扫码登录依赖字段:
| 字段 | 说明 |
|---|---|
WechatOpenid |
公众号 openId,扫码时匹配微信用户 |
UserId |
关联 RBAC 用户,用于签发 JWT |
CustomerCode |
写入缓存,status 接口返回给前端 |
ParentCustomerId |
非 NULL 时拒绝扫码登录(仅主客户) |
Status |
Disabled / Frozen 时拒绝登录 |
7.2 公众号配置
配置来源:t_system_other_config(config_code = wechat_mp)或 appsettings
| 配置项 | 说明 |
|---|---|
| AppKey | 公众号 AppId |
| AppSecret | 公众号 Secret |
| Remark | 服务器 Token |
公众平台服务器配置:
| 项 | 值 |
|---|---|
| URL | https://api.ideship.com/admin/apiwechat/verification |
| Token | 与数据库/AppSettings 一致 |
| 消息加解密 | 明文(当前实现) |
7.3 Nginx
nginx-default.conf 关键规则:
# 微信回调
location ^~ /api/apiwechat/ {
rewrite ^/api/apiwechat/(.*)$ /admin/apiwechat/$1 break;
proxy_pass https://api.nanafa.com;
}
8. 测试用例(扫码登录)
| # | 场景 | 预期 |
|---|---|---|
| 1 | 已绑定客户 PC 扫码 | status 返回 ready=true;wx-login 成功;跳转首页 |
| 2 | 未绑定客户扫码 | 手机收到「未绑定账号」;PC status 一直 ready=false |
| 3 | 二维码 10 分钟过期 | 前端停止轮询;点刷新重新获取 |
| 4 | 未关注公众号用户扫码 | subscribe 事件正确处理 qrscene_ 前缀 |
| 5 | 子客户扫码 | wx-login 返回「子客户不支持微信扫码登录」 |
| 6 | 禁用/冻结客户扫码 | wx-login 返回对应状态提示 |
| 7 | 公众号验签 GET | 合法签名返回 echostr |
| 8 | 登录成功后重复 wx-login | 同一 key 已删除,返回「请扫码登录」 |
| 9 | 管理端 /admin |
不显示微信入口 |
9. 注意事项
| 项 | 说明 |
|---|---|
| 多实例部署 | 必须使用 Redis 作为 IDistributedCache;单机 MemoryCache 在负载均衡下扫码与轮询可能打到不同节点导致失败 |
| 缓存与二维码 TTL | 均为 600 秒,保持一致 |
| 密码登录不受影响 | 缓存或微信异常时,客户仍可用账号密码登录 |
| 集成测试 | WeChatScanLoginIntegrationTests 覆盖扫码事件 → 写缓存 → status 返回 ready |
10. 接口速查
| 步骤 | 方法 | 路径 |
|---|---|---|
| 1. 获取二维码 | GET | /wechat/mp/login-qrcode |
| 2. 轮询状态 | GET | /rbac/auth/wx-login/status |
| 3. 完成登录 | POST | /rbac/auth/wx-login |
| 4. 扫码回调 | POST | /apiwechat/verification |
完整 URL 示例:
GET https://api.ideship.com/admin/wechat/mp/login-qrcode?loginUuid=abc123
GET https://api.ideship.com/admin/rbac/auth/wx-login/status?loginUuid=abc123
POST https://api.ideship.com/admin/rbac/auth/wx-login
POST https://api.ideship.com/admin/apiwechat/verification
GET https://api.ideship.com/admin/wechat/oauth/config