/** * 客服工作台路由 * 提供工单管理、客户会话、租户数据穿透 */ import { Router } from 'express' import { authMiddleware, AuthRequest } from '../middleware/auth' import prisma from '../lib/prisma' import { z } from 'zod' const router = Router() // 客服权限中间件 const supportMiddleware = (req: AuthRequest, res: any, next: any) => { if (req.user!.role !== 'SUPPORT' && req.user!.role !== 'SUPER_ADMIN') { return res.status(403).json({ success: false, error: { code: 'FORBIDDEN', message: '仅客服或超级管理员可访问' } }) } next() } const createTicketSchema = z.object({ title: z.string().min(1, '标题必填'), content: z.string().min(1, '内容必填'), category: z.string().optional(), priority: z.enum(['LOW', 'NORMAL', 'HIGH', 'URGENT']).default('NORMAL'), }) const createMessageSchema = z.object({ content: z.string().min(1, '内容必填'), }) // ==================== 工单管理 ==================== /** 获取工单列表(客服看全部,企业用户看自己租户的) */ router.get('/tickets', authMiddleware, async (req: AuthRequest, res, next) => { try { const isSupport = req.user!.role === 'SUPPORT' || req.user!.role === 'SUPER_ADMIN' const where = isSupport ? {} : { orgId: req.user!.orgId! } const tickets = await prisma.ticket.findMany({ where, orderBy: { createdAt: 'desc' }, include: { org: { select: { id: true, name: true } }, _count: { select: { messages: true } }, }, }) res.json({ success: true, data: tickets }) } catch (err) { next(err) } }) /** 获取工单详情 */ router.get('/tickets/:id', authMiddleware, async (req: AuthRequest, res, next) => { try { const { id } = req.params const ticket = await prisma.ticket.findUnique({ where: { id }, include: { org: { select: { id: true, name: true } }, messages: { orderBy: { createdAt: 'asc' } }, }, }) if (!ticket) throw { code: 'NOT_FOUND', message: '工单不存在' } res.json({ success: true, data: ticket }) } catch (err) { next(err) } }) /** 创建工单(企业用户提交) */ router.post('/tickets', authMiddleware, async (req: AuthRequest, res, next) => { try { const data = createTicketSchema.parse(req.body) const ticket = await prisma.ticket.create({ data: { ...data, orgId: req.user!.orgId!, createdBy: req.user!.id, }, }) res.json({ success: true, data: ticket }) } catch (err) { next(err) } }) /** 回复工单 */ router.post('/tickets/:id/messages', authMiddleware, async (req: AuthRequest, res, next) => { try { const { id } = req.params const data = createMessageSchema.parse(req.body) const isSupport = req.user!.role === 'SUPPORT' || req.user!.role === 'SUPER_ADMIN' const message = await prisma.ticketMessage.create({ data: { ticketId: id, content: data.content, senderId: req.user!.id, senderRole: isSupport ? 'SUPPORT' : 'USER', }, }) // 客服回复时更新工单状态为处理中 if (isSupport) { await prisma.ticket.update({ where: { id }, data: { status: 'IN_PROGRESS', assigneeId: req.user!.id } }) } res.json({ success: true, data: message }) } catch (err) { next(err) } }) /** 关闭工单 */ router.post('/tickets/:id/close', authMiddleware, supportMiddleware, async (req: AuthRequest, res, next) => { try { const { id } = req.params await prisma.ticket.update({ where: { id }, data: { status: 'CLOSED' } }) res.json({ success: true }) } catch (err) { next(err) } }) /** 转派工单 */ router.post('/tickets/:id/assign', authMiddleware, supportMiddleware, async (req: AuthRequest, res, next) => { try { const { id } = req.params const { assigneeId } = req.body await prisma.ticket.update({ where: { id }, data: { assigneeId } }) res.json({ success: true }) } catch (err) { next(err) } }) // ==================== 客户会话 ==================== /** 获取会话列表 */ router.get('/chat/sessions', authMiddleware, async (req: AuthRequest, res, next) => { try { const isSupport = req.user!.role === 'SUPPORT' || req.user!.role === 'SUPER_ADMIN' const where = isSupport ? { supportUserId: req.user!.id } : { orgId: req.user!.orgId! } const sessions = await prisma.chatSession.findMany({ where, orderBy: { lastMessageAt: 'desc' }, include: { org: { select: { id: true, name: true } } }, }) res.json({ success: true, data: sessions }) } catch (err) { next(err) } }) /** 获取会话消息 */ router.get('/chat/sessions/:id/messages', authMiddleware, async (req: AuthRequest, res, next) => { try { const { id } = req.params const messages = await prisma.chatMessage.findMany({ where: { sessionId: id }, orderBy: { createdAt: 'asc' }, }) res.json({ success: true, data: messages }) } catch (err) { next(err) } }) /** 发送会话消息 */ router.post('/chat/sessions/:id/messages', authMiddleware, async (req: AuthRequest, res, next) => { try { const { id } = req.params const { content } = req.body as { content: string } if (!content) throw { code: 'VALIDATION_ERROR', message: '内容必填' } const isSupport = req.user!.role === 'SUPPORT' || req.user!.role === 'SUPER_ADMIN' const session = await prisma.chatSession.findUnique({ where: { id } }) if (!session) throw { code: 'NOT_FOUND', message: '会话不存在' } const message = await prisma.chatMessage.create({ data: { sessionId: id, content, senderId: req.user!.id, senderRole: isSupport ? 'SUPPORT' : 'USER', }, }) await prisma.chatSession.update({ where: { id }, data: { lastMessage: content, lastMessageAt: new Date(), unreadBySupport: isSupport ? session.unreadBySupport : session.unreadBySupport + 1, unreadByUser: isSupport ? session.unreadByUser + 1 : session.unreadByUser, }, }) res.json({ success: true, data: message }) } catch (err) { next(err) } }) // ==================== 租户数据穿透 ==================== /** 获取租户列表(客服用) */ router.get('/tenants', authMiddleware, supportMiddleware, async (req: AuthRequest, res, next) => { try { const tenants = await prisma.organization.findMany({ select: { id: true, name: true, plan: true, maxEmployees: true, contactName: true, contactPhone: true, createdAt: true, _count: { select: { employees: true, users: true } }, }, orderBy: { createdAt: 'desc' }, }) res.json({ success: true, data: tenants }) } catch (err) { next(err) } }) /** 获取租户数据概览(客服代查看) */ router.get('/tenants/:orgId/overview', authMiddleware, supportMiddleware, async (req: AuthRequest, res, next) => { try { const { orgId } = req.params const [employeeCount, activeContracts, pendingApprovals, openTickets] = await Promise.all([ prisma.employee.count({ where: { orgId, status: 'ACTIVE' } }), prisma.laborContract.count({ where: { orgId } }), prisma.approvalInstance.count({ where: { orgId, status: 'PENDING' } }), prisma.ticket.count({ where: { orgId, status: { in: ['OPEN', 'IN_PROGRESS'] } } }), ]) res.json({ success: true, data: { employeeCount, activeContracts, pendingApprovals, openTickets }, }) } catch (err) { next(err) } }) export default router