front/docs/微信登录整合方案.md
zzh c0c37a4eac
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m39s
chore: 拆分生产 ideship / 测试 nanafa 两套环境配置
生产用 .env.production 与 pnpm build;测试用 .env.nanafa 与 pnpm build:test;本地 pnpm dev / dev:ideship 可切换。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 12:38:11 +08:00

15 KiB
Raw Blame History

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 时序图

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):

  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://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 处理链:

  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 前端逻辑摘要

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