135 lines
4.5 KiB
TypeScript
135 lines
4.5 KiB
TypeScript
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 })
|
||
}
|
||
}
|