后端: - 事件流模型(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>
67 lines
2.3 KiB
TypeScript
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);
|