v25 第 25 批(9 条):名册分组不刷页 / 纪念日多人 / 首页等高 / 移出不回流 /

名册点击看详情拖动才排序 / 任务复选框与状态 / 财务标签多选 / 微信读书同步与统计 /
导航线上数据与图标

用户本轮 9 条需求(原话见 templates/ 与 app/ 内的逐条注释):

1. 名册:列表里给某人分组改为 AJAX 就地挪卡片(不再整页刷新)。
2. 纪念日:关联人改**多选**(复用 _person_picker,字段 person_ids[]);
   自建纪念日的按钮与其它模块统一(改=蓝 .btn、删=黄 .btn-warn);
   列表新增「关联人」列并给人名前加头像。
3. 首页:日程/任务两块**完全等高**(align-items:stretch + 固定总高 + flex 子项
   min-height:0);修「0 件待办」——数字与右侧列表同源(原来各算各的)。
4. 🔴 名册里被移出的人过一阵又回来:根因是 PersonRoster::seedFromPersons()
   **每次 Schema::ensure() 都无条件重灌**(线上实测每跑一次 +1 行)。
   修:给它加自守卫(只有在 persons 还留着 group_id/sort_order 的**老库**才跑)。
5. 名册:点击=看详情、**只有拖 ⠿ 手柄**才是排序(原来整行既能点又能拖,语义混乱)。
6. 任务:所有任务前加复选框,任何位置点勾即完成;
   状态语义照业界整理为 To Do / Doing / Done 并写出说明(用户要求参考别人的做法)。
7. 财务:类别标签改**多选非互斥**(再点一次取消该类别),上下两图 + 图例 + 表格
   全部按同一个集合联动;高亮条刻意不给「合计」(多选下各类金额不能相加)。
8. 微信读书:新增**懒同步**(访问时顺手做,6h TTL,只补空字段不覆盖用户填的)、
   封面图**拉下来存本站存储**(原来只存外链 → 看不到)、接官方 /readdata/detail 统计。
9. 导航:线上数据只搬了一半(5 板块 29 站 → 7 板块 67 站 + 67 图标)、
   展示页头像不显示 / 搜索按钮错位 / 点登录 404。三个真根因:
   · 相对地址在 /nav/ 下全 404 → 新增 Nav::abs();
   · 搜索容器丢了 id="search"(CSS 是 #search button.submit 绝对定位);
   · Nav::iconKey() 与 SimpleNav 的 icon_key() **算法不一致**(少一次 md5 归一化)
     → 搬来的图标文件名对不上;且 uploads/ 整目录 Require all denied。
   新增两个**免登录公开出口** nav_icon.php / nav_avatar.php。

顺带修掉一个「数据损坏级」的真 bug(做第 4 条线上反证时发现的,之前一直没人注意):
app/Region.php 三处用中文标点当 trim/ltrim 的 charlist,而 PHP 的 charlist 是
**按字节**匹配的 —— 「。」= E3 80 **82** → 字节 82 进了可剥集合 →
「层」= E5 B1 **82** 被切掉尾字节 → 非法 UTF-8 → backfillAddress 的 UPDATE 报
`1366 Incorrect string value` → **地址拆分升级从上线起一直在失败被跳过**(每请求报一次)。
改成字符级 preg_replace('/…/u');线上实测 ensure() 现在无报错、待拆行数真的在下降。

验证战绩:
- e2e 1289/0(新增 Region 字节级用例等)
- 真机探针:选人 19/0 · 首页/导航 24/0 · 导航模块 34/0 · b18 暂存+确定 10/0 ·
  第23批 52/0
- 桌面几何 428/0 · 移动端几何 680/0 · 交互 92/0 · 窄屏 24/0
- 老库升级 153/0(含 anniversary_people 迁移 + 「再跑 ensure() 名册行数不变」)
- 部署静态断言 180 OK / 0 FAIL · 反向验证 3/3 + Region 三变体各被两条断言同时抓住
- 线上 26 个文件与本地**逐字节一致**;展示页 200、图标 66/66 200、登录 200、
  越权/不存在均 404、签发短语后 ensure() 不再报错。
This commit is contained in:
2026-10-07 11:35:31 +08:00
parent 09e30d27d4
commit a5e559b2b3
23 changed files with 2379 additions and 212 deletions
+44 -1
View File
@@ -104,7 +104,7 @@ final class Schema
* ⚠️ 它**不再**作为"要不要跑升级"的判据 —— 见 fingerprint() 的说明。
* 它仍然要手工维护,因为它给人看(排查时一眼知道代码到哪一版了)。
*/
public const VERSION = '24';
public const VERSION = '25';
/**
* 「源码指纹」:拿**本文件自身的内容**算一个哈希,作为"要不要跑升级"的判据。
@@ -1699,6 +1699,49 @@ SQL);
}
});
// ---- 纪念日关联**多个人**(版本 25,用户 2026-10-07 第 2 条)----
//
// 用户原话:「纪念日模块,每个纪念日现在只能关联一个人,应该可以关联多个人。
// 和选择观影人一样,多选模式。」
//
// ⚠️ 建表放在 ensure() 里(而不是 statements() 的 return 数组):
// 它有两个外键指向 `anniversaries` 与 `persons` —— 那两张表由
// statements() 先建出来,这里紧随其后才安全;而且这一段本来就在
// "改结构"的位置上(statements() 只管最终形态的建表)。
// ⚠️ **不改 anniversaries.person_id 那一列**(保留它、继续写第一个关联人):
// 它是老库/老代码的读点,删掉会让"没跟上升级"的地方静默少一个人。
// 改成"以新表为准、老列只做兜底"—— 见 Anniversary::peopleOf()。
$db->exec(<<<'SQL'
CREATE TABLE IF NOT EXISTS `anniversary_people` (
`anniversary_id` int(10) unsigned NOT NULL,
`person_id` int(10) unsigned NOT NULL,
`sort_order` INT NOT NULL DEFAULT 10 COMMENT '列表里的先后(关联多个人时按它排)',
PRIMARY KEY (`anniversary_id`, `person_id`),
KEY `idx_annp_person` (`person_id`),
CONSTRAINT `fk_annp_ann` FOREIGN KEY (`anniversary_id`) REFERENCES `anniversaries` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_annp_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
COMMENT='纪念日关联的人(v24 第 25 批 用户第 2 条:一个纪念日可以关联多个人)'
SQL);
// 回填:把老库里 `person_id` 非空的那一条搬进新表。
// ⚠️ 必须**只在还没有任何新表行时**搬(用 NOT EXISTS 判),否则
// 用户在新界面上改过关联的人之后,下一次请求又会被老列覆盖回去
// (与第 4 条"移出的人又回来"同一个类别的坑:无条件回填 = 删除被撤销)。
self::tryStep('纪念日关联人搬到新表(一人 → 多人)', static function () use ($db) {
if (!self::hasTable($db, 'anniversary_people')) {
return;
}
$db->exec(
"INSERT IGNORE INTO `anniversary_people` (anniversary_id, person_id, sort_order)
SELECT a.id, a.person_id, 10
FROM `anniversaries` a
WHERE a.person_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `anniversary_people` p
WHERE p.anniversary_id = a.id)"
);
});
// 被容错跳过的数据步骤记进设置,系统设置页会把它显出来 ——
// 不然「升级没跑完」这件事只活在日志里,跟没发生一样。
if (self::$stepErrors) {