diff --git a/plugins/channels/weixin_ilink/login_admin.go b/plugins/channels/weixin_ilink/login_admin.go index 98f284c..47a39be 100644 --- a/plugins/channels/weixin_ilink/login_admin.go +++ b/plugins/channels/weixin_ilink/login_admin.go @@ -133,13 +133,18 @@ func (a *Adapter) StartLogin(ctx context.Context) (LoginQR, error) { a.log.Info("weixin_ilink: login in progress, reusing qrcode", "phase", sess.currentPhase()) return LoginQR{Qrcode: sess.qrcode, Image: sess.image}, nil } - // A literal double-click within the reuse window: serve the same code. - if time.Since(sess.startedAt) < qrReuseWindow { + // Serve the same code while it is still fresh OR already scanned — always + // reuse, never swap, so an in-progress bind (which WeChat may be confirming + // right now) is never dropped. A frontend flooding login-qr calls therefore + // cannot cancel the session under the user's scan. Only a genuinely stale + // (>60s server TTL) and never-scanned code is re-minted. + if time.Since(sess.startedAt) < 60*time.Second { + a.log.Info("weixin_ilink: login in progress, reusing qrcode", "phase", sess.currentPhase()) return LoginQR{Qrcode: sess.qrcode, Image: sess.image}, nil } - // Not scanned yet and past the reuse window: mint a fresh code, cancelling the - // old (unscanned) poll. failLogin guards a cancelled goroutine from clobbering - // the new session's state. + // Code stale (>60s) and never scanned: mint a fresh one, cancelling the old + // (expired, unscanned) poll. failLogin guards a cancelled goroutine from + // clobbering the new session's state. a.log.Info("weixin_ilink: previous qrcode not scanned, minting a fresh one") if sess.cancel != nil { sess.cancel() diff --git a/plugins/channels/weixin_ilink/login_reuse_test.go b/plugins/channels/weixin_ilink/login_reuse_test.go index 65f7c29..a77ccb0 100644 --- a/plugins/channels/weixin_ilink/login_reuse_test.go +++ b/plugins/channels/weixin_ilink/login_reuse_test.go @@ -75,7 +75,7 @@ func TestStartLogin_ScanedPastWindowReusesQR(t *testing.T) { // could keep the QR. Then a refresh must still reuse it. ad.loginMu.Lock() if ad.loginSession != nil { - ad.loginSession.startedAt = time.Now().Add(-(qrReuseWindow + time.Second)) + ad.loginSession.startedAt = time.Now().Add(-(60*time.Second + time.Second)) } ad.loginMu.Unlock() @@ -128,7 +128,7 @@ func TestStartLogin_WaitingPastWindowReMints(t *testing.T) { ad.loginMu.Lock() if ad.loginSession != nil { - ad.loginSession.startedAt = time.Now().Add(-(qrReuseWindow + time.Second)) + ad.loginSession.startedAt = time.Now().Add(-(60*time.Second + time.Second)) } ad.loginMu.Unlock()