增加定时任务调度入口和 Admin 触发路由
- 新增 scheduler/index.ts 统一调度周榜结算和订阅过期检查 - 支持 CLI 入口:bun run src/services/scheduler/index.ts weekly-settlement --dry-run - 支持 Admin 手动触发:GET/POST /v1/admin/jobs - 所有任务支持 dry-run 模式预览不写库
This commit is contained in:
parent
1d9c67b30c
commit
c24be16b6a
@ -123,7 +123,7 @@
|
||||
| G6-2 | 更新 `docs/implementation-plan.md` | [x] | 将本计划作为 Phase 1d 或 Game Economy 阶段索引进去 |
|
||||
| G6-3 | 增加 Admin 配置或只读查看能力 | [x] | 管理端至少能查看用户金币、道具、奖励流水、广告恢复记录 |
|
||||
| G6-4 | 增加 E2E 或集成测试 | [x] | 覆盖游客登录、完成挑战组、广告恢复、购买道具、周榜查询 |
|
||||
| G6-5 | 增加定时任务入口 | [ ] | 周榜结算和订阅/资源周期任务有可部署入口,支持手动 dry-run |
|
||||
| G6-5 | 增加定时任务入口 | [x] | 周榜结算和订阅/资源周期任务有可部署入口,支持手动 dry-run |
|
||||
| G6-6 | 完成最终验证 | [ ] | `bun run typecheck`、`bun run test`、`bun run lint` 通过或记录明确环境阻塞 |
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
@ -9,6 +9,7 @@ import { adminUsersRoutes } from './users.js';
|
||||
import { adminStatsRoutes } from './stats.js';
|
||||
import { adminFeedbackRoutes } from './feedback.js';
|
||||
import { adminGamificationRoutes } from './gamification.js';
|
||||
import { adminJobsRoutes } from './jobs.js';
|
||||
|
||||
export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.register(adminAuthRoutes);
|
||||
@ -21,4 +22,5 @@ export async function adminRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.register(adminStatsRoutes, { prefix: '/stats' });
|
||||
app.register(adminFeedbackRoutes, { prefix: '/feedback' });
|
||||
app.register(adminGamificationRoutes, { prefix: '/gamification' });
|
||||
app.register(adminJobsRoutes, { prefix: '/jobs' });
|
||||
}
|
||||
|
||||
18
src/routes/admin/jobs.ts
Normal file
18
src/routes/admin/jobs.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { listJobs, runJob } from '../../services/scheduler/index.js';
|
||||
import type { JobName } from '../../services/scheduler/index.js';
|
||||
|
||||
export async function adminJobsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// 列出所有可用定时任务
|
||||
app.get('/', async () => {
|
||||
const jobs = listJobs();
|
||||
return { success: true, data: jobs, error: null };
|
||||
});
|
||||
|
||||
// 手动触发定时任务
|
||||
app.post<{ Body: { job: JobName; dryRun?: boolean } }>('/trigger', async (request) => {
|
||||
const { job, dryRun = false } = request.body;
|
||||
const result = await runJob(job, dryRun);
|
||||
return { success: true, data: result, error: null };
|
||||
});
|
||||
}
|
||||
86
src/services/scheduler/index.ts
Normal file
86
src/services/scheduler/index.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* 定时任务调度入口
|
||||
*
|
||||
* 提供周榜结算、订阅过期检查等定时任务的可部署入口。
|
||||
* 支持 dry-run 模式用于手动验证。
|
||||
*
|
||||
* 部署方式:
|
||||
* - cron 调用:`bun run src/services/scheduler/index.ts weekly-settlement`
|
||||
* - Admin 手动触发:`POST /v1/admin/jobs/trigger`
|
||||
*/
|
||||
import { weeklySettlement } from '../gamification/leaderboard-service.js';
|
||||
import { checkAndExpireSubscriptions } from '../payment/subscription-service.js';
|
||||
|
||||
export type JobName = 'weekly-settlement' | 'expire-subscriptions';
|
||||
|
||||
export interface JobResult {
|
||||
job: JobName;
|
||||
dryRun: boolean;
|
||||
executedAt: string;
|
||||
result: unknown;
|
||||
}
|
||||
|
||||
const JOBS: Record<JobName, (dryRun: boolean) => Promise<unknown>> = {
|
||||
'weekly-settlement': async (dryRun) => weeklySettlement(dryRun),
|
||||
'expire-subscriptions': async (dryRun) => {
|
||||
if (dryRun) {
|
||||
return { message: 'dry-run: will check and expire subscriptions', note: '无实际变更' };
|
||||
}
|
||||
const count = await checkAndExpireSubscriptions();
|
||||
return { expiredCount: count };
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行定时任务。
|
||||
* @param jobName 任务名称
|
||||
* @param dryRun 为 true 时只返回预览,不写库
|
||||
*/
|
||||
export async function runJob(jobName: JobName, dryRun = false): Promise<JobResult> {
|
||||
const handler = JOBS[jobName];
|
||||
if (!handler) {
|
||||
throw new Error(`Unknown job: ${jobName}. Available: ${Object.keys(JOBS).join(', ')}`);
|
||||
}
|
||||
|
||||
const result = await handler(dryRun);
|
||||
return {
|
||||
job: jobName,
|
||||
dryRun,
|
||||
executedAt: new Date().toISOString(),
|
||||
result,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有可用定时任务及其描述。
|
||||
*/
|
||||
export function listJobs(): Array<{ name: JobName; description: string; schedule: string }> {
|
||||
return [
|
||||
{
|
||||
name: 'weekly-settlement',
|
||||
description: '周榜结算:按组快照上周排名,给每组前 3 名发金币奖励',
|
||||
schedule: '每周一 UTC 00:30',
|
||||
},
|
||||
{
|
||||
name: 'expire-subscriptions',
|
||||
description: '订阅过期检查:检查并过期到期的订阅',
|
||||
schedule: '每日 UTC 01:00',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// CLI 入口:`bun run src/services/scheduler/index.ts weekly-settlement [--dry-run]`
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length > 0 && args[0] !== '--dry-run') {
|
||||
const jobName = args[0] as JobName;
|
||||
const dryRun = args.includes('--dry-run');
|
||||
runJob(jobName, dryRun)
|
||||
.then((result) => {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`Job failed: ${err instanceof Error ? err.message : err}`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user