- 后端: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>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
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',
|
||
});
|
||
});
|