修复工单系统竞态与接口安全问题
This commit is contained in:
@@ -274,10 +274,11 @@ function migrateSchema() {
|
||||
assignee_id INTEGER,
|
||||
created_at DATETIME DEFAULT (datetime('now')),
|
||||
updated_at DATETIME DEFAULT (datetime('now')),
|
||||
last_reply_at DATETIME DEFAULT (datetime('now')),
|
||||
last_reply_at DATETIME DEFAULT NULL,
|
||||
first_response_at DATETIME,
|
||||
resolved_at DATETIME,
|
||||
closed_at DATETIME,
|
||||
revision INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)`);
|
||||
@@ -305,6 +306,97 @@ function migrateSchema() {
|
||||
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)`);
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_requester ON tickets(requester_id, updated_at DESC)'); } catch {}
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_status_updated ON tickets(status, updated_at DESC)'); } catch {}
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_assignee ON tickets(assignee_id, status, updated_at DESC)'); } catch {}
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_category ON tickets(category, updated_at DESC)'); } catch {}
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_ticket_messages_ticket ON ticket_messages(ticket_id, created_at ASC)'); } catch {}
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_ticket_events_ticket ON ticket_events(ticket_id, created_at ASC)'); } catch {}
|
||||
} },
|
||||
// v17: 工单并发版本与回复时间修正;为已存在的部分工单表补齐字段
|
||||
{ version: 17, up: () => {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS tickets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ticket_no TEXT NOT NULL UNIQUE,
|
||||
requester_id INTEGER,
|
||||
subject TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'other',
|
||||
priority TEXT NOT NULL DEFAULT 'normal',
|
||||
status TEXT NOT NULL DEFAULT 'open',
|
||||
source TEXT NOT NULL DEFAULT 'site',
|
||||
source_url TEXT DEFAULT '',
|
||||
source_type TEXT DEFAULT '',
|
||||
source_id INTEGER DEFAULT 0,
|
||||
browser_info TEXT DEFAULT '',
|
||||
assignee_id INTEGER,
|
||||
created_at DATETIME DEFAULT (datetime('now')),
|
||||
updated_at DATETIME DEFAULT (datetime('now')),
|
||||
last_reply_at DATETIME DEFAULT NULL,
|
||||
first_response_at DATETIME,
|
||||
resolved_at DATETIME,
|
||||
closed_at DATETIME,
|
||||
revision INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (requester_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS ticket_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ticket_id INTEGER NOT NULL,
|
||||
author_id INTEGER,
|
||||
content TEXT NOT NULL,
|
||||
is_internal INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT (datetime('now')),
|
||||
updated_at DATETIME DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)`);
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS ticket_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ticket_id INTEGER NOT NULL,
|
||||
actor_id INTEGER,
|
||||
event_type TEXT NOT NULL,
|
||||
field_name TEXT DEFAULT '',
|
||||
old_value TEXT DEFAULT '',
|
||||
new_value TEXT DEFAULT '',
|
||||
detail TEXT DEFAULT '',
|
||||
created_at DATETIME DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
)`);
|
||||
const ensureColumns = (table, columns) => {
|
||||
const existing = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
||||
for (const [name, definition] of columns) {
|
||||
if (!existing.has(name)) {
|
||||
try { db.exec(`ALTER TABLE ${table} ADD COLUMN ${name} ${definition}`); } catch {}
|
||||
}
|
||||
}
|
||||
};
|
||||
ensureColumns('tickets', [
|
||||
['ticket_no', "TEXT NOT NULL DEFAULT ''"], ['requester_id', 'INTEGER'],
|
||||
['subject', "TEXT NOT NULL DEFAULT ''"], ['description', "TEXT NOT NULL DEFAULT ''"],
|
||||
['category', "TEXT NOT NULL DEFAULT 'other'"], ['priority', "TEXT NOT NULL DEFAULT 'normal'"],
|
||||
['status', "TEXT NOT NULL DEFAULT 'open'"], ['source', "TEXT NOT NULL DEFAULT 'site'"],
|
||||
['source_url', "TEXT DEFAULT ''"], ['source_type', "TEXT DEFAULT ''"], ['source_id', 'INTEGER DEFAULT 0'],
|
||||
['browser_info', "TEXT DEFAULT ''"], ['assignee_id', 'INTEGER'],
|
||||
['created_at', 'DATETIME DEFAULT NULL'], ['updated_at', 'DATETIME DEFAULT NULL'],
|
||||
['last_reply_at', 'DATETIME DEFAULT NULL'], ['first_response_at', 'DATETIME DEFAULT NULL'],
|
||||
['resolved_at', 'DATETIME DEFAULT NULL'], ['closed_at', 'DATETIME DEFAULT NULL'],
|
||||
['revision', 'INTEGER NOT NULL DEFAULT 0'],
|
||||
]);
|
||||
ensureColumns('ticket_messages', [
|
||||
['ticket_id', 'INTEGER NOT NULL DEFAULT 0'], ['author_id', 'INTEGER'],
|
||||
['content', "TEXT NOT NULL DEFAULT ''"], ['is_internal', 'INTEGER NOT NULL DEFAULT 0'],
|
||||
['created_at', 'DATETIME DEFAULT NULL'], ['updated_at', 'DATETIME DEFAULT NULL'],
|
||||
]);
|
||||
ensureColumns('ticket_events', [
|
||||
['ticket_id', 'INTEGER NOT NULL DEFAULT 0'], ['actor_id', 'INTEGER'],
|
||||
['event_type', "TEXT NOT NULL DEFAULT ''"], ['field_name', "TEXT DEFAULT ''"],
|
||||
['old_value', "TEXT DEFAULT ''"], ['new_value', "TEXT DEFAULT ''"],
|
||||
['detail', "TEXT DEFAULT ''"], ['created_at', 'DATETIME DEFAULT NULL'],
|
||||
]);
|
||||
db.exec(`UPDATE tickets SET last_reply_at = NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM ticket_messages WHERE ticket_messages.ticket_id = tickets.id)`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_requester ON tickets(requester_id, updated_at DESC)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_status_updated ON tickets(status, updated_at DESC)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_tickets_assignee ON tickets(assignee_id, status, updated_at DESC)');
|
||||
|
||||
Reference in New Issue
Block a user