v14: 观影/家谱/日程/旅程/资源/任务六模块统一,首页改 dashboard
用户 14 条需求一次交付(1-13 功能 + 14 工作方式)。 观影(1-4) - 返回文案全模块统一为「返回观影」(不再有「返回列表」「返回观影记录」三种叫法) - 观影人一律用彩色人物标签 person_chip:底色由头像主色算出来(上传时存库), 瀑布流卡片也按人显示;五处观影模板全部改用它 - 人员显示统一走 person_name()(shown → 我给他起的称呼 → 姓名) - 影院管理页与年度报告页的上下两块用 .card + .card 统一给间距(原来紧贴成一整块) 家谱(5) - 树根从「我」换成**辈分最高的祖先**:库画的是"树根的祖先链+后代+树根的兄弟姐妹", 根=我 时伯父/堂兄/嫂子/侄子一个都不画(12 人夹具只画出 8 人) - focusMap 按**代数**圈定三代(不再按步数),normalizeLinks 把子女对称挂到父母双方 —— 修掉"母亲登录看不到孩子" - 新增「看全部 / 回到我」两个按钮,放在画布外(画布整块被库接管了手势) 统一事件原语(6-9) - 旅程:TripLog 全量迁入(3 趟旅程 / 21 段行程 / 72 张照片 / 5 个人)。 复用 events(kind='trip') + trip_details + trip_nodes(计划/执行双轨保留) - 日程:每人可建日历本(自定义色)+ 共享只读/可写;观影与旅行是每人默认两本, 内容是**从各模块实时派生**的(不复制,所以改了观影日期日历立刻跟着变); 没有具体时分 = 全天;月视图参照群晖 Calendar - 资源:挂在某人名下的附属信息(标题+正文+多附件),可共享给多人并设只读/读写, 默认折叠、可搜标题与正文 - 任务:看板三列 + 两级子任务 + 评论@人 + 共享 + 循环(完成时按原截止日推下一期) - 统一附件表:四个模块的附件共用一张,下载地址不落库、一律由 media.php 现算 模块化(10-11) - App\Module 是模块的唯一来源(加一行 = 导航条/主题色/个人设置三处自动出现) - 个人设置里可勾选要显示哪些模块、拖动排序(关掉的模块**不会**被兜底补回来) - 每个模块一套主题色(家庭级),只覆盖 --brand 三兄弟,不碰纸墨底色 其他(12-13) - 新增成员表单可选所属分组(编辑表单没有:分组是"我和他的关系",只归我自己) - 首页改 dashboard:接下来的日子 / 未来七天 / 手头在做 / 往年今日。 家人卡片从首页撤走(看人去名册) 验证(全部通过) - e2e 542 条(新增 4 模块端到端 60+ 条) - 桌面几何 368 / 移动端几何 520 / 家谱 12 / 交互 97 / 老库升级 123 - SQL 列名静态校验 0 处可疑(对着真实表结构) - 反向验证 18 条新注入全部抓住 顺带修掉的既有问题 - 老库升级回填 SQL 按实际存在的列动态拼(缺列不再 1054 炸断升级) - 全新安装时 Schema::ensure 跑在"人还没建"之前 → 补建日历本三处齐备 - 窄屏导航横向溢出(模块从 4 个加到 8 个后顶栏撑破视口)
This commit is contained in:
+328
-5
@@ -49,8 +49,23 @@ final class Schema
|
||||
* MovieLog 模型:movie_reviews(每账号每片一条,1-10 分 + 正文 + 感受)
|
||||
* + movie_review_comments(两级平铺讨论),movie_details 旧的单条
|
||||
* rating/review/feelings 迁进 movie_reviews 后删列。
|
||||
* 10 → **豆瓣自动补充**:movie_details.douban_meta(JSON,元数据整体存一列)。
|
||||
* 11 → **头像主色**:persons.avatar_color(CHAR(7))。上传/换头像时算一次存库,
|
||||
* 观影人标签等处按每人主色画底色(用户 2026-10-01 第 2 条);
|
||||
* 老头像在升级时**回读一次**补算,补不到的按姓名兜底(AvatarColor)。
|
||||
* 12 → **四个新模块(用户 2026-10-01 第 6~10 条)**:
|
||||
* ① 旅程:events.kind 加 trip + trip_details(封面/行程小结)+ trip_nodes
|
||||
* (行程节点,计划/执行双轨)+ trip_node_members;
|
||||
* ② 日程:calendars(每人可建、自定义颜色、可共享只读/可写)
|
||||
* + calendar_events(自定义日程);观影/旅行是**每人默认的两本**,
|
||||
* 内容从 events 派生、不重复落库;
|
||||
* ③ 资源:resources(挂在某人名下)+ resource_shares(共享给谁、只读/读写);
|
||||
* ④ 任务:tasks(两级:parent_id)+ task_shares + task_comments(支持 @人)
|
||||
* + task_recur(灵活循环);
|
||||
* ⑤ 统一附件 attachments(旅程/节点/资源/任务/评论共用一张表);
|
||||
* ⑥ 模块导航与主题色走 user_settings / settings,不新建表。
|
||||
*/
|
||||
public const VERSION = '10';
|
||||
public const VERSION = '12';
|
||||
|
||||
/** @return string[] */
|
||||
public static function statements(): array
|
||||
@@ -323,6 +338,204 @@ CREATE TABLE IF NOT EXISTS `event_photos` (
|
||||
KEY `idx_ep_event` (`event_id`, `position`),
|
||||
CONSTRAINT `fk_ep_event` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='事件的现场照片(可多张)'
|
||||
SQL,
|
||||
// ---- 统一附件(v12)----
|
||||
// 旅程照片 / 资源附件 / 任务附件 / 评论附件,全都走这一张表:
|
||||
// 下载、预览、权限判断只写一次;将来再加模块也不用再造一张附件表。
|
||||
// ⚠️ 不做外键(target_type 决定指向哪张表,外键表达不了),
|
||||
// 只在 (target_type, target_id) 上建索引。删宿主时由各模块自己清。
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `attachments` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`family_id` INT UNSIGNED NOT NULL,
|
||||
`target_type` ENUM('trip','trip_node','resource','task','task_comment') NOT NULL COMMENT '挂在谁身上',
|
||||
`target_id` INT UNSIGNED NOT NULL COMMENT '宿主的 id',
|
||||
`key` VARCHAR(255) NOT NULL COMMENT '文件(存储相对键)',
|
||||
`name` VARCHAR(200) DEFAULT NULL COMMENT '原始文件名(下载时回给它)',
|
||||
`mime` VARCHAR(120) DEFAULT NULL COMMENT '真实 MIME(由服务端 finfo 判定,不信浏览器给的)',
|
||||
`size` BIGINT UNSIGNED DEFAULT NULL COMMENT '字节数',
|
||||
`is_image` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '1=图片,浏览器可直接预览',
|
||||
`position` INT NOT NULL DEFAULT 100,
|
||||
`uploader_id` INT UNSIGNED DEFAULT NULL COMMENT '谁传的(persons.id)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_att_target` (`target_type`, `target_id`, `position`),
|
||||
CONSTRAINT `fk_att_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_att_uploader` FOREIGN KEY (`uploader_id`) REFERENCES `persons` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统一附件:旅程/资源/任务/评论共用'
|
||||
SQL,
|
||||
// ---- 旅程(v12,TripLog 迁入)----
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `trip_details` (
|
||||
`event_id` INT UNSIGNED NOT NULL COMMENT '对应 events(kind="trip")',
|
||||
`cover_key` VARCHAR(255) DEFAULT NULL COMMENT '封面图(存储相对键)',
|
||||
`summary` TEXT DEFAULT NULL COMMENT '这趟旅程的一段话小结',
|
||||
PRIMARY KEY (`event_id`),
|
||||
CONSTRAINT `fk_tdet_event` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='旅程事件的特有数据(一对一卫星表)'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `trip_nodes` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`family_id` INT UNSIGNED NOT NULL,
|
||||
`event_id` INT UNSIGNED NOT NULL COMMENT '属于哪趟旅程',
|
||||
`category` ENUM('plan','reality') NOT NULL DEFAULT 'plan' COMMENT 'plan=计划 / reality=实际走过的',
|
||||
`node_type` VARCHAR(20) NOT NULL DEFAULT 'other' COMMENT '游玩 / 用餐 / 赶路 / 住宿 / 其他',
|
||||
`title` VARCHAR(200) NOT NULL,
|
||||
`detail` TEXT DEFAULT NULL COMMENT '这一段的文字记录',
|
||||
`place` VARCHAR(200) DEFAULT NULL COMMENT '地点',
|
||||
`starts_at` DATETIME DEFAULT NULL,
|
||||
`ends_at` DATETIME DEFAULT NULL,
|
||||
`status` ENUM('planned','ongoing','done','cancelled') NOT NULL DEFAULT 'planned',
|
||||
`position` INT NOT NULL DEFAULT 100 COMMENT '手动排序(拖动改的就是它)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tn_event` (`event_id`, `category`, `position`),
|
||||
KEY `idx_tn_time` (`starts_at`),
|
||||
CONSTRAINT `fk_tn_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_tn_event` FOREIGN KEY (`event_id`) REFERENCES `events` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行程节点:一趟旅程里的每一段(计划/执行双轨,TripLog 同款)'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `trip_node_members` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`node_id` INT UNSIGNED NOT NULL,
|
||||
`person_id` INT UNSIGNED NOT NULL COMMENT '这一段里有谁',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tnm_node_person` (`node_id`, `person_id`),
|
||||
CONSTRAINT `fk_tnm_node` FOREIGN KEY (`node_id`) REFERENCES `trip_nodes` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_tnm_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='行程节点的参与人'
|
||||
SQL,
|
||||
// ---- 日程(v12)----
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `calendars` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`family_id` INT UNSIGNED NOT NULL,
|
||||
`owner_id` INT UNSIGNED NOT NULL COMMENT '这本日历是谁的',
|
||||
`name` VARCHAR(50) NOT NULL,
|
||||
`color` CHAR(7) NOT NULL DEFAULT '#4A7C82' COMMENT '日历本的颜色(日程按本子的颜色显示)',
|
||||
`kind` ENUM('custom','movie','trip') NOT NULL DEFAULT 'custom' COMMENT 'movie/trip = 系统给每人预置的默认本,内容从 events 派生',
|
||||
`position` INT NOT NULL DEFAULT 100,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_cal_owner_kind` (`owner_id`, `kind`, `name`),
|
||||
KEY `idx_cal_family` (`family_id`),
|
||||
CONSTRAINT `fk_cal_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_cal_owner` FOREIGN KEY (`owner_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='日历本:每人可建,可自定义颜色,可共享给别人'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `calendar_shares` (
|
||||
`calendar_id` INT UNSIGNED NOT NULL,
|
||||
`person_id` INT UNSIGNED NOT NULL COMMENT '共享给谁',
|
||||
`perm` ENUM('read','write') NOT NULL DEFAULT 'read' COMMENT 'read=只读 / write=可写(能新增与修改日程)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`calendar_id`, `person_id`),
|
||||
CONSTRAINT `fk_cs_cal` FOREIGN KEY (`calendar_id`) REFERENCES `calendars` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_cs_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='日历本共享:对方能看到这本日历(只读/可写)'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `calendar_events` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`calendar_id` INT UNSIGNED NOT NULL,
|
||||
`title` VARCHAR(200) NOT NULL,
|
||||
`starts_at` DATE NOT NULL COMMENT '开始日期',
|
||||
`ends_at` DATE DEFAULT NULL COMMENT '结束日期(跨天日程;NULL=就一天)',
|
||||
`start_time` TIME DEFAULT NULL COMMENT '开始时刻;**NULL 与 00:00:00 都表示全天**',
|
||||
`end_time` TIME DEFAULT NULL,
|
||||
`all_day` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=全天日程(没有具体时分)',
|
||||
`note` TEXT DEFAULT NULL,
|
||||
`created_by` INT UNSIGNED DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ce_cal` (`calendar_id`, `starts_at`),
|
||||
CONSTRAINT `fk_ce_cal` FOREIGN KEY (`calendar_id`) REFERENCES `calendars` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_ce_author` FOREIGN KEY (`created_by`) REFERENCES `persons` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='自定义日程(观影/旅行那两本不落这里,从 events 派生)'
|
||||
SQL,
|
||||
// ---- 资源(v12)----
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `resources` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`family_id` INT UNSIGNED NOT NULL,
|
||||
`owner_id` INT UNSIGNED NOT NULL COMMENT '这是**谁名下**的资源(如「我的身份证」)',
|
||||
`title` VARCHAR(120) NOT NULL COMMENT '标题:身份证 / 学位证 / 一寸照片…',
|
||||
`content` TEXT DEFAULT NULL COMMENT '正文(文字说明)',
|
||||
`created_by` INT UNSIGNED DEFAULT NULL COMMENT '谁建的(本人自然是本人,别人代建时记一笔)',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_res_owner` (`owner_id`, `id`),
|
||||
CONSTRAINT `fk_res_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_res_owner` FOREIGN KEY (`owner_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_res_author` FOREIGN KEY (`created_by`) REFERENCES `persons` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='挂在某人名下的资源信息(证件、照片…)+ 多附件'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `resource_shares` (
|
||||
`resource_id` INT UNSIGNED NOT NULL,
|
||||
`person_id` INT UNSIGNED NOT NULL COMMENT '共享给谁(**不含** owner —— 名下的人本来就能看能改)',
|
||||
`perm` ENUM('read','write') NOT NULL DEFAULT 'read',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`resource_id`, `person_id`),
|
||||
CONSTRAINT `fk_rs_res` FOREIGN KEY (`resource_id`) REFERENCES `resources` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_rs_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源共享给别人:可只读、可读写'
|
||||
SQL,
|
||||
// ---- 任务(v12)----
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `tasks` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`family_id` INT UNSIGNED NOT NULL,
|
||||
`parent_id` INT UNSIGNED DEFAULT NULL COMMENT 'NULL=父任务;非空=某个父任务下的子任务(只做两级)',
|
||||
`title` VARCHAR(200) NOT NULL,
|
||||
`note` TEXT DEFAULT NULL,
|
||||
`status` ENUM('todo','doing','done') NOT NULL DEFAULT 'todo' COMMENT '看板的三列',
|
||||
`due_at` DATETIME DEFAULT NULL COMMENT '截止时间;**有截止时间才会出现在日历里**',
|
||||
`position` INT NOT NULL DEFAULT 100 COMMENT '看板里的先后(拖动改的就是它)',
|
||||
`creator_id` INT UNSIGNED DEFAULT NULL,
|
||||
`assignee_id` INT UNSIGNED DEFAULT NULL COMMENT '主要负责人(可空:共享给谁谁都能完成)',
|
||||
`repeat_rule` VARCHAR(40) DEFAULT NULL COMMENT '循环:day / week / month / year(空=不循环)',
|
||||
`done_at` DATETIME DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_task_board` (`family_id`, `status`, `position`),
|
||||
KEY `idx_task_parent` (`parent_id`),
|
||||
KEY `idx_task_due` (`due_at`),
|
||||
CONSTRAINT `fk_task_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_task_parent` FOREIGN KEY (`parent_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_task_creator` FOREIGN KEY (`creator_id`) REFERENCES `persons` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_task_assignee` FOREIGN KEY (`assignee_id`) REFERENCES `persons` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务:看板三列 + 两级子任务 + 评论 + 共享 + 循环'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `task_shares` (
|
||||
`task_id` INT UNSIGNED NOT NULL,
|
||||
`person_id` INT UNSIGNED NOT NULL COMMENT '共享给谁(两个人里任何一个完成都算完成)',
|
||||
`perm` ENUM('read','write') NOT NULL DEFAULT 'write',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`task_id`, `person_id`),
|
||||
CONSTRAINT `fk_ts_task` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_ts_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务共享:任何人完成都算完成'
|
||||
SQL,
|
||||
<<<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS `task_comments` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`task_id` INT UNSIGNED NOT NULL,
|
||||
`person_id` INT UNSIGNED NOT NULL,
|
||||
`content` TEXT NOT NULL COMMENT '支持 @某人:渲染时把 @名字 高亮出来',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tc_task` (`task_id`, `created_at`),
|
||||
CONSTRAINT `fk_tc_task` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_tc_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='任务评论(支持 @人)'
|
||||
SQL,
|
||||
];
|
||||
}
|
||||
@@ -521,6 +734,31 @@ SQL,
|
||||
self::addColumn($db, 'movie_details', 'douban_meta',
|
||||
"ADD COLUMN `douban_meta` TEXT DEFAULT NULL COMMENT '豆瓣自动补充的元数据(JSON)' AFTER `cinema_id`");
|
||||
|
||||
// ---- 头像主色(版本 11)----
|
||||
// 只在「上传/换头像」那一刻算一次(见 AvatarColor 的说明):渲染时再算
|
||||
// 就要为页面上的每个人回读一次存储,几十个人 = 几十次外网往返。
|
||||
self::addColumn($db, 'persons', 'avatar_color',
|
||||
"ADD COLUMN `avatar_color` CHAR(7) DEFAULT NULL COMMENT '头像主色(上传时算一次;页面按它派生底色/字色)' AFTER `avatar`");
|
||||
// 老头像(v11 之前传的)补算一次:回读已存的图算主色。
|
||||
// 纯数据步骤 → 容错跑:某张图读不回来(OSS 权限、对象丢了)只影响那一个人,
|
||||
// 他会自动落到「按姓名取色」的兜底上,不该把后面的升级步骤挡住。
|
||||
self::tryStep('回填头像主色', static function () use ($db) {
|
||||
self::backfillAvatarColor($db);
|
||||
});
|
||||
|
||||
// ---- 四个新模块(版本 12)----
|
||||
// ⚠️ events.kind 原本只有 'movie',旅程要复用统一事件原语就必须加 'trip'。
|
||||
// 建表语句的 IF NOT EXISTS 对**已存在的表**不会更新枚举 —— 老库必须显式改,
|
||||
// 否则插入 kind='trip' 会被严格模式整条拒掉(v7 就踩过同一类坑)。
|
||||
self::widenEventKindEnum($db);
|
||||
// 每人的两本默认日历(观影 / 旅行):新装的由 Calendar::seedDefaults 在
|
||||
// 建账号时建,老库在这里补(幂等:按 (owner_id, kind, name) 唯一键查过再插)。
|
||||
self::tryStep('补建每人默认的观影/旅行日历本', static function () use ($db) {
|
||||
foreach ($db->query('SELECT id FROM families')->fetchAll() as $fam) {
|
||||
\App\Calendar::seedDefaultsForFamily((int)$fam['id']);
|
||||
}
|
||||
});
|
||||
|
||||
// 被容错跳过的数据步骤记进设置,系统设置页会把它显出来 ——
|
||||
// 不然「升级没跑完」这件事只活在日志里,跟没发生一样。
|
||||
if (self::$stepErrors) {
|
||||
@@ -602,8 +840,22 @@ SQL,
|
||||
*/
|
||||
private static function backfillCinemasFromDetails(\PDO $db): void
|
||||
{
|
||||
// ⚠️ 逐列守卫,而不是只信任调用方那一个判据(`hasColumn(cinema_name)`):
|
||||
// 老库的版本千差万别,"有 cinema_name 但缺 cinema_place"是完全可能的,
|
||||
// 那样下面这句 SELECT 会 1054(Unknown column)—— 而它**不在 tryStep 里**
|
||||
// (回填是删列的唯一前提,炸出来才是对的),一次升级就断在这儿、
|
||||
// 下次请求再断一次。按实际存在的列动态拼查询,缺列就少搬一列,不炸。
|
||||
$cols = [];
|
||||
foreach (['cinema_place', 'cinema_name', 'cinema_address'] as $c) {
|
||||
if (self::hasColumn($db, 'movie_details', $c)) {
|
||||
$cols[] = $c;
|
||||
}
|
||||
}
|
||||
if (!$cols) {
|
||||
return;
|
||||
}
|
||||
$rows = $db->query(
|
||||
'SELECT d.event_id, e.family_id, d.cinema_place, d.cinema_name, d.cinema_address
|
||||
'SELECT d.event_id, e.family_id, d.' . implode(', d.', $cols) . '
|
||||
FROM movie_details d JOIN events e ON e.id = d.event_id
|
||||
WHERE d.cinema_name IS NOT NULL AND d.cinema_name <> \'\''
|
||||
)->fetchAll();
|
||||
@@ -635,10 +887,24 @@ SQL,
|
||||
*/
|
||||
private static function backfillMovieReviewsFromDetails(\PDO $db): void
|
||||
{
|
||||
// 同 backfillCinemasFromDetails:按实际存在的列动态拼,缺列就少搬一列,
|
||||
// 绝不让"某个中间版本的老库正好缺一列"把整条升级路径炸断。
|
||||
$cols = [];
|
||||
foreach (['rating', 'review', 'feelings'] as $c) {
|
||||
if (self::hasColumn($db, 'movie_details', $c)) {
|
||||
$cols[] = $c;
|
||||
}
|
||||
}
|
||||
if (!in_array('rating', $cols, true)) {
|
||||
return; // 连 rating 都没有 → 没有"评价"可迁
|
||||
}
|
||||
$where = 'd.rating IS NOT NULL';
|
||||
if (in_array('review', $cols, true)) {
|
||||
$where .= " OR (d.review IS NOT NULL AND d.review <> '')";
|
||||
}
|
||||
$rows = $db->query(
|
||||
'SELECT d.event_id, e.family_id, e.creator_id, d.rating, d.review, d.feelings
|
||||
FROM movie_details d JOIN events e ON e.id = d.event_id
|
||||
WHERE d.rating IS NOT NULL OR (d.review IS NOT NULL AND d.review <> \'\')'
|
||||
'SELECT d.event_id, e.family_id, e.creator_id, d.' . implode(', d.', $cols) . '
|
||||
FROM movie_details d JOIN events e ON e.id = d.event_id WHERE ' . $where
|
||||
)->fetchAll();
|
||||
if (!$rows) {
|
||||
return;
|
||||
@@ -674,6 +940,45 @@ SQL,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v11:给「有头像但没主色」的人补算主色。
|
||||
*
|
||||
* 幂等:条件是 `avatar_color IS NULL`,算过的人不会被重算(用户后来换的头像
|
||||
* 由上传流程负责更新,升级不该覆盖它)。
|
||||
* 读不回来就跳过 —— 那个人走 AvatarColor::for() 的姓名兜底,永远是同一种颜色。
|
||||
*/
|
||||
private static function backfillAvatarColor(\PDO $db): void
|
||||
{
|
||||
$rows = $db->query(
|
||||
"SELECT id, family_id, avatar, name FROM persons
|
||||
WHERE avatar IS NOT NULL AND avatar <> '' AND avatar_color IS NULL
|
||||
ORDER BY id"
|
||||
)->fetchAll();
|
||||
if (!$rows) {
|
||||
return;
|
||||
}
|
||||
$up = $db->prepare('UPDATE persons SET avatar_color = ? WHERE id = ?');
|
||||
foreach ($rows as $r) {
|
||||
$key = trim((string)$r['avatar']);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$res = \App\Storage\Storage::read((int)$r['family_id'], $key);
|
||||
} catch (\Throwable $e) {
|
||||
continue;
|
||||
}
|
||||
if (empty($res['ok']) || (string)($res['body'] ?? '') === '') {
|
||||
continue;
|
||||
}
|
||||
$hex = \App\AvatarColor::fromBinary((string)$res['body']);
|
||||
if ($hex === '') {
|
||||
continue; // 灰片:留给姓名兜底,不要塞一个飘忽的灰进去
|
||||
}
|
||||
$up->execute([$hex, (int)$r['id']]);
|
||||
}
|
||||
}
|
||||
|
||||
/** 列不存在才加 */
|
||||
private static function addColumn(\PDO $db, string $table, string $column, string $alter): void
|
||||
{
|
||||
@@ -723,6 +1028,24 @@ SQL,
|
||||
}
|
||||
}
|
||||
|
||||
/** events.kind 加 'trip'(旅程复用统一事件原语;见 ensure 里的说明) */
|
||||
private static function widenEventKindEnum(\PDO $db): void
|
||||
{
|
||||
$st = $db->prepare(
|
||||
"SELECT COLUMN_TYPE FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'events' AND COLUMN_NAME = 'kind'"
|
||||
);
|
||||
$st->execute();
|
||||
$type = (string)$st->fetchColumn();
|
||||
if ($type !== '' && strpos($type, 'trip') === false) {
|
||||
$db->exec(
|
||||
"ALTER TABLE `events` MODIFY COLUMN `kind`
|
||||
ENUM('movie','trip') NOT NULL
|
||||
COMMENT '事件类型:movie=观影 / trip=旅程(任务等只在有截止时间时上日历,不占 kind)'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static function hasIndex(\PDO $db, string $table, string $index): bool
|
||||
{
|
||||
$st = $db->prepare(
|
||||
|
||||
Reference in New Issue
Block a user