" . htmlspecialchars($msg) . "
\n"; }; /** * 增量升级模式:仅给已部署的系统补齐新模块的数据表/列,绝不触碰已有业务数据。 * 与 install/upgrade_crm_psi.sql 逻辑等价,但改用 PDO 直连(避免 DELIMITER/存储过程在 PHP 侧不兼容)。 * 用法:php install/install.php --upgrade 或 Web: /install/install.php?upgrade=1 */ function runUpgrade(): void { global $out; $driver = Core\Db::driver(); if ($driver !== 'mysql') { $out("⚠ 当前为 file 模式,CRM/PSI 在文件模式下自动生成 JSON,无需数据库升级。"); return; } $pdo = Core\Db::pdo(); $pdo->exec("SET NAMES utf8mb4"); $stmts = [ // ===== CRM 客户管理 ===== "CREATE TABLE IF NOT EXISTS `crm_customers` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(120) NOT NULL, `company` VARCHAR(160) DEFAULT '', `contact` VARCHAR(60) DEFAULT '', `phone` VARCHAR(40) DEFAULT '', `email` VARCHAR(120) DEFAULT '', `country` VARCHAR(60) DEFAULT '', `type` VARCHAR(20) DEFAULT 'trade', `source` VARCHAR(40) DEFAULT '', `level` VARCHAR(20) DEFAULT 'C', `remark` TEXT, `owner` VARCHAR(60) DEFAULT '', `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `crm_leads` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `customer_id` INT DEFAULT 0, `title` VARCHAR(200) NOT NULL, `amount` DECIMAL(12,2) DEFAULT 0, `stage` VARCHAR(20) DEFAULT 'new', `expected_close` VARCHAR(20) DEFAULT '', `owner` VARCHAR(60) DEFAULT '', `remark` TEXT, `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `crm_followups` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `customer_id` INT DEFAULT 0, `lead_id` INT DEFAULT 0, `content` TEXT, `next_at` VARCHAR(20) DEFAULT '', `owner` VARCHAR(60) DEFAULT '', `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", // ===== 进销存 PSI ===== "CREATE TABLE IF NOT EXISTS `psi_materials` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `code` VARCHAR(40) DEFAULT '', `name` VARCHAR(160) NOT NULL, `spec` VARCHAR(120) DEFAULT '', `unit` VARCHAR(10) DEFAULT '个', `category` VARCHAR(40) DEFAULT '', `stock` DECIMAL(12,2) DEFAULT 0, `price` DECIMAL(10,2) DEFAULT 0, `supplier_id` INT DEFAULT 0, `remark` TEXT, `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `psi_products` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `code` VARCHAR(40) DEFAULT '', `name` VARCHAR(160) NOT NULL, `spec` VARCHAR(120) DEFAULT '', `unit` VARCHAR(10) DEFAULT '件', `category` VARCHAR(40) DEFAULT '', `stock` DECIMAL(12,2) DEFAULT 0, `cost` DECIMAL(10,2) DEFAULT 0, `price` DECIMAL(10,2) DEFAULT 0, `remark` TEXT, `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `psi_suppliers` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(160) NOT NULL, `contact` VARCHAR(60) DEFAULT '', `phone` VARCHAR(40) DEFAULT '', `country` VARCHAR(60) DEFAULT '', `remark` TEXT, `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `psi_purchases` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `order_no` VARCHAR(40) NOT NULL, `supplier_id` INT DEFAULT 0, `item_type` VARCHAR(10) DEFAULT 'material', `item_id` INT DEFAULT 0, `qty` DECIMAL(12,2) DEFAULT 0, `price` DECIMAL(10,2) DEFAULT 0, `amount` DECIMAL(12,2) DEFAULT 0, `status` VARCHAR(20) DEFAULT 'pending', `remark` TEXT, `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `psi_sales` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `order_no` VARCHAR(40) NOT NULL, `customer` VARCHAR(160) DEFAULT '', `channel` VARCHAR(20) DEFAULT 'domestic', `item_id` INT DEFAULT 0, `qty` DECIMAL(12,2) DEFAULT 0, `price` DECIMAL(10,2) DEFAULT 0, `amount` DECIMAL(12,2) DEFAULT 0, `status` VARCHAR(20) DEFAULT 'pending', `remark` TEXT, `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", "CREATE TABLE IF NOT EXISTS `psi_stock_moves` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `item_type` VARCHAR(10) DEFAULT 'product', `item_id` INT DEFAULT 0, `direction` VARCHAR(10) DEFAULT 'in', `qty` DECIMAL(12,2) DEFAULT 0, `ref_no` VARCHAR(40) DEFAULT '', `remark` TEXT, `created_at` VARCHAR(20) DEFAULT '' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", ]; foreach ($stmts as $s) { $pdo->exec($s); } $out("✓ CRM/PSI 数据表已就绪(CREATE TABLE IF NOT EXISTS,幂等可重复执行)"); // 为 admin_users 补齐分系统角色列(先查 information_schema,存在则跳过,兼容 5.7/8.0) $cols = $pdo->query("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='admin_users'")->fetchAll(PDO::FETCH_COLUMN); foreach (['crm_role' => "VARCHAR(20) DEFAULT 'none'", 'psi_role' => "VARCHAR(20) DEFAULT 'none'"] as $c => $def) { if (!in_array($c, $cols, true)) { $pdo->exec("ALTER TABLE `admin_users` ADD COLUMN `{$c}` {$def}"); $out("✓ 已添加 admin_users.{$c}"); } else { $out("⊘ admin_users.{$c} 已存在,跳过"); } } $out("✓ 数据升级完成。请登录后台 → 用户管理,将相关账号 crm_role / psi_role 设为 admin。"); } // 升级模式:绕过 installed.lock,仅增量建表/补列 $isUpgrade = ($cli && in_array('--upgrade', $argv ?? [], true)) || (!$cli && trim((string)($_GET['upgrade'] ?? '')) === '1'); if ($isUpgrade) { runUpgrade(); exit; } // 质量红线:生产环境二次运行保护。已安装后写入 installed.lock,再次访问直接拒绝,避免数据被清空。 $lock = BASE_PATH . '/storage/installed.lock'; if (is_file($lock) && !($cli && in_array('--force', $argv ?? [], true))) { $out("⛔ 检测到 installed.lock,系统已安装。如需重置请删除该文件或执行:php install/install.php --force"); exit; } $seed = require BASE_PATH . '/install/seed.php'; $driver = Core\Db::driver(); if ($driver === 'file') { $dir = Core\Db::fileDir(); foreach ($seed as $table => $rows) { // 确保每条记录带 id(如 settings 种子未含 id),否则文件模式无法按主键更新 $i = 1; foreach ($rows as &$r) { if (!isset($r['id'])) { $r['id'] = $i; } $i++; } unset($r); file_put_contents($dir . "/{$table}.json", json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); $out("✓ 写入 {$table}.json (" . count($rows) . " 条)"); } } else { // MySQL:建表 + 插入 $sql = file_get_contents(BASE_PATH . '/install/schema.sql'); $pdo = Core\Db::pdo(); $pdo->exec($sql); $out("✓ 数据表已创建"); // 幂等升级:为已存在的 admin_users 补齐分系统角色列(已有则忽略) foreach (['crm_role VARCHAR(20) DEFAULT \'none\'', 'psi_role VARCHAR(20) DEFAULT \'none\''] as $col) { try { $pdo->exec("ALTER TABLE `admin_users` ADD COLUMN IF NOT EXISTS `{$col}`"); } catch (\Throwable $e) { /* 5.7 不支持 IF NOT EXISTS 时忽略已存在错误 */ } } $out("✓ admin_users 分系统角色列已补齐"); $map = [ 'categories' => new App\Models\Category(), 'products' => new App\Models\Product(), 'news' => new App\Models\News(), 'pages' => new App\Models\Page(), 'banners' => new App\Models\Banner(), 'cases' => new App\Models\CustomerCase(), 'admin_users'=> new App\Models\AdminUser(), 'settings' => new App\Models\Setting(), 'crm_customers' => new App\Models\CRM\Customer(), 'crm_leads' => new App\Models\CRM\Lead(), 'crm_followups' => new App\Models\CRM\FollowUp(), 'psi_suppliers' => new App\Models\PSI\Supplier(), 'psi_materials' => new App\Models\PSI\Material(), 'psi_products' => new App\Models\PSI\Product(), 'psi_purchases' => new App\Models\PSI\Purchase(), 'psi_sales' => new App\Models\PSI\Sales(), 'psi_stock_moves'=> new App\Models\PSI\StockMove(), 'orders' => new App\Models\Order(), 'payments' => new App\Models\Payment(), ]; foreach ($seed as $table => $rows) { if (!isset($map[$table])) { $out("⊘ 跳过未映射表 {$table}(无对应 Model)"); continue; } $m = $map[$table]; foreach ($rows as $r) { $m->insert($r); } $out("✓ 插入 {$table} (" . count($rows) . " 条)"); } } // 生成主题 CSS Core\Theme::regenerate(); $out("✓ 主题样式 theme.css 已生成"); // 写安装锁(Web 模式自动写;CLI 不写,便于二次部署时手动控制) if (!$cli) { @file_put_contents($lock, date('Y-m-d H:i:s') . " installed by " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown') . "\n"); $out("✓ 已写入 installed.lock(再次运行 install 将被拒绝)"); } $out(""); $out($cli ? "安装完成 ✓" : "安装完成 ✓ ");