diff --git a/backend/src/config.ts b/backend/src/config.ts index 19c5918..ccc4d06 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,5 +1,12 @@ import { z } from 'zod'; +// z.coerce.boolean() 把任何非空字符串当 true(包括"false"),用 preprocess 修正 +const envBool = (defaultValue: boolean) => + z.preprocess((v) => { + if (typeof v !== 'string') return v; + return ['true', '1', 'yes', 'on'].includes(v.toLowerCase()); + }, z.boolean()).default(defaultValue); + const envSchema = z.object({ JWT_SECRET: z.string().min(16, 'JWT_SECRET must be at least 16 characters'), PORT: z.coerce.number().default(3200), @@ -25,13 +32,13 @@ const envSchema = z.object({ ADMIN_PASSWORD: z.string().min(6).default('Admin123!'), // AI (豆包 Doubao / 火山引擎 Ark) - AI_ENABLED: z.coerce.boolean().default(false), + AI_ENABLED: envBool(false), AI_API_KEY: z.string().default(''), AI_MODEL: z.string().default('doubao-seed-2-0-pro-260215'), AI_BASE_URL: z.string().default('https://ark.cn-beijing.volces.com/api/v3'), // ROI 外部营收 API - MOCK_REVENUE_API: z.coerce.boolean().default(false), + MOCK_REVENUE_API: envBool(false), REVENUE_API_BASE_URL: z.string().default('http://localhost:3200/mock'), REVENUE_API_KEY: z.string().default('mock-dev-key-12345'), }); diff --git a/backend/src/sync/scheduler.ts b/backend/src/sync/scheduler.ts index 41f0aca..5d6b781 100644 --- a/backend/src/sync/scheduler.ts +++ b/backend/src/sync/scheduler.ts @@ -49,10 +49,16 @@ export function startScheduler(): void { }); // ROI 营收 ingest:每天 03:00 拉昨日营收 - revenueJob = new Cron('0 3 * * *', async () => { - console.info('[SCHEDULER] ROI 营收 ingest 开始...'); - await runRevenueIngest().catch(e => console.error('[SCHEDULER] ROI 营收 ingest 失败:', e)); - }); + // 仅当 mock 启用 或 REVENUE_API_BASE_URL 不指向本地 mock 时才跑(避免对未挂载的端点请求拿 404) + const revenueApiReady = config.MOCK_REVENUE_API || !config.REVENUE_API_BASE_URL.includes('/mock'); + if (revenueApiReady) { + revenueJob = new Cron('0 3 * * *', async () => { + console.info('[SCHEDULER] ROI 营收 ingest 开始...'); + await runRevenueIngest().catch(e => console.error('[SCHEDULER] ROI 营收 ingest 失败:', e)); + }); + } else { + console.info('[SCHEDULER] 跳过营收 ingest cron: MOCK_REVENUE_API=false 且 REVENUE_API_BASE_URL 仍指向 mock'); + } // ROI 资产摊销:每月 1 号 01:00 amortizerJob = new Cron('0 1 1 * *', async () => {