/** * 提成奖金路由 * 管理按月提成奖金/扣款的 CRUD、批量导入、模板下载 */ import { Router, Response, NextFunction } from 'express' import { authMiddleware, AuthRequest } from '../middleware/auth' import multer from 'multer' import * as XLSX from 'xlsx' import { listByMonth, summaryByMonth, create, update, remove, batchImport, } from '../services/commission-bonus.service' import { auditLog } from '../middleware/auditLog' const router = Router() router.use(authMiddleware) const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024 } }) /** * 按月查询提成奖金列表 * GET /commission-bonus?month=YYYY-MM */ router.get('/', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId const month = (req.query.month as string) || new Date().toISOString().slice(0, 7) const [records, summary] = await Promise.all([ listByMonth(orgId, month), summaryByMonth(orgId, month), ]) res.json({ success: true, data: { records, summary, month } }) } catch (err) { next(err) } }) /** * 新增单条提成奖金 * POST /commission-bonus body: { employeeId, month, amount, remark? } */ router.post('/', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId const { employeeId, month, amount, remark } = req.body if (!employeeId || !month || amount === undefined) { return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 employeeId/month/amount' } }) } const record = await create(orgId, req.user!.id, { employeeId, month, amount: Number(amount), remark }) await auditLog(req, 'CREATE', 'COMMISSION_BONUS', record.id, { employeeId, month, amount }) res.json({ success: true, data: record }) } catch (err: any) { if (err.code === 'NOT_FOUND') return res.status(404).json({ success: false, error: { code: err.code, message: err.message } }) next(err) } }) /** * 更新单条 * PUT /commission-bonus/:id body: { amount?, remark? } */ router.put('/:id', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId const { amount, remark } = req.body const record = await update(orgId, req.params.id, { amount: amount !== undefined ? Number(amount) : undefined, remark }) await auditLog(req, 'UPDATE', 'COMMISSION_BONUS', req.params.id, { amount, remark }) res.json({ success: true, data: record }) } catch (err: any) { if (err.code === 'NOT_FOUND') return res.status(404).json({ success: false, error: { code: err.code, message: err.message } }) next(err) } }) /** * 删除单条 * DELETE /commission-bonus/:id */ router.delete('/:id', async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId await remove(orgId, req.params.id) await auditLog(req, 'DELETE', 'COMMISSION_BONUS', req.params.id) res.json({ success: true, data: { message: '已删除' } }) } catch (err: any) { if (err.code === 'NOT_FOUND') return res.status(404).json({ success: false, error: { code: err.code, message: err.message } }) next(err) } }) /** * 批量导入 Excel * POST /commission-bonus/import multipart: file, month * Excel 列:员工姓名 | 证件号码 | 金额 | 备注 */ router.post('/import', upload.single('file'), async (req: AuthRequest, res: Response, next: NextFunction) => { try { const orgId = req.user!.orgId const month = req.body.month || new Date().toISOString().slice(0, 7) if (!req.file) { return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请上传 Excel 文件' } }) } const wb = XLSX.read(req.file.buffer, { type: 'buffer' }) const ws = wb.Sheets[wb.SheetNames[0]] const rows: any[] = XLSX.utils.sheet_to_json(ws, { defval: '' }) const parsed = rows.map((r) => ({ employeeName: String(r['员工姓名'] || r['姓名'] || '').trim() || undefined, idCardNumber: String(r['证件号码'] || r['身份证号'] || '').trim() || undefined, amount: parseFloat(r['金额'] || r['提成奖金'] || '0') || 0, remark: String(r['备注'] || '').trim() || undefined, })).filter((r) => r.employeeName || r.idCardNumber) const result = await batchImport(orgId, req.user!.id, month, parsed) await auditLog(req, 'IMPORT', 'COMMISSION_BONUS', undefined, { month, ...result }) res.json({ success: true, data: result }) } catch (err) { next(err) } }) /** * 下载导入模板 * GET /commission-bonus/template */ router.get('/template', (req: AuthRequest, res: Response) => { const ws = XLSX.utils.aoa_to_sheet([ ['员工姓名', '证件号码', '金额', '备注'], ['张三', '110101199001011234', '5000', '销售提成'], ['李四', '', '-200', '迟到扣款'], ]) const wb = XLSX.utils.book_new() XLSX.utils.book_append_sheet(wb, ws, '提成奖金模板') const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') res.setHeader('Content-Disposition', 'attachment; filename="commission-bonus-template.xlsx"') res.send(buf) }) export default router