后端: - 事件流模型(project_cost_events / project_revenue_events)+ launchedAt 截断 - 3 大业务体系归属(airhubs/airflow/aircore) + 项目类型(hw/sw) + identifier 自动生成 - AI 三件套推荐(category + bizSystem + projectType) - 营收 mock API + 外部对接规范 + 资产摊销 cron - 5 个 migration(0003 ROI 引擎 / 0004 driver factors / 0005 biz system) - 单测 11/11 过 前端: - 项目级 ROI 看板:4 KPI 卡片 + 折线图(周/月/年)+ 成本/产出事件流并排 - 全公司决策罗盘:3 大 ROI 指标 + 业务线堆叠 + 分类筛选 chip - 项目列表 + 侧边栏:按产品线分组(可折叠 + localStorage 持久化) - Admin: ROI 策略配置 + 项目映射 + 未映射收容 数据: - 23 项目全部 AI 自动分类 + 自动 identifier(airhubs-hw-001 这种) - launchedAt 按各项目首次 commit 时间设置 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
52 lines
1.6 KiB
TypeScript
52 lines
1.6 KiB
TypeScript
import { Hono } from 'hono';
|
|
import { config } from '../config';
|
|
import { generateMockRevenueForDate, listMockBusinessProjects } from '../services/roi/revenue-ingest/mock-generator';
|
|
|
|
/**
|
|
* Mock 营收 API,严格按"附录 A:外部营收 API 接入规范"实现。
|
|
* 仅在 MOCK_REVENUE_API=true 时挂载,挂在 /mock(不在 /api/* 下,避开 JWT auth)。
|
|
*
|
|
* 路由:
|
|
* GET /mock/revenue/daily?date=YYYY-MM-DD
|
|
* GET /mock/revenue/projects
|
|
*/
|
|
export const mockRevenueRoutes = new Hono();
|
|
|
|
mockRevenueRoutes.use('*', async (c, next) => {
|
|
// 鉴权:严格按附录 A 的 Bearer Token
|
|
const auth = c.req.header('Authorization') || '';
|
|
const match = auth.match(/^Bearer\s+(.+)$/i);
|
|
if (!match || match[1] !== config.REVENUE_API_KEY) {
|
|
return c.json({ error: 'UNAUTHORIZED' }, 401);
|
|
}
|
|
await next();
|
|
});
|
|
|
|
mockRevenueRoutes.get('/revenue/daily', async (c) => {
|
|
const date = c.req.query('date');
|
|
if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
return c.json({ error: 'INVALID_DATE', message: 'date must be YYYY-MM-DD' }, 400);
|
|
}
|
|
|
|
try {
|
|
const events = await generateMockRevenueForDate(date);
|
|
return c.json({
|
|
date,
|
|
events,
|
|
nextCursor: null, // mock 不分页(数据量小)
|
|
totalCount: events.length,
|
|
});
|
|
} catch (e) {
|
|
return c.json({ error: 'INTERNAL', message: (e as Error).message }, 500);
|
|
}
|
|
});
|
|
|
|
mockRevenueRoutes.get('/revenue/projects', async (c) => {
|
|
try {
|
|
const projects = await listMockBusinessProjects();
|
|
return c.json({ projects, totalCount: projects.length });
|
|
} catch (e) {
|
|
return c.json({ error: 'INTERNAL', message: (e as Error).message }, 500);
|
|
}
|
|
});
|