183 lines
9.8 KiB
PHP
183 lines
9.8 KiB
PHP
<?php
|
||
/**
|
||
* 安装 / 初始化脚本
|
||
* 用法:
|
||
* CLI: php install/install.php
|
||
* Web: 浏览器访问 /install/install.php
|
||
* 根据 config 中 app.driver 写入 文件(JSON) 或 MySQL,并生成 theme.css。
|
||
*/
|
||
define('BASE_PATH', dirname(__DIR__));
|
||
require BASE_PATH . '/app/Core/App.php';
|
||
Core\App::init();
|
||
|
||
$cli = PHP_SAPI === 'cli';
|
||
$out = function (string $msg) use ($cli) {
|
||
echo $cli ? $msg . "\n" : "<p>" . htmlspecialchars($msg) . "</p>\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 ? "安装完成 ✓" : "安装完成 ✓ <p><a href='" . site_url() . "'>访问前台</a> | <a href='" . site_url('admin') . "'>进入后台</a></p>");
|