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; // 如果提供了 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; 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( '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( '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( '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( '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(); 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( '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( '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; try { let filter: Parameters[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( '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 { const { evaluatePerformance } = await import('../services/AIEvaluationService'); await evaluatePerformance(perfId); } export default router;