Commit c03a4904 by luoqi

fix(ai): stop stale QR requests from appearing as generating

parent 156d9ab6
Pipeline #3871 failed in 0 seconds
......@@ -572,10 +572,8 @@ export class QrLoginService {
* ⚠️ **更正(2026-08-27 实测)**:这里原来写「一轮扫码只有 2 分钟」——**是错的**。
* 实测一轮普通登录码 10:29:18 下发、10:37:18 才 state=3 过期,活了 **8 分钟**。
* 120 秒是**新设备验证码**(qrCodeType=1)的固定有效期,被张冠李戴了。
* ⚠️ 于是下面那个 3 分钟兜底窗**偏短**:audit 查不到、而二维码是 5 分钟前发的时,
* 会把本轮的 [11028] 也滤掉,页面停在「正在生成二维码」。
* ⛔ 但**不能**简单调大 —— 调大就把上面那个串轮问题放回来了。
* ⇒ 正解是别走到兜底:audit 行是 start() 必写的,走到这里说明那一轮不是我们开的。
* 审计窗口内查不到本轮时,明确提示核实并重新授权。旧席位可能仍保存 deviceId,
* ⛔ 不能退回滚动时间窗口并显示「正在生成」,也不能自动再次申请设备。
*/
await this.seats.assertRound(user.tenantId, user.id, deviceId);
let floor = since ?? null;
......@@ -596,14 +594,15 @@ export class QrLoginService {
WHERE occurred_at > now() - interval '30 minutes'
AND action = 'auth.qr.start' AND object_id = ${deviceId}
ORDER BY occurred_at DESC LIMIT 1`;
// 席位可以继续保留旧 deviceId,但不能把过旧/缺失的轮次当成正在生成。
// ⛔ 不自动重新下单:旧设备是否结束必须先核实,避免重复占用供应商设备。
if (!started[0]) {
return { phase: 'failed', hint: '本次绑号请求已无法继续,请联系管理员核实旧请求并重新授权。' };
}
// ⚠️ 减 5 秒:下单和写 audit 之间有间隔,而 [11028] 可能在写完之前就到了。
// 卡得太死会把本轮的二维码事件也滤掉。
floor = started[0]
? new Date(started[0].occurred_at.getTime() - 5_000)
: new Date(Date.now() - 3 * 60_000);
round = started[0]
? { reuse_vid: started[0].reuse_vid, started_by: started[0].started_by }
: null;
floor = new Date(started[0].occurred_at.getTime() - 5_000);
round = { reuse_vid: started[0].reuse_vid, started_by: started[0].started_by };
}
// ⚠️ 带 received_at 谓词:webhook_deliveries 按它分区,不带就要扫每个分区
const events = await this.prisma.$queryRaw<
......
......@@ -4,6 +4,25 @@ import { randomUUID } from 'node:crypto';
describe('扫码环境归属', () => {
const user = { id: 'user', tenantId: 'tenant' } as never;
it('复用旧设备但查不到有效扫码轮次时明确结束,不无限显示生成中或再次下单', async () => {
const prisma = { $queryRaw: jest.fn().mockResolvedValue([]) };
const ecom = { platformCallRaw: jest.fn() };
const service = new QrLoginService(prisma as never, ecom as never, { assertRound: jest.fn() } as never);
const result = await service.status(user, 'old-device');
expect(result.phase).toBe('failed');
expect(result.hint).toContain('重新授权');
expect(prisma.$queryRaw).toHaveBeenCalledTimes(1);
expect(ecom.platformCallRaw).not.toHaveBeenCalled();
});
it('新轮次回调已到达时返回二维码;尚未到达时仍等待', async () => {
const started = { occurred_at: new Date(), reuse_vid: null, started_by: 'user' };
const prisma = { $queryRaw: jest.fn().mockResolvedValueOnce([started]).mockResolvedValueOnce([
{ event_type: '11028', payload: { data: { qrCodeUrl: 'https://example.test/qr' } } },
]).mockResolvedValueOnce([started]).mockResolvedValueOnce([]) };
const service = new QrLoginService(prisma as never, {} as never, { assertRound: jest.fn() } as never);
expect(await service.status(user, 'new-device')).toMatchObject({ phase: 'waiting', qrCodeUrl: 'https://example.test/qr' });
expect((await service.status(user, 'new-device')).phase).toBe('pending');
});
it.each(['15002', '11026'])('冲突 %s 显示失败,不绑定或启动同步', async (event_type) => {
const prisma = { $queryRaw: jest.fn().mockResolvedValue([{ event_type, payload: { __ingressLoginConflict: true } }]) };
const service = new QrLoginService(prisma as never, {} as never, { assertRound: jest.fn() } as never);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment