diff --git a/add_goods_no_barcode_columns.js b/add_goods_no_barcode_columns.js new file mode 100644 index 0000000..8962c60 --- /dev/null +++ b/add_goods_no_barcode_columns.js @@ -0,0 +1,70 @@ +// 一次性迁移脚本:为 goods 表补充 goods_no、barcode、is_on_sale、cost_price、remark 等字段 +// 用法:node add_goods_no_barcode_columns.js + +const mysql = require('mysql2/promise') +require('dotenv').config() + +const COLUMNS = [ + { name: 'cost_price', ddl: 'ALTER TABLE goods ADD COLUMN `cost_price` decimal(10,2) DEFAULT 0 COMMENT "成本价"' }, + { name: 'goods_no', ddl: 'ALTER TABLE goods ADD COLUMN `goods_no` varchar(64) DEFAULT "" COMMENT "商品货号"' }, + { name: 'barcode', ddl: 'ALTER TABLE goods ADD COLUMN `barcode` varchar(64) DEFAULT "" COMMENT "商品条码"' }, + { name: 'is_on_sale', ddl: 'ALTER TABLE goods ADD COLUMN `is_on_sale` tinyint(1) DEFAULT 1 COMMENT "是否上架 1-上架 0-下架"' }, + { name: 'remark', ddl: 'ALTER TABLE goods ADD COLUMN `remark` varchar(500) DEFAULT "" COMMENT "备注"' } +] + +const INDEXES = [ + { name: 'idx_barcode', ddl: 'ALTER TABLE goods ADD INDEX `idx_barcode` (`barcode`)' }, + { name: 'idx_goods_no', ddl: 'ALTER TABLE goods ADD INDEX `idx_goods_no` (`goods_no`)' } +] + +async function main() { + const config = { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '3306'), + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'miniprogram' + } + + const connection = await mysql.createConnection(config) + try { + const [columns] = await connection.execute('DESCRIBE goods') + const existing = new Set(columns.map(col => col.Field)) + + console.log('当前 goods 表字段:', [...existing].join(', ')) + + for (const col of COLUMNS) { + if (existing.has(col.name)) { + console.log(`- 字段 ${col.name} 已存在,跳过`) + continue + } + console.log(`- 添加字段 ${col.name} ...`) + await connection.execute(col.ddl) + } + + const [indexes] = await connection.execute('SHOW INDEX FROM goods') + const existingIndexes = new Set(indexes.map(i => i.Key_name)) + + for (const idx of INDEXES) { + if (existingIndexes.has(idx.name)) { + console.log(`- 索引 ${idx.name} 已存在,跳过`) + continue + } + console.log(`- 添加索引 ${idx.name} ...`) + try { + await connection.execute(idx.ddl) + } catch (err) { + console.warn(` 索引 ${idx.name} 添加失败:${err.message}`) + } + } + + console.log('完成 ✅') + } finally { + await connection.end() + } +} + +main().catch(err => { + console.error('迁移失败:', err) + process.exit(1) +}) diff --git a/config/schema.sql b/config/schema.sql index 5145d09..7c518be 100644 --- a/config/schema.sql +++ b/config/schema.sql @@ -10,13 +10,17 @@ CREATE TABLE IF NOT EXISTS `categories` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品分类表'; -ALTER TABLE `categories` ADD COLUMN IF NOT EXISTS `color` varchar(20) DEFAULT '#1890ff' COMMENT '分类颜色'; +-- 兼容 MySQL < 8.0.29:基于 information_schema 判断后再 ALTER +SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'categories' AND COLUMN_NAME = 'color'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `categories` ADD COLUMN `color` varchar(20) DEFAULT ''#1890ff'' COMMENT ''分类颜色''', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; CREATE TABLE IF NOT EXISTS `goods` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(200) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '售价', `original_price` decimal(10,2) DEFAULT NULL COMMENT '原价', + `cost_price` decimal(10,2) DEFAULT 0 COMMENT '成本价', `unit` varchar(20) DEFAULT '件' COMMENT '单位', `category_id` int(11) DEFAULT NULL COMMENT '分类ID', `images` text COMMENT '图片JSON', @@ -25,11 +29,17 @@ CREATE TABLE IF NOT EXISTS `goods` ( `is_hot` tinyint(1) DEFAULT 0 COMMENT '是否热销', `is_new` tinyint(1) DEFAULT 0 COMMENT '是否新品', `pricing_type` tinyint(1) DEFAULT 1 COMMENT '定价类型 1-按件 2-按重量', + `goods_no` varchar(64) DEFAULT '' COMMENT '商品货号', + `barcode` varchar(64) DEFAULT '' COMMENT '商品条码', + `is_on_sale` tinyint(1) DEFAULT 1 COMMENT '是否上架 1-上架 0-下架', `description` text COMMENT '商品描述', + `remark` varchar(500) DEFAULT '' COMMENT '备注', `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `category_id` (`category_id`), + KEY `idx_barcode` (`barcode`), + KEY `idx_goods_no` (`goods_no`), CONSTRAINT `goods_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表'; @@ -188,31 +198,55 @@ CREATE TABLE IF NOT EXISTS `points_goods` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='积分商品表'; -- 订单表索引:按状态筛选、按时间排序 -ALTER TABLE `orders` ADD INDEX IF NOT EXISTS `idx_orders_status` (`status`); -ALTER TABLE `orders` ADD INDEX IF NOT EXISTS `idx_orders_created_at` (`created_at`); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'orders' AND INDEX_NAME = 'idx_orders_status'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `orders` ADD INDEX `idx_orders_status` (`status`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'orders' AND INDEX_NAME = 'idx_orders_created_at'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `orders` ADD INDEX `idx_orders_created_at` (`created_at`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 商品表索引:热销/新品筛选、名称搜索 -ALTER TABLE `goods` ADD INDEX IF NOT EXISTS `idx_goods_is_hot` (`is_hot`); -ALTER TABLE `goods` ADD INDEX IF NOT EXISTS `idx_goods_is_new` (`is_new`); -ALTER TABLE `goods` ADD INDEX IF NOT EXISTS `idx_goods_name` (`name`(100)); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'goods' AND INDEX_NAME = 'idx_goods_is_hot'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `goods` ADD INDEX `idx_goods_is_hot` (`is_hot`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'goods' AND INDEX_NAME = 'idx_goods_is_new'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `goods` ADD INDEX `idx_goods_is_new` (`is_new`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'goods' AND INDEX_NAME = 'idx_goods_name'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `goods` ADD INDEX `idx_goods_name` (`name`(100))', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 商品表新增上下架字段 -ALTER TABLE `goods` ADD COLUMN IF NOT EXISTS `is_on_sale` tinyint(1) DEFAULT 1 COMMENT '是否上架 1-上架 0-下架'; -ALTER TABLE `goods` ADD COLUMN IF NOT EXISTS `cost_price` decimal(10,2) DEFAULT 0.00 COMMENT '进价'; +SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'goods' AND COLUMN_NAME = 'is_on_sale'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `goods` ADD COLUMN `is_on_sale` tinyint(1) DEFAULT 1 COMMENT ''是否上架 1-上架 0-下架''', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'goods' AND COLUMN_NAME = 'cost_price'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `goods` ADD COLUMN `cost_price` decimal(10,2) DEFAULT 0.00 COMMENT ''进价''', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 商品表全文索引:关键词搜索加速 (MySQL 5.7+/8.0) -- 注意:FULLTEXT 需 MyISAM 或 InnoDB 5.6+;若不支持会被忽略 -ALTER TABLE `goods` ADD FULLTEXT INDEX IF NOT EXISTS `ft_goods_name_desc` (`name`, `description`); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'goods' AND INDEX_NAME = 'ft_goods_name_desc'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `goods` ADD FULLTEXT INDEX `ft_goods_name_desc` (`name`, `description`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 分类表索引:按排序字段排序 -ALTER TABLE `categories` ADD INDEX IF NOT EXISTS `idx_categories_sort_order` (`sort_order`); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'categories' AND INDEX_NAME = 'idx_categories_sort_order'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `categories` ADD INDEX `idx_categories_sort_order` (`sort_order`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 积分记录表索引:按类型筛选 -ALTER TABLE `points_logs` ADD INDEX IF NOT EXISTS `idx_points_logs_type` (`type`); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'points_logs' AND INDEX_NAME = 'idx_points_logs_type'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `points_logs` ADD INDEX `idx_points_logs_type` (`type`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 采购单表索引:按状态筛选、按时间排序 -ALTER TABLE `purchases` ADD INDEX IF NOT EXISTS `idx_purchases_status` (`status`); -ALTER TABLE `purchases` ADD INDEX IF NOT EXISTS `idx_purchases_created_at` (`created_at`); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'purchases' AND INDEX_NAME = 'idx_purchases_status'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `purchases` ADD INDEX `idx_purchases_status` (`status`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'purchases' AND INDEX_NAME = 'idx_purchases_created_at'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `purchases` ADD INDEX `idx_purchases_created_at` (`created_at`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 购物车表 CREATE TABLE IF NOT EXISTS `carts` ( @@ -233,8 +267,12 @@ CREATE TABLE IF NOT EXISTS `carts` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='购物车表'; -- 购物车表索引 -ALTER TABLE `carts` ADD INDEX IF NOT EXISTS `idx_carts_user_id` (`user_id`); -ALTER TABLE `carts` ADD INDEX IF NOT EXISTS `idx_carts_selected` (`selected`); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'carts' AND INDEX_NAME = 'idx_carts_user_id'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `carts` ADD INDEX `idx_carts_user_id` (`user_id`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'carts' AND INDEX_NAME = 'idx_carts_selected'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `carts` ADD INDEX `idx_carts_selected` (`selected`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 退款表 CREATE TABLE IF NOT EXISTS `refunds` ( @@ -258,8 +296,12 @@ CREATE TABLE IF NOT EXISTS `refunds` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='退款申请表'; -- 退款表索引 -ALTER TABLE `refunds` ADD INDEX IF NOT EXISTS `idx_refunds_status` (`status`); -ALTER TABLE `refunds` ADD INDEX IF NOT EXISTS `idx_refunds_created_at` (`created_at`); +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'refunds' AND INDEX_NAME = 'idx_refunds_status'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `refunds` ADD INDEX `idx_refunds_status` (`status`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @idx_exists = (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'refunds' AND INDEX_NAME = 'idx_refunds_created_at'); +SET @sql = IF(@idx_exists = 0, 'ALTER TABLE `refunds` ADD INDEX `idx_refunds_created_at` (`created_at`)', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 首页分类配置表 CREATE TABLE IF NOT EXISTS `home_categories` ( @@ -326,6 +368,10 @@ CREATE TABLE IF NOT EXISTS `ai_cache` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI 响应缓存'; -- 迁移:order_items.order_id 从 int 改为 varchar(64),与 orders.id (varchar(50)) 保持一致 -ALTER TABLE `order_items` DROP FOREIGN KEY IF EXISTS `order_items_ibfk_1`; +SET @fk_exists = (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'order_items' AND CONSTRAINT_NAME = 'order_items_ibfk_1'); +SET @sql = IF(@fk_exists > 0, 'ALTER TABLE `order_items` DROP FOREIGN KEY `order_items_ibfk_1`', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; ALTER TABLE `order_items` MODIFY COLUMN `order_id` varchar(64) NOT NULL COMMENT '订单ID'; -ALTER TABLE `order_items` ADD CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE; \ No newline at end of file +SET @fk_exists = (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'order_items' AND CONSTRAINT_NAME = 'order_items_ibfk_1'); +SET @sql = IF(@fk_exists = 0, 'ALTER TABLE `order_items` ADD CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE CASCADE', 'SELECT 1'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; \ No newline at end of file diff --git a/run_schema.js b/run_schema.js new file mode 100644 index 0000000..7f41e7f --- /dev/null +++ b/run_schema.js @@ -0,0 +1,88 @@ +// 执行 schema.sql 到远程数据库 +// 用法:node run_schema.js + +const mysql = require('mysql2/promise') +const fs = require('fs') +const path = require('path') +require('dotenv').config({ path: path.join(__dirname, '.env') }) + +const CONFIG = { + host: '110.42.255.239', + port: 3306, + user: 'root', + password: 'Wentian9588.', + database: 'miniprogram', + multipleStatements: true, + charset: 'utf8mb4' +} + +const SCHEMA_PATH = path.join(__dirname, 'config', 'schema.sql') + +function splitSql(sql) { + // 去掉 -- 注释行 + const lines = sql.split('\n').map(l => l.replace(/--.*$/, '')).join('\n') + // 按 ; 切分(保留完整语句) + const stmts = [] + let buf = '' + let inString = false + let stringChar = '' + for (let i = 0; i < lines.length; i++) { + const ch = lines[i] + if (inString) { + buf += ch + if (ch === stringChar && lines[i - 1] !== '\\') { + inString = false + } + continue + } + if (ch === "'" || ch === '"') { + inString = true + stringChar = ch + buf += ch + continue + } + if (ch === ';') { + const s = buf.trim() + if (s) stmts.push(s) + buf = '' + continue + } + buf += ch + } + const tail = buf.trim() + if (tail) stmts.push(tail) + return stmts +} + +async function main() { + const sql = fs.readFileSync(SCHEMA_PATH, 'utf8') + const statements = splitSql(sql) + console.log(`解析到 ${statements.length} 条语句`) + + const connection = await mysql.createConnection(CONFIG) + console.log(`已连接 ${CONFIG.host}:${CONFIG.port}/${CONFIG.database}`) + + let ok = 0 + let fail = 0 + for (let i = 0; i < statements.length; i++) { + const stmt = statements[i] + const preview = stmt.replace(/\s+/g, ' ').slice(0, 80) + try { + await connection.query(stmt) + ok++ + console.log(`[${i + 1}/${statements.length}] OK ${preview}`) + } catch (err) { + fail++ + console.error(`[${i + 1}/${statements.length}] FAIL ${preview}`) + console.error(` ${err.code || ''} ${err.sqlMessage || err.message}`) + } + } + + console.log(`\n完成 ✅ 成功 ${ok} / 失败 ${fail}`) + await connection.end() +} + +main().catch(err => { + console.error('连接失败:', err.message) + process.exit(1) +})