# OMS 微信扫码登录 > 版本:v1.1 > 日期:2026-07-01 > 范围:**PC 端客户门户微信扫码登录**(`/` 登录页 → 微信 Tab) --- ## 1. 概述 OMS 客户门户通过**微信公众号临时二维码 + 扫码事件回调 + 缓存桥接 + 前端轮询**完成登录。 核心思路: 1. PC 端向后端申请带 **loginKey** 的二维码; 2. 用户微信扫码后,公众号向 OMS 推送 `SCAN` / `subscribe` 事件; 3. 后端根据扫码微信的 **openId** 查找已绑定客户,将账号信息写入缓存(key = loginKey,TTL 10 分钟); 4. PC 端轮询该 key,有值则调用登录接口签发 JWT。 **前提:** 微信 openId 须已在 `t_customer.WechatOpenid` 中绑定(通过 H5 注册/绑定页完成,本文不展开)。 **门户限制:** 仅客户门户 `/` 提供微信扫码;管理端 `/admin` 不提供。 --- ## 2. 扫码登录流程 ### 2.1 时序图 ```mermaid 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 前端轮询流程 ```mermaid 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 后端扫码事件流程 ```mermaid 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`,参数: ```json { "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) | 见下表 | ```json { "customerId": 42, "customerCode": "C001", "userId": 100, "openId": "oXXXX" } ``` 扫码成功后写入;`wx-login` 成功后 **删除**,防止重复使用。 --- ## 4. 接口说明 API 前缀:`https://api.nanafa.com/admin`(`ASPNETCORE_PATHBASE=/admin`) ### 4.1 获取登录二维码 ``` GET /wechat/mp/login-qrcode?loginUuid={loginKey} ``` | 参数 | 必填 | 说明 | |------|------|------| | `loginUuid` | 否 | 不传则后端自动生成 loginKey | **Response:** ```json { "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(未扫码):** ```json { "ready": false } ``` **Response(已扫码且已绑定):** ```json { "ready": true, "account": "C001" } ``` > 此接口不抛「请扫码登录」异常,专供前端静默轮询。 **实现:** `AuthService.WxLoginStatusAsync` --- ### 4.3 完成登录 ``` POST /rbac/auth/wx-login Content-Type: application/json { "loginUuid": "{loginKey}" } ``` **Response(与密码登录一致 `LoginResultDto`):** ```json { "accessToken": "eyJ...", "refreshToken": "eyJ...", "tokenType": "Bearer", "expiresIn": 3600, "userId": "guid" } ``` **错误 message:** | 场景 | message | |------|---------| | 缓存无值 | `请扫码登录` | | 客户不存在 | `未绑定微信,请先注册或绑定账号` | | 子客户 | `子客户不支持微信扫码登录` | | 禁用/冻结 | `客户已禁用` / `客户已冻结` | | 未关联 RBAC | `客户未关联用户` | **核心步骤(`AuthService.WxLoginAsync`):** 1. 从缓存读取 session; 2. 按 `customerId` 或 `openId` 加载客户; 3. 校验主客户、状态正常; 4. 更新 `LastLoginIp`、`LastLoginTime`; 5. `GetRbacUserGuidByMapIdAsync(customer.UserId)` 取 RBAC 用户; 6. `BuildClaimsAsync(userGuid, OmsLoginType.Customer)` 签发 JWT; 7. 删除缓存 session。 --- ### 4.4 公众号回调(扫码事件入口) **现网配置 URL:** ``` https://oms.nanafa.com/api/apiwechat/verification ``` Nginx 反代至 `https://api.nanafa.com/admin/apiwechat/verification`。 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/apiwechat/verification` | 验签,返回 `echostr` | | HEAD | `/apiwechat/verification` | 探活,返回 200 | | POST | `/apiwechat/verification` | 接收扫码事件 XML | 备用路径(等价): ``` GET/POST /wechat/mp/{appId} ``` **POST 处理链:** 1. `WeChatMpCallbackSupport.HandleMessageAsync` 验签; 2. `WeChatXmlMessageParser` 解析 XML; 3. `WeChatEventHandler.HandleAsync` 处理 `SCAN` / `subscribe`; 4. 返回 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 前端逻辑摘要 ```text 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://oms.nanafa.com/api/apiwechat/verification` | | Token | 与数据库/AppSettings 一致 | | 消息加解密 | **明文**(当前实现) | ### 7.3 Nginx `nginx-default.conf` 关键规则: ```nginx # 微信回调 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.nanafa.com/admin/wechat/mp/login-qrcode?loginUuid=abc123 GET https://api.nanafa.com/admin/rbac/auth/wx-login/status?loginUuid=abc123 POST https://api.nanafa.com/admin/rbac/auth/wx-login POST https://oms.nanafa.com/api/apiwechat/verification ```