Files
performance-evaluation-system/backend/src/routes/performance.ts
2026-04-11 11:51:54 +08:00

802 lines
33 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 { 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;