issue-ai/scripts/import-tickets.ts

248 lines
8.6 KiB
TypeScript
Raw Permalink 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.

/**
* 导入历史工单数据
* 用法: npx tsx scripts/import-tickets.ts [excel文件路径]
* 默认路径: templates-docs/工单跟踪记录-总表.xlsx
*/
import Database from 'better-sqlite3'
import path from 'path'
import fs from 'fs'
import bcrypt from 'bcryptjs'
const XLSX = require('xlsx')
const dbPath = process.env.DATABASE_PATH || './data/issue.db'
const dbDir = path.dirname(dbPath)
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true })
const db = new Database(dbPath)
db.pragma('journal_mode = WAL')
db.pragma('foreign_keys = ON')
function initDb() {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT NOT NULL,
email TEXT,
role TEXT NOT NULL DEFAULT 'viewer',
is_active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
permissions TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticket_no TEXT NOT NULL UNIQUE,
device_ip TEXT,
device_sn TEXT,
device_name TEXT,
content TEXT,
assign_time TEXT,
close_time TEXT,
duration_minutes INTEGER,
availability REAL,
process_summary TEXT,
conclusion TEXT,
fault_category TEXT,
fault_subcategory TEXT,
parts_replaced TEXT,
current_status TEXT NOT NULL DEFAULT 'open',
counted_in_sla INTEGER NOT NULL DEFAULT 1,
responsibility TEXT,
created_by INTEGER REFERENCES users(id),
updated_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS ticket_steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
step_order INTEGER NOT NULL,
time_node TEXT,
handler TEXT,
description TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
report_type TEXT NOT NULL,
period_start TEXT,
period_end TEXT,
format TEXT NOT NULL DEFAULT 'pdf',
file_path TEXT,
file_name TEXT,
status TEXT NOT NULL DEFAULT 'pending',
error_message TEXT,
created_by INTEGER REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id INTEGER,
details TEXT,
ip_address TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_tickets_device_ip ON tickets(device_ip);
CREATE INDEX IF NOT EXISTS idx_tickets_assign_time ON tickets(assign_time);
CREATE INDEX IF NOT EXISTS idx_tickets_current_status ON tickets(current_status);
CREATE INDEX IF NOT EXISTS idx_ticket_steps_ticket_id ON ticket_steps(ticket_id);
`)
const existingAdmin = db.prepare('SELECT id FROM users WHERE username = ?').get('admin')
if (!existingAdmin) {
const passwordHash = bcrypt.hashSync('admin123', 10)
db.prepare('INSERT INTO users (username, password_hash, display_name, role) VALUES (?, ?, ?, ?)')
.run('admin', passwordHash, '系统管理员', 'admin')
}
const defaultRoles = [
{ name: 'admin', display_name: '管理员', permissions: '["*"]' },
{ name: 'operator', display_name: '运维人员', permissions: '["tickets:read","tickets:write","reports:read"]' },
{ name: 'viewer', display_name: '查看者', permissions: '["tickets:read","reports:read"]' },
]
for (const r of defaultRoles) {
const ex = db.prepare('SELECT id FROM roles WHERE name = ?').get(r.name)
if (!ex) db.prepare('INSERT INTO roles (name, display_name, permissions) VALUES (?, ?, ?)')
.run(r.name, r.display_name, r.permissions)
}
}
function excelDateToJS(val: any): string | null {
if (!val) return null
if (val instanceof Date) return val.toISOString().replace('T', ' ').slice(0, 19)
if (typeof val === 'number') {
// Excel serial date: days since 1900-01-01 (with 1900 leap year bug)
const utcDays = val - 25569
const utcSeconds = utcDays * 86400
const d = new Date(utcSeconds * 1000)
return d.toISOString().replace('T', ' ').slice(0, 19)
}
const s = String(val).trim()
return s || null
}
function cleanValue(val: any): any {
if (val === null || val === undefined || val === '' || (typeof val === 'number' && isNaN(val))) return null
if (typeof val === 'string' && val.trim() === '') return null
if (typeof val === 'string' && val.trim() === '/') return null
return val
}
function importTicketSheet(ws: any) {
const data = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' })
if (data.length < 3) {
console.log(' Sheet has insufficient data')
return 0
}
// Headers in row 0, title row in row 0 too, actual data from row 2
const headers = data[0] as string[]
const rows = data.slice(2) // Skip header row + title row
const colIdx: Record<string, number> = {}
for (let i = 0; i < headers.length; i++) {
colIdx[headers[i]] = i
}
const stmt = db.prepare(`
INSERT OR REPLACE INTO tickets (
ticket_no, device_ip, device_sn, device_name, content,
assign_time, close_time, duration_minutes, availability,
process_summary, conclusion, fault_category, fault_subcategory,
parts_replaced, current_status, counted_in_sla
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`)
let count = 0
const statusMap: Record<string, string> = {
'已恢复': 'resolved',
'处理中': 'in_progress',
'已结单': 'closed',
'待处理': 'open',
'已关闭': 'closed',
}
const transaction = db.transaction(() => {
for (const row of rows) {
const r = row as any[]
if (!r || r.length === 0) continue
const ticketNo = cleanValue(r[colIdx['故障编号/工单号']])
if (!ticketNo) continue
const rawStatus = cleanValue(r[colIdx['目前状态']]) || 'resolved'
const mappedStatus = statusMap[String(rawStatus)] || 'resolved'
const slaVal = cleanValue(r[colIdx['是否计入SLA']])
const countedInSla = slaVal === '是' ? 1 : 0
stmt.run(
ticketNo,
cleanValue(r[colIdx['故障节点']]), // device_ip
cleanValue(r[colIdx['SN']]), // device_sn
null, // device_name (not in Excel)
cleanValue(r[colIdx['故障表现/工单内容']]), // content
excelDateToJS(r[colIdx['派单时间']]), // assign_time
excelDateToJS(r[colIdx['结单时间']]), // close_time
cleanValue(r[colIdx['处理时长\n分钟']]), // duration_minutes
cleanValue(r[colIdx['单次月度可用性']]), // availability
cleanValue(r[colIdx['处理过程']]), // process_summary
cleanValue(r[colIdx['故障结论']]), // conclusion
cleanValue(r[colIdx['故障分类']]), // fault_category
cleanValue(r[colIdx['故障分类二']]), // fault_subcategory
cleanValue(r[colIdx['是否更换配件']]), // parts_replaced
mappedStatus,
countedInSla,
)
count++
}
})
transaction()
return count
}
async function main() {
const excelPath = process.argv[2] || path.join(__dirname, '..', 'templates-docs', '工单跟踪记录-总表.xlsx')
if (!fs.existsSync(excelPath)) {
console.error(`Excel file not found: ${excelPath}`)
process.exit(1)
}
console.log('Initializing database...')
initDb()
console.log(`Reading Excel: ${excelPath}`)
const wb = XLSX.readFile(excelPath)
console.log('Importing tickets from 故障记录汇总...')
const sheet = wb.Sheets['故障记录汇总']
const count = importTicketSheet(sheet)
console.log(` Imported ${count} tickets`)
console.log(`\nTotal: ${count} tickets imported`)
db.close()
}
main().catch(console.error)