/** * 数据库迁移执行脚本 * 用于执行采购-付款-物流-退库一体化流程的数据库表创建和字段扩展 * 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md */ const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const fs = require('fs'); const dbPath = path.join(__dirname, 'company_finance.db'); const db = new sqlite3.Database(dbPath); console.log('开始执行数据库迁移...'); console.log('数据库路径:', dbPath); const runSQL = (sql, params = []) => { return new Promise((resolve, reject) => { db.run(sql, params, function(err) { if (err) { if (err.message.includes('already exists') || err.message.includes('duplicate column name')) { resolve({ skipped: true, message: err.message }); } else { reject(err); } } else { resolve({ success: true, lastID: this.lastID, changes: this.changes }); } }); }); }; const runAllSQL = (sql, params = []) => { return new Promise((resolve, reject) => { db.all(sql, params, (err, rows) => { if (err) { reject(err); } else { resolve(rows); } }); }); }; async function migrate() { try { console.log('\n========================================'); console.log('第一部分:创建新表'); console.log('========================================\n'); const createTables = [ { name: 'logistics_companies', sql: `CREATE TABLE IF NOT EXISTS logistics_companies ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, name TEXT NOT NULL, address TEXT, phone TEXT, email TEXT, quotation_description TEXT, status TEXT DEFAULT 'active', remark TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )` }, { name: 'logistics_company_payment_infos', sql: `CREATE TABLE IF NOT EXISTS logistics_company_payment_infos ( id INTEGER PRIMARY KEY AUTOINCREMENT, logistics_company_id INTEGER NOT NULL, account_name TEXT, account_number TEXT, bank_name TEXT, qr_code TEXT, is_default INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (logistics_company_id) REFERENCES logistics_companies(id) ON DELETE CASCADE )` }, { name: 'logistics_records', sql: `CREATE TABLE IF NOT EXISTS logistics_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, purchase_order_id INTEGER NOT NULL, ship_from TEXT DEFAULT 'Laos', logistics_company_id INTEGER, logistics_company TEXT, tracking_number TEXT, ship_date DATE, ship_location TEXT, estimated_arrival_date DATE, customs_arrival_date DATE, customs_clearance_date DATE, use_hub INTEGER DEFAULT 0, hub_arrival_date DATE, hub_receiver TEXT, hub_verified_quantity REAL, second_ship_date DATE, primary_freight REAL DEFAULT 0, primary_freight_currency TEXT DEFAULT 'CNY', primary_freight_status TEXT DEFAULT 'pending', primary_freight_document TEXT, secondary_freight REAL DEFAULT 0, secondary_freight_currency TEXT DEFAULT 'LAK', secondary_freight_status TEXT DEFAULT 'pending', driver_phone TEXT, cargo_weight REAL, transport_distance REAL, final_arrival_date DATE, final_location TEXT, status TEXT DEFAULT 'pending', remark TEXT, created_by TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (purchase_order_id) REFERENCES purchase_orders(id), FOREIGN KEY (logistics_company_id) REFERENCES logistics_companies(id) )` }, { name: 'verification_records', sql: `CREATE TABLE IF NOT EXISTS verification_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, purchase_order_id INTEGER NOT NULL, logistics_record_id INTEGER, verification_type TEXT DEFAULT 'direct', verification_date DATE NOT NULL, verifier TEXT NOT NULL, items TEXT, total_ordered REAL, total_received REAL, total_verified REAL, total_rejected REAL DEFAULT 0, project_id INTEGER, storage_type TEXT, status TEXT DEFAULT 'pending', remark TEXT, attachments TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (purchase_order_id) REFERENCES purchase_orders(id), FOREIGN KEY (logistics_record_id) REFERENCES logistics_records(id), FOREIGN KEY (project_id) REFERENCES projects(id) )` }, { name: 'return_records', sql: `CREATE TABLE IF NOT EXISTS return_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, project_id INTEGER NOT NULL, return_type TEXT DEFAULT 'warehouse', return_date DATE NOT NULL, applicant TEXT NOT NULL, items TEXT, total_quantity REAL, total_amount REAL, cost_adjustment REAL DEFAULT 0, refund_amount REAL DEFAULT 0, status TEXT DEFAULT 'pending', remark TEXT, attachments TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id) )` }, { name: 'material_price_history', sql: `CREATE TABLE IF NOT EXISTS material_price_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, purchase_order_id INTEGER, supplier_id INTEGER, supplier_country TEXT, unit_price REAL NOT NULL, currency TEXT DEFAULT 'CNY', quantity REAL, purchase_date DATE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES products(id), FOREIGN KEY (purchase_order_id) REFERENCES purchase_orders(id), FOREIGN KEY (supplier_id) REFERENCES suppliers(id) )` }, { name: 'project_material_inventory', sql: `CREATE TABLE IF NOT EXISTS project_material_inventory ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, product_id INTEGER NOT NULL, product_name TEXT, unit TEXT, purchased_quantity REAL DEFAULT 0, received_quantity REAL DEFAULT 0, used_quantity REAL DEFAULT 0, returned_quantity REAL DEFAULT 0, current_quantity REAL DEFAULT 0, total_amount REAL DEFAULT 0, average_price REAL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (product_id) REFERENCES products(id), UNIQUE(project_id, product_id) )` } ]; for (const table of createTables) { console.log(`创建表: ${table.name}...`); const result = await runSQL(table.sql); if (result.skipped) { console.log(` 表 ${table.name} 已存在,跳过`); } else { console.log(` 表 ${table.name} 创建成功`); } } console.log('\n========================================'); console.log('第二部分:创建索引'); console.log('========================================\n'); const createIndexes = [ 'CREATE INDEX IF NOT EXISTS idx_logistics_companies_code ON logistics_companies(code)', 'CREATE INDEX IF NOT EXISTS idx_logistics_companies_status ON logistics_companies(status)', 'CREATE INDEX IF NOT EXISTS idx_lc_payment_infos_company ON logistics_company_payment_infos(logistics_company_id)', 'CREATE INDEX IF NOT EXISTS idx_logistics_records_code ON logistics_records(code)', 'CREATE INDEX IF NOT EXISTS idx_logistics_records_order ON logistics_records(purchase_order_id)', 'CREATE INDEX IF NOT EXISTS idx_logistics_records_status ON logistics_records(status)', 'CREATE INDEX IF NOT EXISTS idx_logistics_records_company ON logistics_records(logistics_company_id)', 'CREATE INDEX IF NOT EXISTS idx_verification_records_code ON verification_records(code)', 'CREATE INDEX IF NOT EXISTS idx_verification_records_order ON verification_records(purchase_order_id)', 'CREATE INDEX IF NOT EXISTS idx_verification_records_status ON verification_records(status)', 'CREATE INDEX IF NOT EXISTS idx_return_records_code ON return_records(code)', 'CREATE INDEX IF NOT EXISTS idx_return_records_project ON return_records(project_id)', 'CREATE INDEX IF NOT EXISTS idx_return_records_status ON return_records(status)', 'CREATE INDEX IF NOT EXISTS idx_material_price_history_product ON material_price_history(product_id)', 'CREATE INDEX IF NOT EXISTS idx_material_price_history_supplier ON material_price_history(supplier_id)', 'CREATE INDEX IF NOT EXISTS idx_material_price_history_date ON material_price_history(purchase_date)', 'CREATE INDEX IF NOT EXISTS idx_project_material_inventory_project ON project_material_inventory(project_id)', 'CREATE INDEX IF NOT EXISTS idx_project_material_inventory_product ON project_material_inventory(product_id)' ]; for (const indexSql of createIndexes) { await runSQL(indexSql); } console.log('索引创建完成'); console.log('\n========================================'); console.log('第三部分:扩展现有表字段'); console.log('========================================\n'); const alterTableStatements = [ { table: 'purchase_orders', column: 'project_id', type: 'INTEGER' }, { table: 'purchase_orders', column: 'supplier_country', type: "TEXT DEFAULT 'Laos'" }, { table: 'purchase_orders', column: 'estimated_amount', type: 'REAL DEFAULT 0' }, { table: 'purchase_orders', column: 'paid_amount', type: 'REAL DEFAULT 0' }, { table: 'purchase_orders', column: 'contract_url', type: 'TEXT' }, { table: 'purchase_orders', column: 'quotation_url', type: 'TEXT' }, { table: 'purchase_orders', column: 'actual_delivery_date', type: 'DATE' }, { table: 'purchase_orders', column: 'remark', type: 'TEXT' }, { table: 'purchase_order_items', column: 'received_quantity', type: 'REAL DEFAULT 0' }, { table: 'purchase_order_items', column: 'verified_quantity', type: 'REAL DEFAULT 0' }, { table: 'payment_plans', column: 'stage', type: 'TEXT' }, { table: 'payment_plans', column: 'planned_date', type: 'DATE' }, { table: 'payment_plans', column: 'planned_amount', type: 'REAL' }, { table: 'payment_plans', column: 'planned_percentage', type: 'REAL' }, { table: 'payment_plans', column: 'actual_amount', type: 'REAL DEFAULT 0' }, { table: 'payment_plans', column: 'actual_date', type: 'DATE' }, { table: 'payment_plans', column: 'payment_request_id', type: 'INTEGER' }, { table: 'payment_plans', column: 'reminder_days', type: 'INTEGER DEFAULT 3' }, { table: 'payment_plans', column: 'remark', type: 'TEXT' }, { table: 'payment_requests', column: 'payment_type', type: "TEXT DEFAULT 'material'" }, { table: 'payment_requests', column: 'purchase_order_id', type: 'INTEGER' }, { table: 'payment_requests', column: 'logistics_company_id', type: 'INTEGER' }, { table: 'payment_requests', column: 'logistics_document_url', type: 'TEXT' }, { table: 'payment_requests', column: 'driver_phone', type: 'TEXT' }, { table: 'payment_requests', column: 'cargo_weight', type: 'REAL' }, { table: 'payment_requests', column: 'transport_distance', type: 'REAL' }, { table: 'suppliers', column: 'supply_category', type: 'TEXT' }, { table: 'suppliers', column: 'country', type: 'TEXT' }, { table: 'suppliers', column: 'address', type: 'TEXT' }, { table: 'suppliers', column: 'phone', type: 'TEXT' }, { table: 'suppliers', column: 'email', type: 'TEXT' }, { table: 'suppliers', column: 'status', type: "TEXT DEFAULT 'active'" }, { table: 'purchase_requests', column: 'expected_date', type: 'DATE' } ]; for (const stmt of alterTableStatements) { const sql = `ALTER TABLE ${stmt.table} ADD COLUMN ${stmt.column} ${stmt.type}`; console.log(`扩展表 ${stmt.table} 添加字段 ${stmt.column}...`); const result = await runSQL(sql); if (result.skipped) { console.log(` 字段 ${stmt.column} 已存在,跳过`); } else { console.log(` 字段 ${stmt.column} 添加成功`); } } console.log('\n========================================'); console.log('第四部分:创建扩展字段索引'); console.log('========================================\n'); const extraIndexes = [ 'CREATE INDEX IF NOT EXISTS idx_purchase_orders_project ON purchase_orders(project_id)', 'CREATE INDEX IF NOT EXISTS idx_purchase_orders_supplier_country ON purchase_orders(supplier_country)', 'CREATE INDEX IF NOT EXISTS idx_payment_requests_type ON payment_requests(payment_type)', 'CREATE INDEX IF NOT EXISTS idx_payment_requests_order ON payment_requests(purchase_order_id)', 'CREATE INDEX IF NOT EXISTS idx_suppliers_country ON suppliers(country)', 'CREATE INDEX IF NOT EXISTS idx_suppliers_status ON suppliers(status)' ]; for (const indexSql of extraIndexes) { await runSQL(indexSql); } console.log('扩展字段索引创建完成'); console.log('\n========================================'); console.log('第五部分:验证表结构'); console.log('========================================\n'); const tables = [ 'logistics_companies', 'logistics_company_payment_infos', 'logistics_records', 'verification_records', 'return_records', 'material_price_history', 'project_material_inventory' ]; for (const table of tables) { const rows = await runAllSQL(`PRAGMA table_info(${table})`); console.log(`表 ${table} 字段数: ${rows.length}`); } console.log('\n========================================'); console.log('迁移完成!'); console.log('========================================\n'); } catch (error) { console.error('迁移失败:', error); process.exit(1); } finally { db.close(); } } migrate();