first commit

This commit is contained in:
2026-04-11 11:51:54 +08:00
commit b12a84e388
99 changed files with 19620 additions and 0 deletions

View File

@@ -0,0 +1,801 @@
import { Router, Request, Response } from 'express';
import { authenticate } from '../middlewares/authenticate';
import { authorize } from '../middlewares/authorize';
import * as PerformanceDAO from '../dao/PerformanceDAO';
import { calculateAttendanceScore, calculateLevelAndReward } from '../services/CalculationService';
import { exportPerformanceExcel } from '../services/ExportService';
const router = Router();
// All performance routes require authentication
router.use(authenticate);
// ─── 6.1 POST /api/performance/submit ────────────────────────────────────────
// Supports draft (暂存) and submit (提交) states.
// Writes performance_month, perf_item, attendance in a transaction.
// On submit, triggers async AI evaluation (non-blocking).
router.post(
'/submit',
authorize('employee'),
async (req: Request, res: Response) => {
const user = req.user!;
const { month, status, selfScore, workSummary, items, performanceItems, attendance } = req.body;
if (!month || !status) {
return res.status(400).json({ code: 400, message: '月份和状态不能为空' });
}
if (status !== 'draft' && status !== 'submitted') {
return res.status(400).json({ code: 400, message: '状态值无效,必须为 draft 或 submitted' });
}
// If submitting, check for existing submitted/completed record (idempotency guard)
if (status === 'submitted') {
const existing = await PerformanceDAO.findByUserAndMonth(user.userId, month);
if (existing && (existing.status === 'submitted' || existing.status === 'completed' || existing.status === 'under_review')) {
return res.status(400).json({ code: 400, message: '该月份绩效已提交,不可重复提交' });
}
}
// Calculate attendance score if attendance data provided
let attendanceData: PerformanceDAO.AttendanceRow | undefined;
if (attendance) {
const score = calculateAttendanceScore({
leaveDays: attendance.leave ?? attendance.leave_days ?? 0,
lateTimes: attendance.late ?? attendance.late_times ?? 0,
lackCardTimes: attendance.lackCard ?? attendance.lack_card_times ?? 0,
});
attendanceData = {
perf_id: 0, // will be set by DAO
leave_days: attendance.leave ?? attendance.leave_days ?? 0,
late_times: attendance.late ?? attendance.late_times ?? 0,
absent_days: attendance.absent ?? attendance.absent_days ?? 0,
lack_card_times: attendance.lackCard ?? attendance.lack_card_times ?? 0,
attendance_score: score,
remark: attendance.remark ?? null,
};
}
// Convert performanceItems to items format if needed
const itemsData = performanceItems || items;
const formattedItems = itemsData?.map((item: any) => ({
item_name: item.itemName || item.item_name,
item_category: item.itemCategory || item.item_category || 'business',
weight: item.weight,
user_content: item.userContent || item.user_content,
self_score: item.selfScore || item.self_score,
evidence_url: item.evidence || item.evidence_url,
}));
// 计算自评总分:各项 self_score * weight 加权平均
let calculatedSelfScore: number | undefined;
if (formattedItems && formattedItems.length > 0) {
let weightedSum = 0;
let totalWeight = 0;
for (const item of formattedItems) {
if (item.self_score != null && item.weight != null) {
weightedSum += Number(item.self_score) * Number(item.weight);
totalWeight += Number(item.weight);
}
}
if (totalWeight > 0) {
calculatedSelfScore = parseFloat((weightedSum / totalWeight).toFixed(2));
}
}
try {
const perfId = await PerformanceDAO.upsert({
userId: user.userId,
month,
status,
selfScore: calculatedSelfScore,
workSummary,
items: formattedItems,
attendance: attendanceData,
});
// Trigger async AI evaluation on submit (non-blocking)
if (status === 'submitted') {
triggerAIEvaluation(perfId).catch((err) => {
console.error(`[AI] Evaluation failed for perfId=${perfId}:`, err);
});
}
return res.json({ code: 200, message: status === 'draft' ? '暂存成功' : '提交成功', data: { perfId } });
} catch (err: any) {
// Duplicate key = already submitted
if (err.code === 'ER_DUP_ENTRY') {
return res.status(400).json({ code: 400, message: '该月份绩效已存在' });
}
console.error('[submit]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── 6.2 GET /api/performance/employee/get ───────────────────────────────────
// Returns the authenticated employee's performance records.
// Supports month filter and pagination. Includes AI result, attendance, review opinion.
router.get(
'/employee/get',
authorize('employee'),
async (req: Request, res: Response) => {
const user = req.user!;
const { month, page = '1', pageSize = '10', perfId } = req.query as Record<string, string>;
// 如果提供了 perfId返回单条记录详情
if (perfId) {
try {
const { findDetailByPerfId } = await import('../dao/PerformanceDAO');
const { findByPerfId: findAIByPerfId } = await import('../dao/AIResultDAO');
const detail = await findDetailByPerfId(parseInt(perfId, 10));
if (!detail || detail.performance.user_id !== user.userId) {
return res.status(404).json({ code: 404, message: '绩效记录不存在' });
}
const aiResult = await findAIByPerfId(parseInt(perfId, 10));
const rec = detail.performance;
// 转换为驼峰格式,确保数值字段为 number 类型
const result = {
perfId: rec.perf_id,
userId: rec.user_id,
month: rec.month,
status: rec.status,
selfScore: rec.self_score != null ? Number(rec.self_score) : undefined,
aiScore: rec.ai_score != null ? Number(rec.ai_score) : undefined,
managerScore: rec.manager_score != null ? Number(rec.manager_score) : undefined,
totalScore: rec.total_score != null ? Number(rec.total_score) : undefined,
level: rec.level,
rewardPunish: rec.reward_punish,
workSummary: rec.work_summary,
submitTime: rec.submit_time,
reviewTime: rec.review_time,
reviewOpinion: rec.review_opinion,
performanceItems: detail.items?.map(item => ({
itemId: item.item_id,
perfId: item.perf_id,
itemName: item.item_name,
itemCategory: item.item_category,
weight: Number(item.weight),
userContent: item.user_content,
selfScore: item.self_score != null ? Number(item.self_score) : undefined,
aiScore: item.ai_score != null ? Number(item.ai_score) : undefined,
aiExplanation: item.ai_explanation,
managerScore: item.manager_score != null ? Number(item.manager_score) : undefined,
managerExplanation: item.manager_explanation,
evidenceUrl: item.evidence_url,
})) ?? [],
attendance: detail.attendance ? {
leave: Number(detail.attendance.leave_days),
late: Number(detail.attendance.late_times),
absent: Number(detail.attendance.absent_days),
lackCard: Number(detail.attendance.lack_card_times),
attendanceScore: detail.attendance.attendance_score != null ? Number(detail.attendance.attendance_score) : undefined,
remark: detail.attendance.remark,
} : null,
aiResult: aiResult ? {
aiId: aiResult.aiId,
perfId: aiResult.perfId,
aiScoreDetail: aiResult.aiScoreDetail,
aiTotalScore: aiResult.aiTotalScore,
aiProblems: aiResult.aiProblems,
aiSuggestions: aiResult.aiSuggestions,
createTime: aiResult.createTime,
} : null,
};
return res.json({
code: 200,
message: '查询成功',
data: result,
});
} catch (err) {
console.error('[employee/get detail]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
// 否则返回列表
const pageInfo: PerformanceDAO.PageInfo = {
page: Math.max(1, parseInt(page, 10) || 1),
pageSize: Math.min(100, Math.max(1, parseInt(pageSize, 10) || 10)),
};
try {
const result = await PerformanceDAO.findByUserId(user.userId, month, pageInfo);
// Enrich each record with attendance and AI result
const { findDetailByPerfId } = await import('../dao/PerformanceDAO');
const { findByPerfId: findAIByPerfId } = await import('../dao/AIResultDAO');
const enriched = await Promise.all(
result.records.map(async (rec) => {
const detail = await findDetailByPerfId(rec.perf_id);
const aiResult = await findAIByPerfId(rec.perf_id);
// 转换为驼峰格式,确保数值字段为 number 类型
return {
perfId: rec.perf_id,
userId: rec.user_id,
month: rec.month,
status: rec.status,
selfScore: rec.self_score != null ? Number(rec.self_score) : undefined,
aiScore: rec.ai_score != null ? Number(rec.ai_score) : undefined,
managerScore: rec.manager_score != null ? Number(rec.manager_score) : undefined,
totalScore: rec.total_score != null ? Number(rec.total_score) : undefined,
level: rec.level,
rewardPunish: rec.reward_punish,
workSummary: rec.work_summary,
submitTime: rec.submit_time,
reviewTime: rec.review_time,
reviewOpinion: rec.review_opinion,
performanceItems: detail?.items?.map(item => ({
itemId: item.item_id,
perfId: item.perf_id,
itemName: item.item_name,
itemCategory: item.item_category,
weight: Number(item.weight),
userContent: item.user_content,
selfScore: item.self_score != null ? Number(item.self_score) : undefined,
aiScore: item.ai_score != null ? Number(item.ai_score) : undefined,
aiExplanation: item.ai_explanation,
managerScore: item.manager_score != null ? Number(item.manager_score) : undefined,
managerExplanation: item.manager_explanation,
evidenceUrl: item.evidence_url,
})) ?? [],
attendance: detail?.attendance ? {
leave: Number(detail.attendance.leave_days),
late: Number(detail.attendance.late_times),
absent: Number(detail.attendance.absent_days),
lackCard: Number(detail.attendance.lack_card_times),
attendanceScore: detail.attendance.attendance_score != null ? Number(detail.attendance.attendance_score) : undefined,
remark: detail.attendance.remark,
} : null,
aiResult: aiResult ? {
aiId: aiResult.aiId,
perfId: aiResult.perfId,
aiScoreDetail: aiResult.aiScoreDetail,
aiTotalScore: aiResult.aiTotalScore,
aiProblems: aiResult.aiProblems,
aiSuggestions: aiResult.aiSuggestions,
createTime: aiResult.createTime,
} : null,
};
})
);
return res.json({
code: 200,
message: '查询成功',
data: { total: result.total, records: enriched },
});
} catch (err) {
console.error('[employee/get]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── 6.3 POST /api/performance/request-modification ──────────────────────────
// Employee requests modification of a submitted performance record.
// Records the reason and notifies the manager (logged for now).
router.post(
'/request-modification',
authorize('employee'),
async (req: Request, res: Response) => {
const user = req.user!;
const { perfId, reason } = req.body;
if (!perfId || !reason) {
return res.status(400).json({ code: 400, message: '绩效ID和申请原因不能为空' });
}
try {
const detail = await PerformanceDAO.findDetailByPerfId(Number(perfId));
if (!detail) {
return res.status(404).json({ code: 404, message: '绩效记录不存在' });
}
// Verify ownership
if (detail.performance.user_id !== user.userId) {
return res.status(403).json({ code: 403, message: '权限不足' });
}
// Only submitted records can request modification
if (detail.performance.status !== 'submitted' && detail.performance.status !== 'under_review') {
return res.status(400).json({ code: 400, message: '当前状态不允许申请修改' });
}
// Record the modification request in operation_log
const pool = (await import('../config/database')).default;
await pool.query(
`INSERT INTO operation_log (user_id, operation_type, target_type, target_id, operation_detail)
VALUES (?, 'request_modification', 'performance', ?, ?)`,
[user.userId, perfId, JSON.stringify({ reason })]
);
// Notify manager: in a real system this would send a notification;
// here we log it as a manager-targeted operation log entry.
console.log(`[修改申请] 员工 ${user.name}(${user.userId}) 申请修改绩效 ${perfId},原因:${reason}`);
return res.json({ code: 200, message: '修改申请已提交,等待管理层审批' });
} catch (err) {
console.error('[request-modification]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── 8.1 GET /api/performance/manager/list ───────────────────────────────────
// Returns subordinates' performance list with optional filters and pagination.
// Supports filtering by month, department, employee name, and status.
// Requirements: 4.1, 7.1, 7.2
router.get(
'/manager/list',
authorize('manager', 'generalManager'),
async (req: Request, res: Response) => {
const user = req.user!;
const {
month,
department,
employeeName,
status,
page = '1',
pageSize = '10',
} = req.query as Record<string, string>;
const pageInfo: PerformanceDAO.PageInfo = {
page: Math.max(1, parseInt(page, 10) || 1),
pageSize: Math.min(100, Math.max(1, parseInt(pageSize, 10) || 10)),
};
const filters: PerformanceDAO.PerformanceFilter = {
month: month || undefined,
department: department || undefined,
employeeName: employeeName || undefined,
status: (status as PerformanceDAO.PerformanceStatus) || undefined,
};
try {
const result = await PerformanceDAO.findByManagerId(user.userId, filters, pageInfo);
// Enrich each record with employee info
const pool = (await import('../config/database')).default;
const enriched = await Promise.all(
result.records.map(async (rec) => {
const [userRows] = await pool.query<any[]>(
'SELECT name, department, position FROM user WHERE user_id = ? LIMIT 1',
[rec.user_id]
);
return {
perfId: rec.perf_id,
userId: rec.user_id,
month: rec.month,
status: rec.status,
selfScore: rec.self_score != null ? Number(rec.self_score) : null,
aiScore: rec.ai_score != null ? Number(rec.ai_score) : null,
managerScore: rec.manager_score != null ? Number(rec.manager_score) : null,
totalScore: rec.total_score != null ? Number(rec.total_score) : null,
level: rec.level,
submitTime: rec.submit_time,
reviewTime: rec.review_time,
// 员工信息
userName: userRows[0]?.name ?? null,
userDepartment: userRows[0]?.department ?? null,
userPosition: userRows[0]?.position ?? null,
};
})
);
return res.json({
code: 200,
message: '查询成功',
data: { total: result.total, records: enriched },
});
} catch (err) {
console.error('[manager/list]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── GET /api/performance/manager/detail/:perfId ─────────────────────────────
// Returns full detail of a subordinate's performance record for manager review.
router.get(
'/manager/detail/:perfId',
authorize('manager', 'generalManager'),
async (req: Request, res: Response) => {
const user = req.user!;
const perfId = parseInt(req.params.perfId, 10);
try {
const detail = await PerformanceDAO.findDetailByPerfId(perfId);
if (!detail) {
return res.status(404).json({ code: 404, message: '绩效记录不存在' });
}
// Verify subordinate relationship (generalManager can view all)
if (user.role !== 'generalManager') {
const pool = (await import('../config/database')).default;
const [empRows] = await pool.query<any[]>(
'SELECT manager_id, name, department, position FROM user WHERE user_id = ? LIMIT 1',
[detail.performance.user_id]
);
if (!empRows[0] || empRows[0].manager_id !== user.userId) {
return res.status(403).json({ code: 403, message: '权限不足' });
}
}
const pool = (await import('../config/database')).default;
const [empRows] = await pool.query<any[]>(
'SELECT name, department, position FROM user WHERE user_id = ? LIMIT 1',
[detail.performance.user_id]
);
const { findByPerfId: findAIByPerfId } = await import('../dao/AIResultDAO');
const aiResult = await findAIByPerfId(perfId);
const rec = detail.performance;
const result = {
perfId: rec.perf_id,
userId: rec.user_id,
month: rec.month,
status: rec.status,
selfScore: rec.self_score != null ? Number(rec.self_score) : null,
aiScore: rec.ai_score != null ? Number(rec.ai_score) : null,
managerScore: rec.manager_score != null ? Number(rec.manager_score) : null,
totalScore: rec.total_score != null ? Number(rec.total_score) : null,
level: rec.level,
rewardPunish: rec.reward_punish,
workSummary: rec.work_summary,
submitTime: rec.submit_time,
reviewTime: rec.review_time,
reviewOpinion: rec.review_opinion,
// 员工信息
userName: empRows[0]?.name ?? null,
userDepartment: empRows[0]?.department ?? null,
userPosition: empRows[0]?.position ?? null,
performanceItems: detail.items?.map(item => ({
itemId: item.item_id,
itemName: item.item_name,
itemCategory: item.item_category,
weight: Number(item.weight),
userContent: item.user_content,
selfScore: item.self_score != null ? Number(item.self_score) : null,
aiScore: item.ai_score != null ? Number(item.ai_score) : null,
aiExplanation: item.ai_explanation,
managerScore: item.manager_score != null ? Number(item.manager_score) : null,
managerExplanation: item.manager_explanation,
evidenceUrl: item.evidence_url,
})) ?? [],
attendance: detail.attendance ? {
leave: Number(detail.attendance.leave_days),
late: Number(detail.attendance.late_times),
absent: Number(detail.attendance.absent_days),
lackCard: Number(detail.attendance.lack_card_times),
attendanceScore: detail.attendance.attendance_score != null ? Number(detail.attendance.attendance_score) : null,
remark: detail.attendance.remark,
} : null,
aiResult: aiResult ? {
aiTotalScore: aiResult.aiTotalScore,
aiProblems: aiResult.aiProblems,
aiSuggestions: aiResult.aiSuggestions,
} : null,
};
return res.json({ code: 200, message: '查询成功', data: result });
} catch (err) {
console.error('[manager/detail]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── 8.2 POST /api/performance/manager/review ────────────────────────────────
// Manager reviews a performance record: updates item scores, calculates final
// total score, level, reward/punishment, and archives the record (status → completed).
// Requirements: 4.3, 4.4, 4.5, 4.7
router.post(
'/manager/review',
authorize('manager', 'generalManager'),
async (req: Request, res: Response) => {
const user = req.user!;
const { perfId, reviewOpinion, itemScores } = req.body;
if (!perfId || !reviewOpinion || !Array.isArray(itemScores)) {
return res.status(400).json({ code: 400, message: '绩效ID、审核意见和考核项评分不能为空' });
}
try {
const detail = await PerformanceDAO.findDetailByPerfId(Number(perfId));
if (!detail) {
return res.status(404).json({ code: 404, message: '绩效记录不存在' });
}
// Verify the performance belongs to a subordinate of this manager
const pool = (await import('../config/database')).default;
const [empRows] = await pool.query<any[]>(
'SELECT manager_id FROM user WHERE user_id = ? LIMIT 1',
[detail.performance.user_id]
);
if (!empRows[0] || empRows[0].manager_id !== user.userId) {
return res.status(403).json({ code: 403, message: '权限不足,该员工不是您的下属' });
}
// Only submitted/under_review records can be reviewed
if (
detail.performance.status !== 'submitted' &&
detail.performance.status !== 'under_review'
) {
return res.status(400).json({ code: 400, message: '当前状态不允许审核' });
}
// Build a map of item scores provided by manager
const scoreMap = new Map<string, { managerScore: number; managerExplanation: string }>();
for (const s of itemScores) {
if (typeof s.itemName !== 'string' || typeof s.managerScore !== 'number') {
return res.status(400).json({ code: 400, message: '考核项评分格式无效' });
}
scoreMap.set(s.itemName, {
managerScore: s.managerScore,
managerExplanation: s.managerExplanation ?? '',
});
}
// Calculate manager total score: weighted average of item manager scores
let weightedSum = 0;
let totalWeight = 0;
for (const item of detail.items) {
const scored = scoreMap.get(item.item_name);
const score = scored ? scored.managerScore : (item.ai_score ?? item.self_score ?? 0);
weightedSum += score * item.weight;
totalWeight += item.weight;
}
const managerScore = totalWeight > 0 ? weightedSum / totalWeight : 0;
// Get attendance score
const attendanceScore = detail.attendance?.attendance_score ?? 0;
// Final total score = manager score (already weighted across items) + attendance contribution
// The items already include attendance-category items; use managerScore directly as total
const totalScore = Math.min(100, Math.max(0, parseFloat(managerScore.toFixed(2))));
const { level, rewardPunish } = calculateLevelAndReward(totalScore);
const reviewData: PerformanceDAO.ReviewData = {
managerScore: parseFloat(managerScore.toFixed(2)),
reviewOpinion,
totalScore,
level,
rewardPunish,
itemScores: detail.items.map((item) => {
const scored = scoreMap.get(item.item_name);
return {
itemName: item.item_name,
managerScore: scored ? scored.managerScore : (item.ai_score ?? item.self_score ?? 0),
managerExplanation: scored ? scored.managerExplanation : '',
};
}),
};
await PerformanceDAO.updateReview(Number(perfId), reviewData);
// Log the review action
await pool.query(
`INSERT INTO operation_log (user_id, operation_type, target_type, target_id, operation_detail)
VALUES (?, 'review_performance', 'performance', ?, ?)`,
[user.userId, perfId, JSON.stringify({ totalScore, level, rewardPunish })]
);
return res.json({
code: 200,
message: '审核完成',
data: { totalScore, level, rewardPunish },
});
} catch (err) {
console.error('[manager/review]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── 8.3 POST /api/performance/manager/reject ────────────────────────────────
// Manager rejects a performance record, recording the reason and setting status to rejected.
// Requirements: 4.6
router.post(
'/manager/reject',
authorize('manager', 'generalManager'),
async (req: Request, res: Response) => {
const user = req.user!;
const { perfId, reason } = req.body;
if (!perfId || !reason) {
return res.status(400).json({ code: 400, message: '绩效ID和驳回原因不能为空' });
}
try {
const detail = await PerformanceDAO.findDetailByPerfId(Number(perfId));
if (!detail) {
return res.status(404).json({ code: 404, message: '绩效记录不存在' });
}
// Verify subordinate relationship
const pool = (await import('../config/database')).default;
const [empRows] = await pool.query<any[]>(
'SELECT manager_id FROM user WHERE user_id = ? LIMIT 1',
[detail.performance.user_id]
);
if (!empRows[0] || empRows[0].manager_id !== user.userId) {
return res.status(403).json({ code: 403, message: '权限不足,该员工不是您的下属' });
}
if (
detail.performance.status !== 'submitted' &&
detail.performance.status !== 'under_review'
) {
return res.status(400).json({ code: 400, message: '当前状态不允许驳回' });
}
// Update status to rejected and record the reason in review_opinion
await pool.query(
`UPDATE performance_month
SET status = 'rejected', review_opinion = ?, review_time = NOW()
WHERE perf_id = ?`,
[reason, perfId]
);
// Log the rejection
await pool.query(
`INSERT INTO operation_log (user_id, operation_type, target_type, target_id, operation_detail)
VALUES (?, 'reject_performance', 'performance', ?, ?)`,
[user.userId, perfId, JSON.stringify({ reason })]
);
console.log(`[驳回] 管理层 ${user.name}(${user.userId}) 驳回绩效 ${perfId},原因:${reason}`);
return res.json({ code: 200, message: '驳回成功,已通知员工' });
} catch (err) {
console.error('[manager/reject]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── 8.4 POST /api/performance/manager/approve-modification ──────────────────
// Manager approves or rejects an employee's modification request.
// Approve: unlocks the performance record (status → draft).
// Reject: notifies employee (logged).
// Requirements: 12.3, 12.4
router.post(
'/manager/approve-modification',
authorize('manager', 'generalManager'),
async (req: Request, res: Response) => {
const user = req.user!;
const { perfId, approved, rejectReason } = req.body;
if (!perfId || typeof approved !== 'boolean') {
return res.status(400).json({ code: 400, message: '绩效ID和审批结果不能为空' });
}
if (!approved && !rejectReason) {
return res.status(400).json({ code: 400, message: '拒绝时必须填写拒绝原因' });
}
try {
const detail = await PerformanceDAO.findDetailByPerfId(Number(perfId));
if (!detail) {
return res.status(404).json({ code: 404, message: '绩效记录不存在' });
}
// Verify subordinate relationship
const pool = (await import('../config/database')).default;
const [empRows] = await pool.query<any[]>(
'SELECT manager_id, name FROM user WHERE user_id = ? LIMIT 1',
[detail.performance.user_id]
);
if (!empRows[0] || empRows[0].manager_id !== user.userId) {
return res.status(403).json({ code: 403, message: '权限不足,该员工不是您的下属' });
}
if (approved) {
// Unlock: reset status to draft so employee can re-edit
await pool.query(
`UPDATE performance_month SET status = 'draft' WHERE perf_id = ?`,
[perfId]
);
await pool.query(
`INSERT INTO operation_log (user_id, operation_type, target_type, target_id, operation_detail)
VALUES (?, 'approve_modification', 'performance', ?, ?)`,
[user.userId, perfId, JSON.stringify({ approved: true })]
);
console.log(`[修改审批] 管理层 ${user.name}(${user.userId}) 同意员工 ${empRows[0].name} 修改绩效 ${perfId}`);
return res.json({ code: 200, message: '已同意修改申请,绩效表单已解锁' });
} else {
// Reject: log the rejection reason as notification to employee
await pool.query(
`INSERT INTO operation_log (user_id, operation_type, target_type, target_id, operation_detail)
VALUES (?, 'reject_modification', 'performance', ?, ?)`,
[user.userId, perfId, JSON.stringify({ approved: false, rejectReason })]
);
console.log(`[修改审批] 管理层 ${user.name}(${user.userId}) 拒绝员工 ${empRows[0].name} 修改绩效 ${perfId},原因:${rejectReason}`);
return res.json({ code: 200, message: '已拒绝修改申请,已通知员工' });
}
} catch (err) {
console.error('[manager/approve-modification]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── 10.3 GET /api/performance/export ────────────────────────────────────────
// Exports performance records as an Excel file.
// Query params:
// - userId: export a single employee's history (employee self or manager/GM)
// - month: restrict to a specific month
// - scope: 'team' (manager's subordinates) | 'company' (GM, all employees)
// Requirements: 7.3, 7.6, 8.4
router.get(
'/export',
authorize('employee', 'manager', 'generalManager'),
async (req: Request, res: Response) => {
const user = req.user!;
const { userId, month, scope } = req.query as Record<string, string>;
try {
let filter: Parameters<typeof exportPerformanceExcel>[0] = {};
if (user.role === 'employee') {
// Employees can only export their own data
filter.userId = user.userId;
if (month) filter.month = month;
} else if (user.role === 'manager') {
if (userId) {
// Export a specific subordinate's history — verify relationship
const pool = (await import('../config/database')).default;
const [empRows] = await pool.query<any[]>(
'SELECT manager_id FROM user WHERE user_id = ? LIMIT 1',
[Number(userId)]
);
if (!empRows[0] || empRows[0].manager_id !== user.userId) {
return res.status(403).json({ code: 403, message: '权限不足,该员工不是您的下属' });
}
filter.userId = Number(userId);
} else {
// Export entire team
filter.managerId = user.userId;
}
if (month) filter.month = month;
} else if (user.role === 'generalManager') {
if (userId) {
filter.userId = Number(userId);
} else if (scope === 'company') {
filter.allEmployees = true;
} else {
filter.allEmployees = true;
}
if (month) filter.month = month;
}
const buffer = await exportPerformanceExcel(filter);
const filename = encodeURIComponent(`绩效数据_${month ?? '全部'}.xlsx`);
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${filename}`);
return res.send(buffer);
} catch (err) {
console.error('[performance/export]', err);
return res.status(500).json({ code: 500, message: '服务器内部错误' });
}
}
);
// ─── Internal helper ──────────────────────────────────────────────────────────
async function triggerAIEvaluation(perfId: number): Promise<void> {
const { evaluatePerformance } = await import('../services/AIEvaluationService');
await evaluatePerformance(perfId);
}
export default router;