From 7ec3ea559e8aa5bb07c6b3ee7e90f400abf291bc Mon Sep 17 00:00:00 2001 From: laoyang Date: Mon, 28 Sep 2026 20:55:16 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=90=91=E5=AF=BC=EF=BC=9A?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E9=83=A8=E7=BD=B2=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E5=BC=95=E5=AF=BC=E5=BB=BA=E5=BA=93=E5=BB=BA=E8=A1=A8=EF=BC=88?= =?UTF-8?q?=E6=89=B9=2026=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 install.php 独立安装向导:环境检查 → 表单(站点标题/家庭名/数据库/ 高德/管理员)→ 全量建表 → 基本种子数据 → 生成 config.php → 自动自删。 已安装判定以 config.php 为准,重复访问只显示「已安装」。 - 新增 sql/schema.sql 全量建表脚本(15 张表,按外键依赖排序,全部 IF NOT EXISTS),由向导执行;tools/migrate.php 继续负责增量迁移。 - index.php 加未安装检测:config.php 缺失或无效时跳转安装页。 - layout.php 标题回退值接通 app.name,安装时填的网站标题在页面上生效。 - 种子数据仅基本款:节点类型 5、角色标签 4、出行方式 9(不含需绑定 车辆的自驾两类);管理员账号按既有加密方式写入。 - README 部署章节改为向导优先,手动方式降为备选。 - 验证:本地 60 文件 php -l 全过;渲染检查 386 项全过; 安装流程端到端 46 项全过(临时副本 + 独立 MariaDB:跳转、表单、 建表、种子、加密可逆、自删、已安装拒绝、错误路径)。 --- README.md | 69 +++--- index.php | 17 ++ install.php | 529 +++++++++++++++++++++++++++++++++++++++++++ sql/schema.sql | 267 ++++++++++++++++++++++ templates/layout.php | 2 +- 5 files changed, 853 insertions(+), 31 deletions(-) create mode 100644 install.php create mode 100644 sql/schema.sql diff --git a/README.md b/README.md index 8642f92..01f6729 100644 --- a/README.md +++ b/README.md @@ -68,52 +68,61 @@ uploads/ 用户上传内容(不入库) ### 1. 环境要求 -- PHP **8.x**(开发与线上均在 8.1 与 7.4 双版本下做过语法检查与冒烟) -- MySQL 5.7+ / 8.0,字符集 `utf8mb4` -- PHP 扩展:`pdo_mysql`、`curl`、`gd`、`mbstring`、`json` +- PHP **7.4+**(推荐 8.x;开发与线上均在 8.1 与 7.4 双版本下做过语法检查与冒烟) +- MySQL 5.7+ / 8.0(MariaDB 亦可),字符集 `utf8mb4` +- PHP 扩展:`pdo_mysql`、`curl`、`gd`、`mbstring`、`json`、`openssl` +- 站点根目录对 Web 进程**可写**(安装时生成 `config.php` 用;`uploads/` 始终需要可写) -### 2. 取代码与配置 +### 2. 安装向导(推荐) + +1. 把代码放入站点目录,确保站点根对 Web 进程可写; +2. 在 MySQL 里创建一个**空的**数据库(表结构由安装向导自动生成): + + ```sql + CREATE DATABASE trip DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + -- 再创建专用账号并授予 trip 库全部权限(推荐,不要用 root) + ``` + +3. 浏览器访问站点根 —— 未安装时 `index.php` 会自动跳转到 `install.php` 安装向导; +4. 按表单填写:网站标题、家庭名称、数据库连接、高德地图 Key(可留空稍后补)、 + 管理员账号(手机号 + 密码); +5. 提交后向导会:全量建表(`sql/schema.sql`)→ 执行增量迁移(`src/Migrate.php`)→ + 写入基本数据(你的家庭、管理员账号、节点类型、角色标签、常用出行方式)→ + 生成 `config.php` → **自动删除 `install.php`**。 + +安装完成后即可用管理员账号登录。「已安装」的判定以 `config.php` 为准: +即使 `install.php` 没有被成功删除,再次访问也只会看到「已安装」提示,无法重新安装。 + +> 种子的出行方式不含「自驾」两种 —— 自驾方式必须关联具体车辆, +> 请先在「车辆管理」里添加车辆,再自行添加自驾出行方式。 + +### 3. 手动安装(备选) + +不想用向导时,也可以完全手动: ```bash git clone triplog cd triplog -cp config.example.php config.php +cp config.example.php config.php # 手工填写数据库与高德参数 +php tools/migrate.php # 不够:首次还需要全量建表,见下 ``` -编辑 `config.php` 填数据库与高德参数。**`config.php` 不入库**(含口令),只提交模板。 +全量建表:把 `sql/schema.sql` 导入空库(`mysql trip < sql/schema.sql`), +再执行 `php tools/migrate.php` 补齐增量迁移。管理员账号目前需自行向 `families`、 +`users` 插入(密码用 `password_hash()`,手机号存 `phone_hash`(SHA-256)与 +`phone_encrypted`(AES-256-CBC)),**推荐直接使用安装向导**。 -所有配置项都支持环境变量覆盖(`DB_HOST` / `DB_NAME` / `AMAP_NAV_SRC` 等), -便于在不同机器上跑同一份代码。 - -### 3. 数据库 - -创建库与账号(示例): - -```sql -CREATE DATABASE trip DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE USER 'trip'@'localhost' IDENTIFIED BY 'your-password'; -GRANT ALL PRIVILEGES ON trip.* TO 'trip'@'localhost'; -``` - -执行结构迁移(**幂等,可反复跑**): - -```bash -php tools/migrate.php # 执行迁移 -php tools/migrate.php --status # 只看当前版本,不改动 -``` - -该入口**只在命令行可用**,被 HTTP 访问时直接返回 404。 +`config.php` 不入库(含口令),只提交模板 `config.example.php`。 +所有配置项都支持环境变量覆盖(`DB_HOST` / `DB_NAME` / `AMAP_NAV_SRC` 等)。 ### 4. Web 服务器 把站点根指向项目根目录。Apache 需允许 `.htaccess`(`uploads/` 下有一条禁止执行脚本的规则, 不可删除);Nginx 请自行配置等价规则,并注意 `uploads/` 内不得解析 PHP。 -确保 `uploads/` 对 Web 进程可写。 - ### 5. 高德地图 -在 `config.php` 的 `amap` 段填写: +在 `config.php` 的 `amap` 段填写(安装向导里填过的可跳过): - `key` —— 服务端接口用 - `security_js` —— 前端 JS API 2.0 的安全密钥 diff --git a/index.php b/index.php index 9c30dc0..9599307 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,23 @@ ='), '当前 ' . PHP_VERSION]; + foreach ($exts as $ext) { + $rows[] = ["扩展 {$ext}", extension_loaded($ext), extension_loaded($ext) ? '已加载' : '未安装']; + } + $rows[] = ['站点根目录可写(用于生成 config.php)', is_writable($root), is_writable($root) ? '可写' : '不可写']; + $rows[] = ['uploads/ 目录可写(用于存放照片)', is_writable($root . '/uploads'), is_writable($root . '/uploads') ? '可写' : '不可写']; + $rows[] = ['sql/schema.sql 存在', is_file($root . '/sql/schema.sql'), is_file($root . '/sql/schema.sql') ? '存在' : '缺失']; + return $rows; +} + +/** 把 schema.sql 拆成可逐条执行的 SQL(剥离注释行、过滤空段) */ +function schema_statements(string $root): array +{ + $raw = (string)file_get_contents($root . '/sql/schema.sql'); + $parts = preg_split('/;\s*[\r\n]+/', $raw) ?: []; + $out = []; + foreach ($parts as $part) { + // 去掉每段里以 -- 开头的注释行 + $lines = preg_grep('/^\s*--/', explode("\n", $part), PREG_GREP_INVERT) ?: []; + $stmt = trim(implode("\n", $lines)); + if ($stmt !== '' && stripos($stmt, 'CREATE TABLE') === 0) { + $out[] = $stmt . ';'; + } + } + return $out; +} + +// --------------------------------------------------------------------------- +// 2) 已安装:直接拒绝 +// --------------------------------------------------------------------------- +if ($installed) { + ?> + + TripLog 安装 + +
+
✅
+

TripLog 已安装

+

+ 检测到站点已完成安装(config.php 已存在)。
+ 为安全起见,安装向导已停用。
+ 如果 install.php 仍在站点目录中,请手动删除它。 +

+ 进入首页 +
'', + 'family_name' => '我的家庭', + 'db_host' => '127.0.0.1', + 'db_port' => '3306', + 'db_name' => '', + 'db_user' => '', + 'db_pass' => '', + 'amap_key' => '', + 'amap_sec' => '', + 'amap_nav_src' => 'trip.example.com', + 'admin_name' => '', + 'admin_phone' => '', +]; + +$done = false; +$removed = false; // install.php 是否自删成功 + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $token = (string)($_POST['_csrf'] ?? ''); + if (!hash_equals($_SESSION['install_csrf'], $token)) { + $errors[] = '页面已过期,请刷新后重试。'; + } else { + // ---- 收集(密码类不回填,单独处理)---- + foreach ($values as $k => $_) { + if (isset($_POST[$k]) && is_string($_POST[$k])) { + $values[$k] = trim($_POST[$k]); + } + } + $adminPass = (string)($_POST['admin_pass'] ?? ''); + $adminPass2 = (string)($_POST['admin_pass2'] ?? ''); + $dbPass = $values['db_pass']; + + // ---- 校验 ---- + if ($values['site_name'] === '') { + $errors[] = '网站标题不能为空'; + } elseif (mb_strlen($values['site_name']) > 50) { + $errors[] = '网站标题不能超过 50 个字符'; + } + if ($values['family_name'] === '') { + $values['family_name'] = '我的家庭'; + } elseif (mb_strlen($values['family_name']) > 30) { + $errors[] = '家庭名称不能超过 30 个字符'; + } + if (!preg_match('/^\d{1,5}$/', $values['db_port']) || (int)$values['db_port'] < 1 || (int)$values['db_port'] > 65535) { + $errors[] = '数据库端口不正确'; + } + if (!preg_match('/^[A-Za-z0-9_]+$/', $values['db_name'])) { + $errors[] = '数据库名只能包含字母、数字和下划线'; + } + if ($values['db_user'] === '') { + $errors[] = '数据库用户名不能为空'; + } + if ($values['admin_name'] === '') { + $errors[] = '管理员姓名不能为空'; + } + if (!preg_match('/^1[3-9]\d{9}$/', $values['admin_phone'])) { + $errors[] = '管理员手机号格式不正确(11 位,1 开头)'; + } + if (strlen($adminPass) < 6) { + $errors[] = '管理员密码至少 6 位'; + } + if ($adminPass !== $adminPass2) { + $errors[] = '两次输入的管理员密码不一致'; + } + if ($values['amap_nav_src'] === '') { + $values['amap_nav_src'] = 'trip.example.com'; + } + + // ---- 执行安装 ---- + if (!$errors) { + $pdo = null; + try { + $dsn = sprintf( + 'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', + $values['db_host'], (int)$values['db_port'], $values['db_name'] + ); + $pdo = new PDO($dsn, $values['db_user'], $dbPass, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]); + } catch (PDOException $ex) { + $code = (int)$ex->getCode(); + $msg = $ex->getMessage(); + if (strpos($msg, '1049') !== false || $code === 1049 + || strpos($msg, '1044') !== false || $code === 1044) { + // MySQL 8 对不存在的库报 1049,MariaDB 报 1044(Access denied)—— + // 对用户来说两种情况都是「库没建好或账号不对」,合并成一条可操作的提示 + $errors[] = '数据库不存在,或该用户无权访问「' . $values['db_name'] . '」:请先在 MySQL 里创建名为「' + . $values['db_name'] . '」的空数据库(CREATE DATABASE ' . $values['db_name'] + . ' DEFAULT CHARACTER SET utf8mb4;)并把它的全部权限授予刚才填写的用户,再回到本页重试。'; + } elseif (strpos($msg, '1045') !== false || $code === 1045) { + $errors[] = '无法连接数据库主机「' . $values['db_host'] . '」,请确认主机地址与端口。'; + } else { + $errors[] = '数据库连接失败:' . $msg; + } + } + + if ($pdo) { + try { + // ---- 建表(schema.sql 全量 + Migrate 增量,均幂等)---- + $stmts = schema_statements($ROOT); + if (count($stmts) < 10) { + throw new RuntimeException('sql/schema.sql 内容异常(仅解析出 ' . count($stmts) . ' 条建表语句)'); + } + foreach ($stmts as $stmt) { + $pdo->exec($stmt); + } + require $ROOT . '/src/Migrate.php'; + \App\Migrate::run($pdo); + + // ---- 种子数据(一个事务;均只在空表时插入,重复执行不重复)---- + $pdo->beginTransaction(); + $familyName = $values['family_name']; + $pdo->prepare('INSERT INTO families (name) VALUES (?)')->execute([$familyName]); + $familyId = (int)$pdo->lastInsertId(); + + $pdo->prepare('INSERT INTO users + (family_id, name, phone_encrypted, phone_hash, password_hash, role, status) + VALUES (?, ?, ?, ?, ?, ?, ?)') + ->execute([ + $familyId, + $values['admin_name'], + install_encrypt($values['admin_phone']), + hash('sha256', $values['admin_phone']), + password_hash($adminPass, PASSWORD_DEFAULT), + 'admin', + 'active', + ]); + + // 节点类型(内置,不可删除) + $insType = $pdo->prepare('INSERT IGNORE INTO node_types (family_id, name, icon, is_system) VALUES (?, ?, ?, 1)'); + foreach ([ + ['游玩', '🎡'], ['用餐', '🍜'], ['住宿', '🏨'], ['赶路', '🚄'], ['休息', '☕'], + ] as $t) { + $insType->execute([$familyId, $t[0], $t[1]]); + } + + // 角色标签 + $insTag = $pdo->prepare('INSERT IGNORE INTO role_tags (family_id, name, icon) VALUES (?, ?, ?)'); + foreach ([ + ['司机', '🚗'], ['摄影担当', '📷'], ['向导', '🗺️'], ['财务', '💰'], + ] as $t) { + $insTag->execute([$familyId, $t[0], $t[1]]); + } + + // 出行方式:不含自驾两种(自驾需先在「车辆管理」里添加车辆, + // travel_modes.vehicle_id 外键指向车辆,空挂没有意义) + $insMode = $pdo->prepare('INSERT IGNORE INTO travel_modes (family_id, mode_type) VALUES (?, ?)'); + foreach (['train', 'plane', 'bus', 'subway', 'taxi', 'bike', 'walk', 'ship', 'other'] as $m) { + $insMode->execute([$familyId, $m]); + } + + $pdo->commit(); + + // ---- 生成 config.php(临时文件 → include 验证 → rename)---- + $configArr = [ + 'db' => [ + 'host' => $values['db_host'], + 'port' => (int)$values['db_port'], + 'name' => $values['db_name'], + 'user' => $values['db_user'], + 'pass' => $dbPass, + 'charset' => 'utf8mb4', + ], + 'app' => [ + 'name' => $values['site_name'], + 'timezone' => 'Asia/Shanghai', + ], + 'amap' => [ + 'key' => $values['amap_key'], + 'security_js' => $values['amap_sec'], + 'nav_src' => $values['amap_nav_src'], + ], + ]; + $php = "inTransaction()) { + $pdo->rollBack(); + } + $errors[] = $ex instanceof PDOException + ? '安装执行失败:' . $ex->getMessage() + : $ex->getMessage(); + } + } + } + } +} + +// --------------------------------------------------------------------------- +// 4) 结果页 / 表单页 +// --------------------------------------------------------------------------- +$checks = env_checks($ROOT); +$envOk = true; +foreach ($checks as $c) { + if (!$c[1]) { $envOk = false; break; } +} +?> + + + + + + +TripLog 安装向导 + + + +
+
+ +

TripLog 安装向导

+

家庭旅行日记 · 首次部署配置

+
+ + +
+
+
🎉
+

安装完成!

+

+ 数据表已创建,基本数据已就绪(节点类型、角色标签、出行方式)。
+ 请使用刚才设置的管理员手机号和密码登录。
+ + ⚠️ 安装文件 install.php 未能自动删除,请手动删除它。
+ +

+ 开始使用 +
+
+ + + + +
+ 安装未完成,请修正以下问题:
+ ·
+
+ + +
+

环境检查

+ +
+ + + +
+ +
+ +
+ + +
+

站点信息

+
+ + +
+
+ + +
所有成员、行程、花销都归属这个家庭;之后还可以在后台调整。
+
+
+ +
+

数据库(MySQL)

+
+ 请先在 MySQL 中创建一个空的数据库,表结构由安装向导自动生成。 +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+

高德开放平台(可稍后在 config.php 中补充)

+
+ 用于地图展示与地点搜索。lbs.amap.com + → 控制台 → 应用管理 → 创建应用(Web端(JS API) + Web服务)后填写。 +
+
+ + +
+
+ + +
+
+ + +
仅用于高德侧的调用来源统计,填任意英数字串均可,通常填你的域名。
+
+
+ +
+

管理员账号

+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + diff --git a/sql/schema.sql b/sql/schema.sql new file mode 100644 index 0000000..5224c1a --- /dev/null +++ b/sql/schema.sql @@ -0,0 +1,267 @@ +-- ============================================================================ +-- TripLog 全量建表脚本(安装向导使用) +-- +-- 来源:2026-09-28 从生产库 SHOW CREATE TABLE 导出后整理: +-- * 去掉导出文件里的 AUTO_INCREMENT=N 当前值; +-- * 全部 CREATE TABLE 加 IF NOT EXISTS(重复执行不破坏已有库); +-- * 按「外键依赖」排好建表顺序:families → users → 字典表 → 业务表。 +-- +-- 由 install.php(Web 安装向导)在安装时执行;tools/migrate.php 只负责 +-- 之后的**增量**迁移(见 src/Migrate.php),两者幂等、互不冲突。 +-- ============================================================================ + +-- ---- 家庭空间(一切数据的根:users / 字典表都挂在 family 下)---- +CREATE TABLE IF NOT EXISTS `families` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '家庭名称', + `default_home_location` json DEFAULT NULL COMMENT '默认家地址 {name, address, lat, lng}', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='家庭空间'; + +-- ---- 用户表 ---- +CREATE TABLE IF NOT EXISTS `users` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `family_id` bigint NOT NULL COMMENT '所属家庭', + `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名', + `phone_encrypted` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AES加密手机号', + `phone_hash` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号SHA-256哈希,用于登录查找', + `id_card_encrypted` varchar(300) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'AES加密身份证号', + `password_hash` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码哈希,仅admin/user有', + `role` enum('admin','user','guest') COLLATE utf8mb4_unicode_ci DEFAULT 'guest' COMMENT '角色', + `avatar` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像URL', + `status` enum('active','inactive') COLLATE utf8mb4_unicode_ci DEFAULT 'active' COMMENT '状态', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_family_role` (`family_id`,`role`), + KEY `idx_phone_hash` (`phone_hash`), + CONSTRAINT `fk_users_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- ---- 节点类型库 ---- +CREATE TABLE IF NOT EXISTS `node_types` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `family_id` bigint NOT NULL, + `name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '游玩/用餐/赶路/休息/突发', + `icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标/表情', + `is_system` tinyint(1) DEFAULT '0' COMMENT '内置类型不可删除', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_family_name` (`family_id`,`name`), + CONSTRAINT `fk_node_types_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点类型库'; + +-- ---- 角色标签库 ---- +CREATE TABLE IF NOT EXISTS `role_tags` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `family_id` bigint NOT NULL, + `name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名:司机/摄影担当', + `icon` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '图标/表情', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_family_name` (`family_id`,`name`), + CONSTRAINT `fk_role_tags_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签库'; + +-- ---- 车辆库 ---- +CREATE TABLE IF NOT EXISTS `vehicles` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `family_id` bigint NOT NULL, + `plate_number` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '车牌号', + `model_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '车型名称', + `vehicle_type` enum('own','rental') COLLATE utf8mb4_unicode_ci DEFAULT 'own' COMMENT '自有/租车', + `rental_company` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '租车公司', + `rental_order_no` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '租车订单号', + `is_default` tinyint(1) DEFAULT '0' COMMENT '是否为默认自有车', + `notes` text COLLATE utf8mb4_unicode_ci COMMENT '备注', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_family_type` (`family_id`,`vehicle_type`), + CONSTRAINT `fk_vehicles_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='车辆库'; + +-- ---- 出行方式 ---- +CREATE TABLE IF NOT EXISTS `travel_modes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `family_id` bigint NOT NULL, + `mode_type` enum('drive_own','drive_rental','train','plane','bus','subway','taxi','bike','walk','ship','other') COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '交通类型', + `vehicle_id` bigint DEFAULT NULL COMMENT '仅drive_own/drive_rental时关联vehicles', + `notes` text COLLATE utf8mb4_unicode_ci COMMENT '备注(延误等)', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_family_type` (`family_id`,`mode_type`), + KEY `fk_travel_modes_vehicle` (`vehicle_id`), + CONSTRAINT `fk_travel_modes_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_travel_modes_vehicle` FOREIGN KEY (`vehicle_id`) REFERENCES `vehicles` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='出行方式'; + +-- ---- 活动容器 ---- +CREATE TABLE IF NOT EXISTS `activities` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `family_id` bigint NOT NULL, + `title` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '活动主题', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '活动简介/前言', + `cover_image` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '封面图URL', + `created_by` bigint NOT NULL COMMENT '创建者user_id', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_family_created` (`family_id`,`created_at`), + KEY `fk_activities_creator` (`created_by`), + CONSTRAINT `fk_activities_creator` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`), + CONSTRAINT `fk_activities_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动容器'; + +-- ---- 活动级人员名单 ---- +CREATE TABLE IF NOT EXISTS `activity_members` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `activity_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `permission` enum('owner','editor','viewer') COLLATE utf8mb4_unicode_ci DEFAULT 'viewer' COMMENT '权限', + `joined_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间', + `left_at` datetime DEFAULT NULL COMMENT '离开时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_activity_user` (`activity_id`,`user_id`), + KEY `idx_activity` (`activity_id`), + KEY `fk_activity_members_user` (`user_id`), + CONSTRAINT `fk_activity_members_activity` FOREIGN KEY (`activity_id`) REFERENCES `activities` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_activity_members_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='活动级人员名单'; + +-- ---- 行程节点(计划/执行双轨)---- +CREATE TABLE IF NOT EXISTS `trip_nodes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `activity_id` bigint NOT NULL, + `node_type_id` bigint NOT NULL COMMENT '关联node_types', + `node_category` enum('plan','reality') COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'plan=计划, reality=执行', + `original_plan_node_id` bigint DEFAULT NULL COMMENT '执行节点克隆自哪个计划节点(空=临时新增)', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime NOT NULL COMMENT '结束时间', + `duration_minutes` int DEFAULT NULL COMMENT '持续分钟数', + `start_location` json DEFAULT NULL COMMENT '位置信息', + `end_location` json DEFAULT NULL COMMENT '位置信息', + `title` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '节点标题', + `description` text COLLATE utf8mb4_unicode_ci COMMENT '详细文字描述', + `vehicle_id` bigint DEFAULT NULL COMMENT '关联vehicles(仅自驾)', + `travel_mode_id` bigint DEFAULT NULL COMMENT '关联travel_modes(赶路节点必填)', + `status` enum('planned','ongoing','completed','cancelled') COLLATE utf8mb4_unicode_ci DEFAULT 'planned', + `cancel_reason` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '取消原因', + `sort_order` int DEFAULT '0' COMMENT '手动排序', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `line_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '线路名称(如京沪高铁、地铁10号线)', + `carrier` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '承运公司(如中国铁路、滴滴)', + `trip_number` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '车次/航班号/线路号', + `seat_info` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '座位号/车厢', + `gate` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '登机口/站台', + PRIMARY KEY (`id`), + KEY `idx_activity_category` (`activity_id`,`node_category`,`start_time`), + KEY `idx_original` (`original_plan_node_id`), + KEY `idx_travel_mode` (`travel_mode_id`), + KEY `fk_trip_nodes_type` (`node_type_id`), + KEY `fk_trip_nodes_vehicle` (`vehicle_id`), + CONSTRAINT `fk_trip_nodes_activity` FOREIGN KEY (`activity_id`) REFERENCES `activities` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_trip_nodes_original` FOREIGN KEY (`original_plan_node_id`) REFERENCES `trip_nodes` (`id`) ON DELETE SET NULL, + CONSTRAINT `fk_trip_nodes_travel` FOREIGN KEY (`travel_mode_id`) REFERENCES `travel_modes` (`id`) ON DELETE SET NULL, + CONSTRAINT `fk_trip_nodes_type` FOREIGN KEY (`node_type_id`) REFERENCES `node_types` (`id`), + CONSTRAINT `fk_trip_nodes_vehicle` FOREIGN KEY (`vehicle_id`) REFERENCES `vehicles` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行程节点(计划/执行双轨)'; + +-- ---- 节点级参与人员 ---- +CREATE TABLE IF NOT EXISTS `node_members` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `trip_node_id` bigint NOT NULL, + `user_id` bigint NOT NULL, + `role_tag_ids` json DEFAULT NULL COMMENT '[12, 34] 角色ID数组', + `emotion` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '情绪表情(仅执行节点)', + `check_in_time` datetime DEFAULT NULL COMMENT '签到时间', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_node_user` (`trip_node_id`,`user_id`), + KEY `idx_node` (`trip_node_id`), + KEY `fk_node_members_user` (`user_id`), + CONSTRAINT `fk_node_members_node` FOREIGN KEY (`trip_node_id`) REFERENCES `trip_nodes` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_node_members_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='节点级参与人员'; + +-- ---- 花销记录 ---- +CREATE TABLE IF NOT EXISTS `expenses` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `trip_node_id` bigint NOT NULL, + `payer_user_id` bigint NOT NULL COMMENT '付款人', + `currency` varchar(10) COLLATE utf8mb4_unicode_ci DEFAULT 'CNY' COMMENT '币种', + `total_amount` decimal(12,2) NOT NULL COMMENT '总金额', + `category` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '餐饮/门票/交通/购物/其他', + `description` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '消费描述', + `is_budget` tinyint(1) DEFAULT '0' COMMENT 'TRUE=预算, FALSE=实际', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_node_budget` (`trip_node_id`,`is_budget`), + KEY `fk_expenses_payer` (`payer_user_id`), + CONSTRAINT `fk_expenses_node` FOREIGN KEY (`trip_node_id`) REFERENCES `trip_nodes` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_expenses_payer` FOREIGN KEY (`payer_user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花销记录'; + +-- ---- 花销分摊明细 ---- +CREATE TABLE IF NOT EXISTS `expense_splits` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `expense_id` bigint NOT NULL, + `user_id` bigint NOT NULL COMMENT '分摊对象(为谁花的)', + `share_amount` decimal(12,2) NOT NULL COMMENT '承担金额', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_expense` (`expense_id`), + KEY `fk_expense_splits_user` (`user_id`), + CONSTRAINT `fk_expense_splits_expense` FOREIGN KEY (`expense_id`) REFERENCES `expenses` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_expense_splits_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='花销分摊明细'; + +-- ---- 评论表 ---- +CREATE TABLE IF NOT EXISTS `comments` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `author_id` bigint NOT NULL, + `target_type` enum('activity','trip_node') COLLATE utf8mb4_unicode_ci NOT NULL, + `target_id` bigint NOT NULL, + `parent_id` bigint DEFAULT NULL COMMENT '父评论ID(楼中楼)', + `content` text COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_target` (`target_type`,`target_id`,`created_at`), + KEY `fk_comments_author` (`author_id`), + KEY `fk_comments_parent` (`parent_id`), + CONSTRAINT `fk_comments_author` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`), + CONSTRAINT `fk_comments_parent` FOREIGN KEY (`parent_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='评论表'; + +-- ---- 统一附件表 ---- +CREATE TABLE IF NOT EXISTS `attachments` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `target_type` enum('activity','trip_node','comment') COLLATE utf8mb4_unicode_ci NOT NULL, + `target_id` bigint NOT NULL COMMENT '对应表的ID', + `uploader_id` bigint NOT NULL, + `file_url` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL, + `file_name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `sort_order` int DEFAULT '0', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `storage` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'local' COMMENT 'local|oss', + `object_key` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'OSS 对象键,或本地相对路径', + `mime_type` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `size_bytes` bigint DEFAULT NULL, + `width` int DEFAULT NULL COMMENT '图片宽(非图片为空)', + `height` int DEFAULT NULL COMMENT '图片高(非图片为空)', + `is_image` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `idx_target` (`target_type`,`target_id`), + KEY `fk_attachments_uploader` (`uploader_id`), + KEY `idx_storage` (`storage`), + CONSTRAINT `fk_attachments_uploader` FOREIGN KEY (`uploader_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统一附件表'; + +-- ---- 站点级键值设置 ---- +CREATE TABLE IF NOT EXISTS `app_settings` ( + `setting_key` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL, + `setting_value` text COLLATE utf8mb4_unicode_ci, + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`setting_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='站点级键值设置'; diff --git a/templates/layout.php b/templates/layout.php index 83d708c..4a4a6d1 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -14,7 +14,7 @@ $__initial = $__name !== '' ? mb_substr($__name, 0, 1) : '·'; - <?= e($pageTitle ?? 'TripLog 家庭旅行日记') ?> + <?= e($pageTitle ?? ((string)(app_config()['app']['name'] ?? '') ?: 'TripLog 家庭旅行日记')) ?>