issue-ai/src/app/api/tickets/import/route.ts

135 lines
4.5 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 { NextRequest, NextResponse } from 'next/server'
import { getDb } from '@/lib/db'
import { initDatabase } from '@/lib/db-schema'
import { getCurrentUser } from '@/lib/auth'
import { hasPermission } from '@/lib/permissions'
import { parseExcelTickets } from '@/lib/excel'
function validateTicketNo(ticketNo: string): string | null {
if (!/^\d{14}$/.test(ticketNo)) {
return '工单号必须为 14 位纯数字'
}
const y = parseInt(ticketNo.slice(0, 4))
const m = parseInt(ticketNo.slice(4, 6))
const d = parseInt(ticketNo.slice(6, 8))
const dt = new Date(y, m - 1, d)
if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) {
return '工单号前 8 位必须为合法日期YYYYMMDD'
}
return null
}
export async function POST(request: NextRequest) {
try {
initDatabase()
const user = await getCurrentUser()
if (!user) return NextResponse.json({ error: '未登录' }, { status: 401 })
if (!hasPermission(user, 'tickets:write')) return NextResponse.json({ error: '权限不足' }, { status: 403 })
const formData = await request.formData()
const file = formData.get('file') as File | null
const overwrite = formData.get('overwrite') === 'true'
if (!file) return NextResponse.json({ error: '请上传文件' }, { status: 400 })
const buffer = Buffer.from(await file.arrayBuffer())
const parsed = parseExcelTickets(buffer)
if (parsed.length === 0) return NextResponse.json({ error: '文件中没有有效工单数据' }, { status: 400 })
const db = getDb()
// 校验工单号
const validationErrors: string[] = []
const validTickets: typeof parsed = []
for (let i = 0; i < parsed.length; i++) {
const t = parsed[i]
if (!t.ticket_no) {
validationErrors.push(`${i + 2} 行: 缺少工单号`)
continue
}
const err = validateTicketNo(t.ticket_no)
if (err) {
validationErrors.push(`${i + 2} 行: ${err}${t.ticket_no}`)
continue
}
validTickets.push(t)
}
if (validTickets.length === 0) {
return NextResponse.json({
success: false,
error: '所有工单号校验失败',
validationErrors,
}, { status: 400 })
}
// 检查重复
const existingIds = new Set(
(db.prepare('SELECT id FROM tickets').all() as { id: number }[]).map(r => r.id)
)
const conflicts = validTickets.filter(t => existingIds.has(parseInt(t.ticket_no!)))
if (conflicts.length > 0 && !overwrite) {
return NextResponse.json({
success: false,
conflicts: conflicts.map(t => t.ticket_no),
message: `${conflicts.length} 个工单号已存在,是否覆盖?`,
requireOverwrite: true,
validationErrors: validationErrors.length > 0 ? validationErrors : undefined,
}, { status: 409 })
}
// 执行导入
const insertStmt = db.prepare(`
INSERT OR REPLACE INTO tickets (id, device_ip, device_sn, device_name, content, assign_time,
fault_category, fault_subcategory, responsibility, current_status, counted_in_sla, created_by, updated_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
const imported: string[] = []
const errors: string[] = []
const transaction = db.transaction(() => {
for (const t of validTickets) {
try {
const ticketId = parseInt(t.ticket_no!)
insertStmt.run(
ticketId,
t.device_ip || null,
t.device_sn || null,
t.device_name || null,
t.content || null,
t.assign_time || new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString().slice(0, 19),
t.fault_category || null,
t.fault_subcategory || null,
t.responsibility || null,
t.current_status || 'open',
t.counted_in_sla ?? 1,
user.id,
user.id,
)
imported.push(t.ticket_no!)
} catch (e) {
errors.push(`${validTickets.indexOf(t) + 2} 行: ${e instanceof Error ? e.message : '导入失败'}`)
}
}
})
transaction()
return NextResponse.json({
success: true,
imported: imported.length,
overwritten: conflicts.length,
errors: errors.length > 0 ? errors : undefined,
validationErrors: validationErrors.length > 0 ? validationErrors : undefined,
})
} catch (e) {
const msg = e instanceof Error ? e.message : '导入失败'
return NextResponse.json({ error: msg }, { status: 500 })
}
}