From d665cd69769933a2d4ee522e548f06319b1196ed Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Mon, 19 Oct 2026 09:22:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(order):=20=E8=AE=A2=E5=8D=95=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E8=87=AA=E5=8A=A8=E6=B5=81=E8=BD=AC=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 startOrderTimeoutScheduler:默认每 60s 扫描(ORDER_TIMEOUT_SCAN_MS 可配,DISABLE_ORDER_TIMEOUT_SCAN=1 可禁用),带防重入与 unref - 启动时自动开启;移除订单查询接口里的被动扫描,避免每次请求触发 - 文档补充定时扫描环境变量;新增回归护栏 order-timeout-scheduler.spec.ts(83 套件/622 用例全绿) --- docs/部署验收与1分钱支付闭环.md | 4 +++ server/src/db/repository.ts | 4 +-- server/src/index.ts | 35 ++++++++++++++++++++ server/tests/order-timeout-scheduler.spec.ts | 31 +++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 server/tests/order-timeout-scheduler.spec.ts diff --git a/docs/部署验收与1分钱支付闭环.md b/docs/部署验收与1分钱支付闭环.md index 04f9dac..4772613 100644 --- a/docs/部署验收与1分钱支付闭环.md +++ b/docs/部署验收与1分钱支付闭环.md @@ -21,6 +21,10 @@ MYSQL_USER=... MYSQL_PASSWORD=... MYSQL_DATABASE=peiwan +# 可选:订单超时自动流转扫描(默认 60000ms;DISABLE_ORDER_TIMEOUT_SCAN=1 可禁用) +# ORDER_TIMEOUT_SCAN_MS=60000 +# DISABLE_ORDER_TIMEOUT_SCAN=0 + # 可选:微信(若已在后台"微信支付配置"页填写,则无需在这里重复;后台配置优先) # WX_APP_ID=... # WX_APP_SECRET=... diff --git a/server/src/db/repository.ts b/server/src/db/repository.ts index 658da31..79adb53 100644 --- a/server/src/db/repository.ts +++ b/server/src/db/repository.ts @@ -1297,7 +1297,7 @@ export class MysqlRepository { // 订单相关 async getOrders(): Promise { - await this.processOrderTimeouts().catch(() => {}); + // 订单超时自动流转统一由后台定时任务处理(见 index.ts startOrderTimeoutScheduler) const [rows]: any = await pool.query(` SELECT id, @@ -1335,7 +1335,7 @@ export class MysqlRepository { */ async getOrdersForUser(userId: number): Promise { const uid = Number(userId) || 0; - await this.processOrderTimeouts().catch(() => {}); + // 订单超时自动流转统一由后台定时任务处理(见 index.ts startOrderTimeoutScheduler) const [rows]: any = await pool.query(` SELECT id, diff --git a/server/src/index.ts b/server/src/index.ts index 15e9f04..f3e060f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -4307,6 +4307,40 @@ async function logStartupConfigSelfCheck(): Promise { } } +// 订单超时自动流转定时任务:定期扫描并处理「超时未支付取消 / 超时未接单退款 / 48小时自动完成结算」 +const ORDER_TIMEOUT_SCAN_MS = Number(process.env.ORDER_TIMEOUT_SCAN_MS || 60000); +let orderTimeoutTimer: NodeJS.Timeout | null = null; +let orderTimeoutRunning = false; + +function startOrderTimeoutScheduler(): void { + if (process.env.DISABLE_ORDER_TIMEOUT_SCAN === '1') { + console.log('[Peiwan Scheduler] 订单超时扫描已禁用 (DISABLE_ORDER_TIMEOUT_SCAN=1)'); + return; + } + if (orderTimeoutTimer) return; + const intervalMs = Number.isFinite(ORDER_TIMEOUT_SCAN_MS) ? Math.max(1000, ORDER_TIMEOUT_SCAN_MS) : 60000; + orderTimeoutTimer = setInterval(async () => { + if (orderTimeoutRunning) return; // 防重入:上一轮未结束则跳过本轮 + orderTimeoutRunning = true; + try { + const r = await repo.processOrderTimeouts(); + if (r && (r.cancelledCount || r.refundedCount || r.completedCount)) { + console.log( + `[Peiwan Scheduler] 订单超时扫描: 取消 ${r.cancelledCount} / 退款 ${r.refundedCount} / 完成结算 ${r.completedCount}` + ); + } + } catch (e) { + logServerError('orderTimeoutScheduler', e); + } finally { + orderTimeoutRunning = false; + } + }, intervalMs); + if (typeof (orderTimeoutTimer as any).unref === 'function') { + (orderTimeoutTimer as any).unref(); + } + console.log(`[Peiwan Scheduler] 订单超时扫描已启动,间隔 ${Math.round(intervalMs / 1000)}s`); +} + async function bootstrap() { try { await initDatabase(); @@ -4323,6 +4357,7 @@ async function bootstrap() { } await logStartupConfigSelfCheck(); + startOrderTimeoutScheduler(); app.listen(port, () => { console.log(`[Peiwan Server] HTTP server is running on http://localhost:${port}`); diff --git a/server/tests/order-timeout-scheduler.spec.ts b/server/tests/order-timeout-scheduler.spec.ts new file mode 100644 index 0000000..908bd82 --- /dev/null +++ b/server/tests/order-timeout-scheduler.spec.ts @@ -0,0 +1,31 @@ +// 已读取AGENTS.md +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * 第六十七批回归闸门:订单超时自动流转改为后台定时任务 + */ +describe('第六十七批订单超时定时任务闸门', () => { + const rootDir = path.resolve(__dirname, '../../'); + const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8'); + const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8'); + + it('提供可配置的订单超时扫描定时任务', () => { + expect(indexCode).toContain('function startOrderTimeoutScheduler('); + expect(indexCode).toContain('ORDER_TIMEOUT_SCAN_MS'); + expect(indexCode).toContain('DISABLE_ORDER_TIMEOUT_SCAN'); + expect(indexCode).toContain('setInterval('); + expect(indexCode).toContain('orderTimeoutRunning'); + }); + + it('启动时启用定时任务', () => { + const bootstrapStart = indexCode.indexOf('async function bootstrap('); + expect(bootstrapStart).toBeGreaterThan(-1); + const block = indexCode.slice(bootstrapStart, bootstrapStart + 1200); + expect(block).toContain('startOrderTimeoutScheduler();'); + }); + + it('订单查询不再按请求被动触发超时扫描(改由定时任务统一处理)', () => { + expect(repoCode).not.toContain('await this.processOrderTimeouts().catch'); + }); +});