- 新增财务菜单与应收应付、充值记录、账户流水页面及路由 - 组织管理改为左右分栏,优化左侧树层级展示与选中交互 - 角色权限分配改为列表形式,支持各级菜单独立勾选 - 我的订单工具栏按钮样式与独立 loading 交互优化 Co-authored-by: Cursor <cursoragent@cursor.com>
475 lines
15 KiB
Markdown
475 lines
15 KiB
Markdown
# 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
|
||
```
|