init
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
|
||||
let databaseInstance = null
|
||||
|
||||
export function getDb() {
|
||||
if (!databaseInstance) {
|
||||
const filePath = resolveDatabasePath()
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
||||
databaseInstance = new DatabaseSync(filePath)
|
||||
databaseInstance.exec('PRAGMA foreign_keys = ON;')
|
||||
databaseInstance.exec('PRAGMA journal_mode = WAL;')
|
||||
}
|
||||
|
||||
return databaseInstance
|
||||
}
|
||||
|
||||
export function runInTransaction(fn) {
|
||||
const db = getDb()
|
||||
db.exec('BEGIN')
|
||||
|
||||
try {
|
||||
const result = fn(db)
|
||||
db.exec('COMMIT')
|
||||
return result
|
||||
} catch (error) {
|
||||
db.exec('ROLLBACK')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDatabasePath() {
|
||||
const configured = String(runtimeConfig.database.filePath || '').trim()
|
||||
|
||||
if (!configured) {
|
||||
throw new Error('缺少数据库文件路径配置')
|
||||
}
|
||||
|
||||
return path.resolve(configured)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import process from 'node:process'
|
||||
|
||||
import { getDb } from './client.js'
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const MIGRATIONS_DIR = path.join(CURRENT_DIR, 'migrations')
|
||||
|
||||
export function runDatabaseMigrations() {
|
||||
const db = getDb()
|
||||
const files = fs.readdirSync(MIGRATIONS_DIR)
|
||||
.filter((name) => name.endsWith('.sql'))
|
||||
.sort()
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
applied_at TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
const appliedRows = db.prepare('SELECT filename FROM schema_migrations').all()
|
||||
const applied = new Set(appliedRows.map((row) => row.filename))
|
||||
const insertApplied = db.prepare(`
|
||||
INSERT INTO schema_migrations (filename, applied_at)
|
||||
VALUES (?, ?)
|
||||
`)
|
||||
|
||||
for (const filename of files) {
|
||||
if (applied.has(filename)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, filename), 'utf8')
|
||||
db.exec(sql)
|
||||
insertApplied.run(filename, new Date().toISOString())
|
||||
}
|
||||
}
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url)
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
|
||||
runDatabaseMigrations()
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS orders (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform TEXT NOT NULL,
|
||||
platform_order_id TEXT NOT NULL,
|
||||
order_status TEXT NOT NULL DEFAULT 'created',
|
||||
pay_status TEXT NOT NULL DEFAULT 'unpaid',
|
||||
buyer_id TEXT NOT NULL DEFAULT '',
|
||||
buyer_name TEXT NOT NULL DEFAULT '',
|
||||
receiver_contact TEXT NOT NULL DEFAULT '',
|
||||
total_amount INTEGER NOT NULL DEFAULT 0,
|
||||
currency TEXT NOT NULL DEFAULT 'CNY',
|
||||
raw_payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
paid_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(platform, platform_order_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS order_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
sku_code TEXT NOT NULL,
|
||||
sku_name TEXT NOT NULL DEFAULT '',
|
||||
quantity INTEGER NOT NULL DEFAULT 1,
|
||||
spec_json TEXT NOT NULL DEFAULT '{}',
|
||||
delivery_mode TEXT NOT NULL DEFAULT 'claim_link',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_order_items_sku_code ON order_items(sku_code);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cdk_inventory (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
batch_no TEXT NOT NULL DEFAULT '',
|
||||
sku_code TEXT NOT NULL,
|
||||
cdk_code TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'available',
|
||||
reserved_by_task_id INTEGER,
|
||||
invalid_reason TEXT NOT NULL DEFAULT '',
|
||||
delivered_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(sku_code, cdk_code)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_sku_code ON cdk_inventory(sku_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_status ON cdk_inventory(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_cdk_inventory_reserved_task ON cdk_inventory(reserved_by_task_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS delivery_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
order_id INTEGER NOT NULL,
|
||||
order_item_id INTEGER NOT NULL,
|
||||
platform_order_id TEXT NOT NULL,
|
||||
task_no TEXT NOT NULL,
|
||||
task_status TEXT NOT NULL DEFAULT 'pending_payment',
|
||||
login_type TEXT NOT NULL DEFAULT '',
|
||||
claim_token_id INTEGER,
|
||||
reserved_cdk_id INTEGER,
|
||||
browser_session_id TEXT NOT NULL DEFAULT '',
|
||||
nickname TEXT NOT NULL DEFAULT '',
|
||||
role_id TEXT NOT NULL DEFAULT '',
|
||||
role_name TEXT NOT NULL DEFAULT '',
|
||||
area TEXT NOT NULL DEFAULT '',
|
||||
partition_name TEXT NOT NULL DEFAULT '',
|
||||
result_code TEXT NOT NULL DEFAULT '',
|
||||
result_message TEXT NOT NULL DEFAULT '',
|
||||
screenshot_path TEXT NOT NULL DEFAULT '',
|
||||
artifacts_json TEXT NOT NULL DEFAULT '{}',
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||
expires_at TEXT,
|
||||
claimed_at TEXT,
|
||||
role_confirmed_at TEXT,
|
||||
redeemed_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(order_item_id) REFERENCES order_items(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(reserved_cdk_id) REFERENCES cdk_inventory(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_delivery_tasks_task_no ON delivery_tasks(task_no);
|
||||
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_order_id ON delivery_tasks(order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_order_item_id ON delivery_tasks(order_item_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_status ON delivery_tasks(task_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_delivery_tasks_platform_order_id ON delivery_tasks(platform_order_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS claim_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
expired_at TEXT NOT NULL,
|
||||
used_at TEXT,
|
||||
max_use_count INTEGER NOT NULL DEFAULT 1,
|
||||
used_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(task_id) REFERENCES delivery_tasks(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_claim_tokens_token ON claim_tokens(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_claim_tokens_task_id ON claim_tokens(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_claim_tokens_status ON claim_tokens(status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS webhook_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_key TEXT NOT NULL,
|
||||
signature_valid INTEGER NOT NULL DEFAULT 0,
|
||||
headers_json TEXT NOT NULL DEFAULT '{}',
|
||||
query_json TEXT NOT NULL DEFAULT '{}',
|
||||
body_json TEXT NOT NULL DEFAULT '{}',
|
||||
processed INTEGER NOT NULL DEFAULT 0,
|
||||
process_error TEXT NOT NULL DEFAULT '',
|
||||
related_order_id INTEGER,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_events_platform_key ON webhook_events(platform, event_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_events_processed ON webhook_events(processed);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_events_created_at ON webhook_events(created_at);
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'operator',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_users_role ON admin_users(role);
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_users_status ON admin_users(status);
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE IF NOT EXISTS admin_audit_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
actor_user_id INTEGER NOT NULL,
|
||||
actor_username TEXT NOT NULL,
|
||||
actor_role TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
target_type TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL DEFAULT '',
|
||||
payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (actor_user_id) REFERENCES admin_users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_actor_user_id ON admin_audit_logs(actor_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_action ON admin_audit_logs(action);
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_target_type ON admin_audit_logs(target_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_created_at ON admin_audit_logs(created_at);
|
||||
@@ -0,0 +1,29 @@
|
||||
CREATE TABLE IF NOT EXISTS message_deliveries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
order_id INTEGER,
|
||||
task_id INTEGER,
|
||||
platform_order_id TEXT NOT NULL DEFAULT '',
|
||||
recipient_key TEXT NOT NULL DEFAULT '',
|
||||
message_content TEXT NOT NULL DEFAULT '',
|
||||
claim_url TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
request_url TEXT NOT NULL DEFAULT '',
|
||||
request_headers_json TEXT NOT NULL DEFAULT '{}',
|
||||
request_body_json TEXT NOT NULL DEFAULT '{}',
|
||||
response_status INTEGER NOT NULL DEFAULT 0,
|
||||
response_json TEXT NOT NULL DEFAULT '{}',
|
||||
error_message TEXT NOT NULL DEFAULT '',
|
||||
sent_at TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (task_id) REFERENCES delivery_tasks(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_message_deliveries_order_id ON message_deliveries(order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_deliveries_task_id ON message_deliveries(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_deliveries_platform_order_id ON message_deliveries(platform_order_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_deliveries_status ON message_deliveries(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_message_deliveries_created_at ON message_deliveries(created_at);
|
||||
Reference in New Issue
Block a user