diff --git a/frontend/web/src/admin/modules/channels/WeixinLogin.tsx b/frontend/web/src/admin/modules/channels/WeixinLogin.tsx index be98805..a294416 100644 --- a/frontend/web/src/admin/modules/channels/WeixinLogin.tsx +++ b/frontend/web/src/admin/modules/channels/WeixinLogin.tsx @@ -120,7 +120,7 @@ export function WeixinLogin({ .finally(() => setBusy(null)); }; - const showQR = status === "waiting" || qrCode !== ""; + const showQR = status === "waiting" || qrUrl !== ""; return ( @@ -147,8 +147,8 @@ export function WeixinLogin({ {showQR ? (
- {qrCode ? ( - + {qrUrl ? ( + ) : ( 二维码暂不可用,请点击「刷新二维码」重试。 )} diff --git a/plugins/channels/weixin_ilink/login_admin.go b/plugins/channels/weixin_ilink/login_admin.go index 265439a..8650833 100644 --- a/plugins/channels/weixin_ilink/login_admin.go +++ b/plugins/channels/weixin_ilink/login_admin.go @@ -51,10 +51,11 @@ type loginSession struct { done chan error } -// qrRefreshTTL bounds how long a WeChat bot QR is usable before StartLogin re-mints -// a fresh one. iLink bot QRs are short-lived; an idempotent StartLogin must not keep -// serving a long-expired QR (the user would scan it and get "二维码过期"). -const qrRefreshTTL = 90 * time.Second +// qrReuseWindow is how long a just-minted QR is reused on an immediate re-click +// before StartLogin re-mints a fresh one. WeChat bot QRs are short-lived, so a +// "扫码登录/刷新二维码" click must yield a brand-new code — reusing an old QR is +// exactly what makes WeChat report "二维码过期". +const qrReuseWindow = 5 * time.Second // StartLogin begins a scan-login and returns the QR code to display. It is // idempotent: when a login is already in progress it returns the current QR @@ -70,9 +71,10 @@ func (a *Adapter) StartLogin(ctx context.Context) (LoginQR, error) { return LoginQR{}, ErrAlreadyLoggedIn } if sess := a.loginSession; sess != nil { - // Idempotent for a fresh QR, but re-mint once the current QR has aged past - // qrRefreshTTL so the panel's 刷新二维码 always yields a scannable code. - if time.Since(sess.startedAt) < qrRefreshTTL { + // Reuse only within a tiny window (a literal double-click); otherwise re-mint + // a brand-new QR so the panel's "扫码登录/刷新二维码" never serves a stale, + // expired WeChat code. The old poll goroutine is cancelled. + if time.Since(sess.startedAt) < qrReuseWindow { return LoginQR{Qrcode: sess.qrcode, Image: sess.image}, nil } if sess.cancel != nil {