a83ed58864
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 23s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m59s
AI Code Review / AI Code Review (pull_request) Failing after 2m5s
CI/CD Pipeline / Validate - Style (push) Successful in 2m21s
CI/CD Pipeline / Integration Tests (push) Successful in 2m19s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m14s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m47s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
CI/CD Pipeline / Validate - Security (push) Successful in 5m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m7s
CI/CD Pipeline / Unit Tests (push) Successful in 8m26s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 12m1s
CI/CD Pipeline / Canary Release to Production (push) Failing after 254h44m2s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 254h44m4s
CI/CD Pipeline / Build Production Web Image (push) Failing after 254h44m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 254h48m59s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 254h50m6s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 254h50m8s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 254h51m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 254h51m20s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 254h51m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 254h51m27s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 254h51m32s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 254h51m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 254h51m49s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 254h52m26s
CI/CD Pipeline / Deploy Production (push) Failing after 254h44m2s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 254h52m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 254h44m4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 254h52m26s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 254h52m27s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 254h52m28s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 254h52m28s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 254h52m29s
CI/CD Pipeline / PR Build Web Image (push) Failing after 254h52m30s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 254h51m51s
CI/CD Pipeline / Frontend Lint (push) Failing after 254h52m31s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 254h51m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 254h52m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 254h52m39s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 254h52m28s
CI/CD Pipeline / CI Gate (push) Failing after 255h18m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 255h24m41s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 255h25m52s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 255h25m58s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 255h26m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 255h26m59s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 255h27m0s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 255h27m1s
CI/CD Pipeline / PR Build API Image (push) Failing after 255h27m5s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
/**
|
||
* 微信扫码登录 WxLogin JS-SDK 动态加载与授权参数解析
|
||
*
|
||
* 微信官网嵌入式二维码方案:页面引入 https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
|
||
* 后挂载全局 window.WxLogin,new WxLogin({...}) 会在指定容器内渲染二维码 iframe。
|
||
* 本模块负责:动态加载该脚本(带超时/失败检测)、从后端返回的 auth_url 中解析
|
||
* WxLogin 所需的 appid / redirect_uri / state。
|
||
*/
|
||
|
||
const WX_LOGIN_SRC = "https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"
|
||
/** 脚本加载超时(毫秒):超时视为加载失败,调用方回退整页跳转 */
|
||
const WX_LOGIN_LOAD_TIMEOUT = 8000
|
||
|
||
/** WxLogin 构造参数(微信官方字段,保持原名) */
|
||
export interface WxLoginOptions {
|
||
/** 是否内嵌二维码(回调在 iframe 内完成) */
|
||
self_redirect: boolean
|
||
/** 二维码容器元素 id */
|
||
id: string
|
||
/** 微信开放平台 AppID */
|
||
appid: string
|
||
/** 应用授权作用域,网站应用固定 snsapi_login */
|
||
scope: "snsapi_login"
|
||
/** 回调地址(需与微信开放平台配置一致,WxLogin 内部会 encodeURIComponent) */
|
||
redirect_uri: string
|
||
/** 防 CSRF 随机串,由后端 state store 生成并在回调时一次性消费 */
|
||
state: string
|
||
/** 二维码样式:black / white */
|
||
style?: "black" | "white"
|
||
/** 自定义样式链接(可选) */
|
||
href?: string
|
||
}
|
||
|
||
/** 微信脚本挂载到 window 上的全局构造函数类型 */
|
||
export interface WxLoginConstructor {
|
||
new (options: WxLoginOptions): unknown
|
||
}
|
||
|
||
declare global {
|
||
interface Window {
|
||
WxLogin?: WxLoginConstructor
|
||
}
|
||
}
|
||
|
||
let loadPromise: Promise<WxLoginConstructor> | null = null
|
||
|
||
/**
|
||
* 动态加载微信 WxLogin JS(单例:并发调用复用同一个 promise)。
|
||
* 加载失败或超时会 reject,调用方应回退到整页跳转授权方式。
|
||
*/
|
||
export function loadWxLoginScript(): Promise<WxLoginConstructor> {
|
||
if (window.WxLogin) return Promise.resolve(window.WxLogin)
|
||
if (loadPromise) return loadPromise
|
||
|
||
loadPromise = new Promise<WxLoginConstructor>((resolve, reject) => {
|
||
const script = document.createElement("script")
|
||
script.src = WX_LOGIN_SRC
|
||
script.async = true
|
||
script.onload = () => {
|
||
if (window.WxLogin) {
|
||
resolve(window.WxLogin)
|
||
} else {
|
||
loadPromise = null
|
||
reject(new Error("微信登录脚本加载完成但 WxLogin 未挂载"))
|
||
}
|
||
}
|
||
script.onerror = () => {
|
||
loadPromise = null
|
||
script.remove()
|
||
reject(new Error("微信登录脚本加载失败"))
|
||
}
|
||
document.head.appendChild(script)
|
||
|
||
// 超时兜底:部分网络环境下脚本既不 onload 也不 onerror
|
||
window.setTimeout(() => {
|
||
if (window.WxLogin) {
|
||
resolve(window.WxLogin)
|
||
return
|
||
}
|
||
loadPromise = null
|
||
script.remove()
|
||
reject(new Error("微信登录脚本加载超时"))
|
||
}, WX_LOGIN_LOAD_TIMEOUT)
|
||
})
|
||
|
||
return loadPromise
|
||
}
|
||
|
||
/** 从微信授权链接 query 中解析出的 WxLogin 所需参数 */
|
||
export interface ParsedWxAuthParams {
|
||
appid: string
|
||
/** 已 URL 解码的回调地址(传给 WxLogin 时由其内部再次编码) */
|
||
redirect_uri: string
|
||
state: string
|
||
}
|
||
|
||
/**
|
||
* 从后端返回的微信授权链接(https://open.weixin.qq.com/connect/qrconnect?appid=...&redirect_uri=...&state=...)
|
||
* 中解析 appid / redirect_uri / state。解析失败时返回 null,由调用方回退整页跳转。
|
||
*/
|
||
export function parseWxAuthUrl(authUrl: string, stateFallback?: string): ParsedWxAuthParams | null {
|
||
try {
|
||
const url = new URL(authUrl)
|
||
const appid = url.searchParams.get("appid")
|
||
const redirectUri = url.searchParams.get("redirect_uri")
|
||
const state = url.searchParams.get("state") || stateFallback || ""
|
||
if (!appid || !redirectUri || !state) return null
|
||
return { appid, redirect_uri: redirectUri, state }
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|