fix(roi): 关闭 mock 营收 + 修 zod envBool 把 "false" 当 true 的 bug
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m6s

变更:
- scheduler 加判断: MOCK_REVENUE_API=false 且 URL 仍指 /mock 时跳过 revenue cron,
  避免对未挂载的端点 404 写错误日志
- config.ts 新增 envBool() preprocess: 替代 z.coerce.boolean(),
  正确把 "false"/"0"/"no" 识别为 false (zod 默认所有非空串都是 true)
- 影响:AI_ENABLED 和 MOCK_REVENUE_API 两个 boolean env 现在按字面值生效

副作用:
- 数据库 1997 条 mock revenue + 5 条 unmapped 已清空 (mysql DELETE 手动执行)
- 项目级 ROI 现在显示真实状态: 成本来自 commit 估算, 产出 ¥0 (待业务方接入)
- 真实营收 API 就绪后只需改 REVENUE_API_BASE_URL 即可恢复 cron

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zyc 2026-05-22 15:41:00 +08:00
parent 4a2ed8d414
commit 7b5a2a823c
2 changed files with 19 additions and 6 deletions

View File

@ -1,5 +1,12 @@
import { z } from 'zod'; 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({ const envSchema = z.object({
JWT_SECRET: z.string().min(16, 'JWT_SECRET must be at least 16 characters'), JWT_SECRET: z.string().min(16, 'JWT_SECRET must be at least 16 characters'),
PORT: z.coerce.number().default(3200), PORT: z.coerce.number().default(3200),
@ -25,13 +32,13 @@ const envSchema = z.object({
ADMIN_PASSWORD: z.string().min(6).default('Admin123!'), ADMIN_PASSWORD: z.string().min(6).default('Admin123!'),
// AI (豆包 Doubao / 火山引擎 Ark) // AI (豆包 Doubao / 火山引擎 Ark)
AI_ENABLED: z.coerce.boolean().default(false), AI_ENABLED: envBool(false),
AI_API_KEY: z.string().default(''), AI_API_KEY: z.string().default(''),
AI_MODEL: z.string().default('doubao-seed-2-0-pro-260215'), 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'), AI_BASE_URL: z.string().default('https://ark.cn-beijing.volces.com/api/v3'),
// ROI 外部营收 API // 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_BASE_URL: z.string().default('http://localhost:3200/mock'),
REVENUE_API_KEY: z.string().default('mock-dev-key-12345'), REVENUE_API_KEY: z.string().default('mock-dev-key-12345'),
}); });

View File

@ -49,10 +49,16 @@ export function startScheduler(): void {
}); });
// ROI 营收 ingest:每天 03:00 拉昨日营收 // ROI 营收 ingest:每天 03:00 拉昨日营收
// 仅当 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 () => { revenueJob = new Cron('0 3 * * *', async () => {
console.info('[SCHEDULER] ROI 营收 ingest 开始...'); console.info('[SCHEDULER] ROI 营收 ingest 开始...');
await runRevenueIngest().catch(e => console.error('[SCHEDULER] ROI 营收 ingest 失败:', e)); 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 // ROI 资产摊销:每月 1 号 01:00
amortizerJob = new Cron('0 1 1 * *', async () => { amortizerJob = new Cron('0 1 1 * *', async () => {