zyc 44464dd334 feat: DevPerf Dashboard 研发人效看板 v1.0
- 后端:Bun + Hono + Drizzle ORM + SQLite
- 前端:Vue 3 + Naive UI + ECharts
- 项目管理:创建项目 + 绑定 Git 仓库
- OKR 系统:目标/关键结果 CRUD + 进度追踪
- Git 同步:Gitea API 自动同步 commit/PR + 作者关联
- 数据看板:项目 OKR 进度 + KR 状态分布 + 代码活动
- 权限体系:admin/manager/developer/viewer 四级
- Docker 部署:docker-compose + nginx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:57:14 +08:00

104 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Hono } from 'hono';
import { db } from '../db/index';
import { gitCommits, gitPRs, users } from '../db/schema';
import { eq, and, gte, desc } from 'drizzle-orm';
import { AppError } from '../middleware/error-handler';
import dayjs from 'dayjs';
export const gitRoutes = new Hono();
// GET /api/git/activity
gitRoutes.get('/git/activity', async (c) => {
const user = c.get('user');
const queryUserId = c.req.query('userId');
const weeks = parseInt(c.req.query('weeks') || '12');
if (user.role === 'viewer') {
throw new AppError(40103, 'Insufficient permissions', 403);
}
let targetUserId: string | undefined;
if (user.role === 'developer') {
targetUserId = user.sub;
} else if (queryUserId) {
targetUserId = queryUserId;
}
const startDate = dayjs().subtract(weeks, 'week').startOf('week').toDate();
// 所有 commits
const commitQuery = targetUserId
? db.select().from(gitCommits).where(and(eq(gitCommits.userId, targetUserId), gte(gitCommits.committedAt, startDate)))
: db.select().from(gitCommits).where(gte(gitCommits.committedAt, startDate));
const commits = await commitQuery;
// Heatmap按天
const dayMap: Record<string, { commits: number; additions: number; deletions: number }> = {};
const today = dayjs();
for (let d = dayjs(startDate); d.isBefore(today) || d.isSame(today, 'day'); d = d.add(1, 'day')) {
dayMap[d.format('YYYY-MM-DD')] = { commits: 0, additions: 0, deletions: 0 };
}
for (const commit of commits) {
const day = dayjs(commit.committedAt).format('YYYY-MM-DD');
if (dayMap[day]) {
dayMap[day].commits++;
dayMap[day].additions += commit.additions || 0;
dayMap[day].deletions += commit.deletions || 0;
}
}
const heatmap = Object.entries(dayMap).map(([date, data]) => ({ date, ...data }));
// 统计指标(替代原来的 PR 指标)
const allCommits = await db.select().from(gitCommits);
const thisMonthStart = dayjs().startOf('month').toDate();
const thisMonthCommits = allCommits.filter(c => dayjs(c.committedAt).isAfter(thisMonthStart));
const activeContributors = new Set(allCommits.filter(c => c.userId).map(c => c.userId)).size;
const activeRepos = new Set(allCommits.map(c => c.repoName)).size;
const stats = {
totalCommits: allCommits.length,
activeContributors,
thisMonthCommits: thisMonthCommits.length,
activeRepos,
};
// 每周趋势(按人堆叠)
const allUsers = await db.select().from(users);
const userMap = new Map(allUsers.map(u => [u.id, u.displayName]));
const weeklyTrend = [];
for (let i = 0; i < weeks; i++) {
const weekStart = dayjs().subtract(weeks - 1 - i, 'week').startOf('week');
const weekEnd = weekStart.add(7, 'day');
const weekCommits = commits.filter(c => {
const d = dayjs(c.committedAt);
return d.isAfter(weekStart) && d.isBefore(weekEnd);
});
// 按人分组
const byUser: Record<string, number> = {};
for (const c of weekCommits) {
const uid = c.userId || 'unknown';
byUser[uid] = (byUser[uid] || 0) + 1;
}
weeklyTrend.push({
weekStart: weekStart.format('YYYY-MM-DD'),
total: weekCommits.length,
additions: weekCommits.reduce((sum, c) => sum + (c.additions || 0), 0),
deletions: weekCommits.reduce((sum, c) => sum + (c.deletions || 0), 0),
byUser: Object.entries(byUser).map(([userId, count]) => ({
userId,
name: userMap.get(userId) || '未关联',
commits: count,
})),
});
}
return c.json({
code: 0,
data: { heatmap, stats, weeklyTrend },
message: 'success',
});
});