文件还在测试中

This commit is contained in:
2026-08-08 15:53:53 +08:00
parent 5f1e7adb64
commit 8101177cb5
241 changed files with 18074 additions and 22 deletions
+365
View File
@@ -0,0 +1,365 @@
<?php
namespace Core;
/**
* 安装 / 数据升级 核心逻辑(供 install.php、后台「数据升级」与「数据库初始化」共用)
*
* 设计原则:
* - install() 首次安装:建全部表 + 插种子(可指定自定义超级管理员),不破坏已有数据。
* - upgrade() 增量升级:补齐新增模块表/列 + 按 id 补齐缺失种子,绝不 DELETE / 覆盖客户数据。
* - 所有 DDL 均为 IF NOT EXISTS / ADD COLUMN IF NOT EXISTS 幂等写法。
*/
class Installer
{
/** 完整安装(首次)。$super 非空时用自定义超级管理员覆盖种子账号。 */
public static function install(?array $super = null): array
{
$seed = require BASE_PATH . '/install/seed.php';
if ($super) {
$seed['admin_users'] = [[
'id' => 1,
'username' => $super['username'],
'password' => password_hash($super['password'], PASSWORD_DEFAULT),
'name' => $super['name'] ?? '超级管理员',
'role' => 'super_admin',
'crm_role' => 'admin',
'psi_role' => 'admin',
'status' => 1,
'created_at' => date('Y-m-d'),
]];
}
$driver = Db::driver();
$msgs = [];
if ($driver === 'file') {
$dir = Db::fileDir();
foreach ($seed as $table => $rows) {
$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));
$msgs[] = "写入 {$table}.json (" . count($rows) . " 条)";
}
foreach (['crm_contacts'] as $t) {
$f = $dir . "/{$t}.json";
if (!is_file($f)) { file_put_contents($f, '[]'); $msgs[] = "创建 {$t}.json"; }
}
} else {
$pdo = Db::pdo();
$pdo->exec(file_get_contents(BASE_PATH . '/install/schema.sql'));
$msgs[] = "数据表已创建/更新(基础 + CRM + PSI";
foreach (['categories', 'products', 'news', 'cases'] as $t) {
try { $pdo->exec("ALTER TABLE `{$t}` ADD COLUMN `layout` TEXT"); } catch (\Throwable $e) {}
}
self::ensureColumns($pdo, $msgs);
$map = self::modelMap();
foreach ($seed as $table => $rows) {
$m = $map[$table] ?? null;
if (!$m) continue;
foreach ($rows as $r) { $m->insert($r); }
$msgs[] = "插入 {$table} (" . count($rows) . " 条)";
}
}
try { Theme::regenerate(); $msgs[] = "主题样式 theme.css 已生成"; } catch (\Throwable $e) {}
@file_put_contents(BASE_PATH . '/storage/installed.lock', date('Y-m-d H:i:s') . " installed\n");
return $msgs;
}
/** 数据升级(后台按钮 / 已安装系统):补齐新模块表/列,按 id 补齐缺失种子,保留客户数据 */
public static function upgrade(): array
{
$seed = require BASE_PATH . '/install/seed.php';
$driver = Db::driver();
$msgs = [];
$content = [
'categories', 'products', 'news', 'cases', 'pages', 'banners', 'settings',
'crm_customers', 'crm_leads', 'crm_followups', 'crm_contacts',
'psi_materials', 'psi_products', 'psi_suppliers', 'psi_purchases', 'psi_sales',
];
if ($driver === 'file') {
$dir = Db::fileDir();
foreach ($seed as $table => $rows) {
if (!in_array($table, $content, true)) continue;
$ef = $dir . "/{$table}.json";
$existing = [];
if (is_file($ef)) {
$ed = @json_decode(file_get_contents($ef), true);
if (is_array($ed)) $existing = $ed;
}
if ($table === 'settings') {
$keys = [];
foreach ($existing as $er) { if (isset($er['skey'])) $keys[$er['skey']] = true; }
foreach ($rows as $r) { if (!isset($keys[$r['skey']])) $existing[] = $r; }
} else {
$ids = [];
foreach ($existing as $er) { if (isset($er['id'])) $ids[$er['id']] = true; }
foreach ($rows as $r) {
$rid = $r['id'] ?? null;
if ($rid !== null && !isset($ids[$rid])) $existing[] = $r;
}
}
file_put_contents($ef, json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
$msgs[] = "刷新 {$table}.json (" . count($existing) . " 条,保留客户数据)";
}
foreach (['orders', 'payments', 'crm_contacts'] as $t) {
$f = $dir . "/{$t}.json";
if (!is_file($f)) { file_put_contents($f, '[]'); $msgs[] = "创建 {$t}.json"; }
}
} else {
$pdo = Db::pdo();
$pdo->exec(file_get_contents(BASE_PATH . '/install/schema.sql'));
$msgs[] = "数据表已创建/更新(补齐新增模块表)";
foreach (['categories', 'products', 'news', 'cases'] as $t) {
try { $pdo->exec("ALTER TABLE `{$t}` ADD COLUMN `layout` TEXT"); } catch (\Throwable $e) {}
}
self::ensureColumns($pdo, $msgs);
$map = self::modelMap();
foreach ($seed as $table => $rows) {
if (!in_array($table, $content, true)) continue;
$m = $map[$table] ?? null;
if (!$m) continue;
if ($table === 'settings') {
$keys = [];
try { $rs = Db::query("SELECT skey FROM `settings`"); foreach ($rs->fetchAll() as $er) $keys[$er['skey']] = true; } catch (\Throwable $e) {}
$added = 0;
foreach ($rows as $r) { if (!isset($keys[$r['skey']])) { $m->insert($r); $added++; } }
$msgs[] = "补齐 settings (" . $added . " 条)";
continue;
}
$ids = [];
try { $rs = Db::query("SELECT id FROM `{$table}`"); foreach ($rs->fetchAll() as $er) $ids[$er['id']] = true; } catch (\Throwable $e) {}
$added = 0;
foreach ($rows as $r) {
$rid = $r['id'] ?? null;
if ($rid !== null && !isset($ids[$rid])) { $m->insert($r); $added++; }
}
$msgs[] = "补齐 {$table} 缺失种子 (" . $added . " 条,已有 " . count($ids) . " 条保留)";
}
}
try { Theme::regenerate(); $msgs[] = "主题样式 theme.css 已生成"; } catch (\Throwable $e) {}
return $msgs;
}
/** 执行单个 SQL 文件(用于「数据库升级」的升级包)。失败会抛出异常由调用方捕获。 */
public static function applySqlFile(string $path): void
{
if (!is_file($path)) {
throw new \RuntimeException("升级文件不存在:{$path}");
}
$sql = file_get_contents($path);
if ($sql === false || trim($sql) === '') return;
self::dbExecute($sql);
}
/**
* 分段执行 SQL(支持 DELIMITER 命令,兼容 PDO 不支持的客户端语法)。
* @param string $sql 原始 SQL 文本(含 / 不含 DELIMITER 均可)
* @throws \Throwable
*/
private static function dbExecute(string $sql): void
{
$pdo = Db::pdo();
// 逐行解析 DELIMITER 与多语句拆分
$lines = explode("\n", $sql);
$delimiter = ';';
$buffer = '';
foreach ($lines as $raw) {
$line = trim($raw);
// 跳过空行与单行注释(兼容 PHP 7,不用 str_starts_with
if ($line === '' || strpos($line, '--') === 0 || strpos($line, '#') === 0) continue;
// 检测 DELIMITER 命令(客户端命令,不进入 SQL 执行)
if (strtoupper(substr($line, 0, 10)) === 'DELIMITER ') {
// 积压的 SQL 遇到 DELIMITER 修改时先执行
$stmt = trim($buffer);
if ($stmt !== '') {
if ($pdo->exec($stmt) === false) {
$err = $pdo->errorInfo();
throw new \RuntimeException("SQL 执行错误:{$err[2]} (SQL: " . substr($stmt, 0, 120) . ')');
}
}
$buffer = '';
$delimiter = trim(substr($line, 10));
continue;
}
$buffer .= $raw . "\n";
// 检查 buffer 是否以当前分隔符结尾(忽略末尾空白与行末注释)
$trimmed = rtrim($buffer, " \t\r\n");
if (substr($trimmed, -strlen($delimiter)) === $delimiter) {
$stmt = rtrim(substr($trimmed, 0, -strlen($delimiter)));
$stmt = trim($stmt);
if ($stmt !== '') {
if ($pdo->exec($stmt) === false) {
$err = $pdo->errorInfo();
throw new \RuntimeException("SQL 执行错误:{$err[2]} (SQL: " . substr($stmt, 0, 120) . ')');
}
}
$buffer = '';
}
}
// 最后一段(无结束分隔符的纯 SQL)
$stmt = trim($buffer);
if ($stmt !== '') {
if ($pdo->exec($stmt) === false) {
$err = $pdo->errorInfo();
throw new \RuntimeException("SQL 执行错误:{$err[2]} (SQL: " . substr($stmt, 0, 120) . ')');
}
}
}
/** 确保升级记录表存在(幂等) */
public static function ensureUpgradeLog(): void
{
if (Db::driver() !== 'mysql') return;
try {
Db::pdo()->exec("CREATE TABLE IF NOT EXISTS db_upgrades (
id INT AUTO_INCREMENT PRIMARY KEY,
file VARCHAR(255) NOT NULL COMMENT '升级包文件名',
hash CHAR(32) NOT NULL COMMENT '文件 MD5,用于识别内容变更',
applied_at DATETIME NOT NULL COMMENT '执行时间',
applied_by VARCHAR(64) DEFAULT '' COMMENT '操作人',
note TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
} catch (\Throwable $e) {}
}
public static function isInstalled(): bool
{
if (Db::driver() !== 'mysql') return true; // file 模式无「安装」概念
try {
$cnt = (new \App\Models\AdminUser())->count();
return $cnt > 0;
} catch (\Throwable $e) {
return false;
}
}
/** 返回每张预期表的存在状态(mysql 模式) */
public static function tableStatus(): array
{
if (Db::driver() !== 'mysql') return [];
$pdo = Db::pdo();
$exist = $pdo->query("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE()")->fetchAll(\PDO::FETCH_COLUMN);
$all = [
'categories', 'products', 'news', 'cases', 'pages', 'banners', 'admin_users',
'crm_customers', 'crm_leads', 'crm_followups', 'crm_contacts',
'psi_materials', 'psi_products', 'psi_suppliers', 'psi_purchases', 'psi_sales', 'psi_stock_moves',
'settings', 'orders', 'payments',
];
$out = [];
foreach ($all as $t) { $out[$t] = in_array($t, $exist, true); }
return $out;
}
/** 补齐各表新增列(防御性,schema.sql 已含,此处兜底供「数据升级」使用,幂等) */
private static function ensureColumns($pdo, array &$msgs): void
{
$map = [
'pages' => [
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
],
'products' => [
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
],
'news' => [
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
],
'cases' => [
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
],
'categories' => [
'mode' => "VARCHAR(16) NOT NULL DEFAULT 'fixed'",
],
'admin_users' => [
'crm_role' => "VARCHAR(20) DEFAULT 'none'",
'psi_role' => "VARCHAR(20) DEFAULT 'none'",
'crm_perms' => "TEXT",
'psi_perms' => "TEXT",
],
'crm_customers' => [
'customer_no' => "VARCHAR(40) DEFAULT ''",
'industry' => "VARCHAR(20) DEFAULT ''",
'region' => "VARCHAR(40) DEFAULT ''",
'credit_limit'=> "DECIMAL(12,2) DEFAULT 0",
'status' => "VARCHAR(20) DEFAULT 'lead'",
],
'crm_leads' => [
'source' => "VARCHAR(30) DEFAULT ''",
'probability' => "TINYINT DEFAULT 0",
],
'crm_followups' => [
'way' => "VARCHAR(20) DEFAULT ''",
'result' => "VARCHAR(60) DEFAULT ''",
],
'psi_materials' => [
'composition' => "VARCHAR(60) DEFAULT ''",
'weight_gsm' => "DECIMAL(8,2) DEFAULT 0",
'width_cm' => "DECIMAL(8,2) DEFAULT 0",
'color' => "VARCHAR(40) DEFAULT ''",
'batch_no' => "VARCHAR(40) DEFAULT ''",
],
'psi_products' => [
'style_no' => "VARCHAR(40) DEFAULT ''",
'color' => "VARCHAR(40) DEFAULT ''",
'size_run' => "VARCHAR(60) DEFAULT ''",
'season' => "VARCHAR(20) DEFAULT ''",
'year' => "VARCHAR(10) DEFAULT ''",
],
'psi_suppliers' => [
'type' => "VARCHAR(20) DEFAULT ''",
'grade' => "VARCHAR(20) DEFAULT ''",
'ontime_rate'=> "DECIMAL(5,2) DEFAULT 0",
'qc_rate' => "DECIMAL(5,2) DEFAULT 0",
],
'psi_purchases' => [
'batch_no' => "VARCHAR(40) DEFAULT ''",
'expected_at' => "VARCHAR(20) DEFAULT ''",
],
'psi_sales' => [
'region' => "VARCHAR(40) DEFAULT ''",
'batch_no' => "VARCHAR(40) DEFAULT ''",
],
'psi_stock_moves' => [
'batch_no' => "VARCHAR(40) DEFAULT ''",
],
];
foreach ($map as $table => $cols) {
try {
$have = $pdo->query("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='{$table}'")->fetchAll(\PDO::FETCH_COLUMN);
} catch (\Throwable $e) {
continue;
}
foreach ($cols as $c => $def) {
if (!in_array($c, $have, true)) {
try {
$pdo->exec("ALTER TABLE `{$table}` ADD COLUMN `{$c}` {$def}");
$msgs[] = "已添加 {$table}.{$c}";
} catch (\Throwable $e) {}
}
}
}
}
private static function modelMap(): array
{
return [
'categories' => new \App\Models\Category(),
'products' => new \App\Models\Product(),
'news' => new \App\Models\News(),
'cases' => new \App\Models\CustomerCase(),
'pages' => new \App\Models\Page(),
'banners' => new \App\Models\Banner(),
'admin_users'=> new \App\Models\AdminUser(),
'settings' => new \App\Models\Setting(),
'orders' => new \App\Models\Order(),
'payments' => new \App\Models\Payment(),
'crm_customers' => new \App\Models\CRM\Customer(),
'crm_leads' => new \App\Models\CRM\Lead(),
'crm_followups' => new \App\Models\CRM\FollowUp(),
'crm_contacts' => new \App\Models\CRM\Contact(),
'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(),
];
}
}