devperf/backend/scripts/backfill-launched-at.ts
zyc 5af612e3fd feat(roi): ROI 动态规则引擎 v1 + 业务体系归属
后端:
- 事件流模型(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>
2026-05-22 13:20:22 +08:00

67 lines
2.3 KiB
TypeScript

/**
* 一次性脚本:把所有项目的 launchedAt 改成「该项目最早一次 commit 时间」。
* 没有绑定 repo / repo 里无 commit 的项目,默认 2026-01-01。
*
* 用法:bun run scripts/backfill-launched-at.ts
*/
import { asc, eq, inArray } from 'drizzle-orm';
import { db } from '../src/db/index';
import { projects, projectRepos, gitCommits } from '../src/db/schema';
const DEFAULT_DATE = new Date('2026-01-01T00:00:00+08:00');
/** 抹除 .git 后缀和 URL 前缀,只保留仓库名 */
function normalizeRepoName(raw: string): string {
let cleaned = raw.trim().replace(/\.git$/, '');
if (cleaned.includes('://')) {
try {
const parts = new URL(cleaned).pathname.split('/').filter(Boolean);
return parts[parts.length - 1] || cleaned;
} catch { /* fallthrough */ }
}
if (cleaned.includes('/')) return cleaned.split('/').pop() || cleaned;
return cleaned;
}
const all = await db.select().from(projects);
console.log(`Total projects: ${all.length}`);
let withCommitsCount = 0, fallbackCount = 0;
for (const p of all) {
const repos = await db.select().from(projectRepos).where(eq(projectRepos.projectId, p.id));
const repoNames = repos.map(r => normalizeRepoName(r.repoName));
let launchedAt = DEFAULT_DATE;
let source = 'default-2026-01-01';
if (repoNames.length > 0) {
const earliest = await db.select({ committedAt: gitCommits.committedAt, repoName: gitCommits.repoName, sha: gitCommits.sha })
.from(gitCommits)
.where(inArray(gitCommits.repoName, repoNames))
.orderBy(asc(gitCommits.committedAt))
.limit(1);
if (earliest.length > 0 && earliest[0].committedAt) {
launchedAt = earliest[0].committedAt instanceof Date ? earliest[0].committedAt : new Date(earliest[0].committedAt);
source = `first commit ${earliest[0].repoName}/${earliest[0].sha?.slice(0, 7)}`;
withCommitsCount += 1;
} else {
fallbackCount += 1;
}
} else {
fallbackCount += 1;
}
await db.update(projects).set({
launchedAt,
updatedAt: new Date(),
}).where(eq(projects.id, p.id));
const label = `${p.identifier || p.id} (${p.name})`;
console.log(` ${label.padEnd(50)}${launchedAt.toISOString().slice(0, 10)} [${source}]`);
}
console.log('');
console.log(`Done. with-commits=${withCommitsCount} fallback=${fallbackCount}`);
process.exit(0);