feat(order): 订单超时自动流转改为后台定时任务

- 新增 startOrderTimeoutScheduler:默认每 60s 扫描(ORDER_TIMEOUT_SCAN_MS 可配,DISABLE_ORDER_TIMEOUT_SCAN=1 可禁用),带防重入与 unref

- 启动时自动开启;移除订单查询接口里的被动扫描,避免每次请求触发

- 文档补充定时扫描环境变量;新增回归护栏 order-timeout-scheduler.spec.ts(83 套件/622 用例全绿)
这个提交包含在:
xcbyy
2026-10-19 09:22:52 +08:00
父节点 99d1c018e6
当前提交 d665cd6976
共有 4 个文件被更改,包括 72 次插入 和 2 次删除

查看文件

@@ -1297,7 +1297,7 @@ export class MysqlRepository {
// 订单相关
async getOrders(): Promise<any[]> {
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<any[]> {
const uid = Number(userId) || 0;
await this.processOrderTimeouts().catch(() => {});
// 订单超时自动流转统一由后台定时任务处理(见 index.ts startOrderTimeoutScheduler)
const [rows]: any = await pool.query(`
SELECT
id,

查看文件

@@ -4307,6 +4307,40 @@ async function logStartupConfigSelfCheck(): Promise<void> {
}
}
// 订单超时自动流转定时任务:定期扫描并处理「超时未支付取消 / 超时未接单退款 / 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}`);

查看文件

@@ -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');
});
});