From c11e9fbc64bc7032e6604f3a17d82a39bbd27831 Mon Sep 17 00:00:00 2001 From: gitadmin Date: Fri, 10 Jul 2026 13:22:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=82=AE=E4=BB=B6=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 scan-emails/import API 端点,支持手动导入跳过的邮件为工单 - 前端添加'导入跳过的邮件为工单'按钮,扫描完成后可选择导入 - 添加扫描详情日志,记录无内容邮件的跳过原因 - 修复 scan_history 表 details_json 为空的问题 --- src/app/(app)/settings/monitor/page.tsx | 65 ++++++++++ .../api/monitor/scan-emails/import/route.ts | 121 ++++++++++++++++++ src/app/api/monitor/scan-emails/route.ts | 5 +- 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 src/app/api/monitor/scan-emails/import/route.ts diff --git a/src/app/(app)/settings/monitor/page.tsx b/src/app/(app)/settings/monitor/page.tsx index 53e66c7..c1c1251 100644 --- a/src/app/(app)/settings/monitor/page.tsx +++ b/src/app/(app)/settings/monitor/page.tsx @@ -90,6 +90,7 @@ export default function MonitorSettingsPage() { const [scanTimeValue, setScanTimeValue] = useState(7) const [scanTimeUnit, setScanTimeUnit] = useState('day') const scanTimerRef = useRef(null) + const [importing, setImporting] = useState(false) // 从 scanState.status 派生 scanning 状态 const scanning = scanState.status === 'running' || scanState.status === 'cancelling' @@ -307,6 +308,43 @@ export default function MonitorSettingsPage() { } } + // 导入跳过的邮件为工单 + const handleImportSkipped = async () => { + if (!scanState.details || scanState.status !== 'completed') return + + const skippedEmails = scanState.details.filter(d => + d.status === 'skipped' && d.order_number && d.error !== '工单已存在' && d.error !== '邮件已处理过' + ) + + if (skippedEmails.length === 0) { + setMessage({ type: 'error', text: '没有可导入的邮件(已跳过的邮件需要有工单号)' }) + return + } + + if (!confirm(`确认导入 ${skippedEmails.length} 封邮件为工单?`)) return + + setImporting(true) + try { + const res = await fetch('/api/monitor/scan-emails/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ emails: skippedEmails }), + }) + const data = await res.json() + if (data.success) { + setMessage({ type: 'success', text: data.message }) + // 刷新扫描状态 + refreshScanState() + } else { + setMessage({ type: 'error', text: data.error }) + } + } catch (err) { + setMessage({ type: 'error', text: '导入失败' }) + } finally { + setImporting(false) + } + } + // 清理扫描定时器 useEffect(() => { return () => { @@ -861,6 +899,33 @@ export default function MonitorSettingsPage() { )} + + {/* 导入按钮 */} + {scanState.status === 'completed' && scanState.details && scanState.details.some(d => + d.status === 'skipped' && d.order_number && d.error !== '工单已存在' && d.error !== '邮件已处理过' + ) && ( +
+ + + 可导入 {scanState.details.filter(d => + d.status === 'skipped' && d.order_number && d.error !== '工单已存在' && d.error !== '邮件已处理过' + ).length} 封邮件 + +
+ )} )} diff --git a/src/app/api/monitor/scan-emails/import/route.ts b/src/app/api/monitor/scan-emails/import/route.ts new file mode 100644 index 0000000..45ad502 --- /dev/null +++ b/src/app/api/monitor/scan-emails/import/route.ts @@ -0,0 +1,121 @@ +// src/app/api/monitor/scan-emails/import/route.ts +import { NextRequest, NextResponse } from 'next/server' +import { initDatabase } from '@/lib/db-schema' +import { getDb } from '@/lib/db' +import { getCurrentUser } from '@/lib/auth' +import { hasPermission } from '@/lib/permissions' +import { writeAuditLog, getClientIP } from '@/lib/audit' + +interface ImportEmail { + msg_id: string + subject: string + date: string + order_number: string | null + status: string + ticket_no?: string + error?: string +} + +export async function POST(request: NextRequest) { + initDatabase() + const user = await getCurrentUser() + if (!user || !hasPermission(user, 'monitor:write')) { + return NextResponse.json({ error: '权限不足' }, { status: 403 }) + } + + const clientIP = getClientIP(request) + const body = await request.json() + const { emails } = body as { emails: ImportEmail[] } + + if (!emails || !Array.isArray(emails) || emails.length === 0) { + return NextResponse.json({ error: '没有可导入的邮件' }, { status: 400 }) + } + + const db = getDb() + let imported = 0 + let skipped = 0 + let errors = 0 + const results: { order_number: string; status: string; error?: string }[] = [] + + for (const email of emails) { + if (!email.order_number) { + skipped++ + results.push({ + order_number: '-', + status: 'skipped', + error: '无工单号', + }) + continue + } + + const ticketId = parseInt(email.order_number) + + // 检查工单是否已存在 + const existing = db.prepare('SELECT id FROM tickets WHERE id = ?').get(ticketId) + if (existing) { + skipped++ + results.push({ + order_number: email.order_number, + status: 'skipped', + error: '工单已存在', + }) + continue + } + + // 检查是否已处理过 + const processed = db.prepare('SELECT msg_id FROM processed_emails WHERE msg_id = ?').get(email.msg_id) + if (processed) { + skipped++ + results.push({ + order_number: email.order_number, + status: 'skipped', + error: '邮件已处理过', + }) + continue + } + + try { + // 创建工单 + db.prepare(` + INSERT INTO tickets (id, content, current_status, created_by, created_at, updated_at) + VALUES (?, ?, 'open', ?, datetime('now', '+8 hours'), datetime('now', '+8 hours')) + `).run(ticketId, `从邮件导入:${email.subject}`, user.id) + + // 记录已处理邮件 + db.prepare("INSERT OR IGNORE INTO processed_emails (msg_id, subject) VALUES (?, ?)").run(email.msg_id, email.subject) + + // 审计日志 + writeAuditLog({ + userId: user.id, + apiKeyId: null, + action: 'import', + entityType: 'ticket', + entityId: ticketId, + details: { created: { ticket_no: email.order_number, source: 'email_import' } }, + ipAddress: clientIP, + }) + + imported++ + results.push({ + order_number: email.order_number, + status: 'imported', + }) + } catch (err) { + errors++ + results.push({ + order_number: email.order_number, + status: 'error', + error: err instanceof Error ? err.message : '导入失败', + }) + } + } + + return NextResponse.json({ + success: true, + message: `导入完成:成功 ${imported} 封,跳过 ${skipped} 封,错误 ${errors} 封`, + imported, + skipped, + errors, + results, + }) +} diff --git a/src/app/api/monitor/scan-emails/route.ts b/src/app/api/monitor/scan-emails/route.ts index 13a584d..3a8d51e 100644 --- a/src/app/api/monitor/scan-emails/route.ts +++ b/src/app/api/monitor/scan-emails/route.ts @@ -209,7 +209,10 @@ async function scanEmails( try { const msg = await client.fetchOne(uid, { source: true, uid: true }, { uid: true }) - if (!msg || !('source' in msg) || !(msg as any).source) continue + if (!msg || !('source' in msg) || !(msg as any).source) { + console.log(`[Scan] 邮件 ${uid} 无内容,跳过`) + continue + } const parsed = await simpleParser((msg as any).source as Buffer) const subject = parsed.subject || ''