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:
+143
-4
@@ -62,7 +62,8 @@ final class Anniversary
|
||||
/**
|
||||
* 新建 / 修改一条纪念日。
|
||||
*
|
||||
* @param array $d title / repeat_type / month / day / kind / note / person_id / leap
|
||||
* @param array $d title / repeat_type / month / day / kind / note / leap
|
||||
* + **person_ids[]**(v24 第 25 批:可以关联多个人)
|
||||
* @return int 记录 id
|
||||
*/
|
||||
public static function save(int $familyId, array $d, int $id = 0): int
|
||||
@@ -80,15 +81,47 @@ final class Anniversary
|
||||
$day = max(1, min(31, (int)($d['day'] ?? 1)));
|
||||
$kind = mb_substr(trim((string)($d['kind'] ?? '')), 0, 40, 'UTF-8');
|
||||
$note = trim((string)($d['note'] ?? ''));
|
||||
$pid = (int)($d['person_id'] ?? 0) ?: null;
|
||||
$leap = !empty($d['leap']) ? 1 : 0;
|
||||
|
||||
// ---- 关联的人(v24 第 25 批 用户第 2 条:一人 → 多人)----
|
||||
//
|
||||
// ⚠️ 兼容老表单:只传 `person_id`(单值)时也认,包成一个人的数组。
|
||||
// 这样任何还没来得及改的调用点(老链接、外部脚本)都不会静默丢人。
|
||||
$pids = [];
|
||||
if (array_key_exists('person_ids', $d)) {
|
||||
foreach ((array)$d['person_ids'] as $x) {
|
||||
$x = (int)$x;
|
||||
if ($x > 0) {
|
||||
$pids[] = $x;
|
||||
}
|
||||
}
|
||||
} elseif (array_key_exists('person_id', $d)) {
|
||||
$x = (int)$d['person_id'];
|
||||
if ($x > 0) {
|
||||
$pids[] = $x;
|
||||
}
|
||||
}
|
||||
$pids = array_values(array_unique($pids));
|
||||
// 只认这个家庭里的人(表单可以改,不能凭它挂到别人家的人身上)
|
||||
if ($pids) {
|
||||
$in = implode(',', array_fill(0, count($pids), '?'));
|
||||
$st = $db->prepare("SELECT id FROM persons WHERE family_id = ? AND id IN ($in)");
|
||||
$st->execute(array_merge([$familyId], $pids));
|
||||
$ok = array_map('intval', array_column($st->fetchAll(), 'id'));
|
||||
$pids = array_values(array_intersect($pids, $ok));
|
||||
}
|
||||
|
||||
// ⚠️ 老列 person_id 继续写**第一个**关联人(不写 NULL):
|
||||
// 它是老代码/老库的兜底读点,写成 NULL 会让"没升级到的读点"少一个人。
|
||||
$pid = $pids[0] ?? null;
|
||||
|
||||
if ($id > 0) {
|
||||
$db->prepare(
|
||||
'UPDATE anniversaries SET title=?, repeat_type=?, month=?, day=?, is_leap=?,
|
||||
kind=?, note=?, person_id=? WHERE id=? AND family_id=?'
|
||||
)->execute([$title, $repeat, $month, $day, $leap, $kind !== '' ? $kind : null,
|
||||
$note !== '' ? $note : null, $pid, $id, $familyId]);
|
||||
self::savePeople($id, $pids);
|
||||
return $id;
|
||||
}
|
||||
$db->prepare(
|
||||
@@ -97,12 +130,101 @@ final class Anniversary
|
||||
VALUES (?,?,?,?,?,?,?,?,?)'
|
||||
)->execute([$familyId, $title, $repeat, $month, $day, $leap,
|
||||
$kind !== '' ? $kind : null, $note !== '' ? $note : null, $pid]);
|
||||
return (int)$db->lastInsertId();
|
||||
$newId = (int)$db->lastInsertId();
|
||||
self::savePeople($newId, $pids);
|
||||
return $newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 整组替换某个纪念日关联的人(v24 第 25 批)。
|
||||
*
|
||||
* 「整组替换」而不是"逐条增删":表单交上来的就是**一份完整名单**,
|
||||
* 逐条比对会产生"先删后加"的中间态;一次 DELETE + 一次 INSERT 是确定结果。
|
||||
*/
|
||||
public static function savePeople(int $anniversaryId, array $personIds): int
|
||||
{
|
||||
$db = Database::get();
|
||||
$db->prepare('DELETE FROM anniversary_people WHERE anniversary_id = ?')->execute([$anniversaryId]);
|
||||
if (!$personIds) {
|
||||
return 0;
|
||||
}
|
||||
$ins = $db->prepare(
|
||||
'INSERT IGNORE INTO anniversary_people (anniversary_id, person_id, sort_order) VALUES (?,?,?)'
|
||||
);
|
||||
$n = 0;
|
||||
$i = 0;
|
||||
foreach ($personIds as $pid) {
|
||||
$pid = (int)$pid;
|
||||
if ($pid <= 0) {
|
||||
continue;
|
||||
}
|
||||
$i += 10;
|
||||
$ins->execute([$anniversaryId, $pid, $i]);
|
||||
$n++;
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
|
||||
/**
|
||||
* 某个纪念日关联的人(**含兜底**)。
|
||||
*
|
||||
* ⚠️ 兜底逻辑(v24 第 25 批升级过渡期必须有):
|
||||
* 新表里没有行时,回落到老列 `person_id` —— 这样"升级跑到一半"
|
||||
* 或"老数据没回填成功"时,页面上仍然看得到那个人,而不是静默变成"没关联"。
|
||||
*
|
||||
* @return int[] person id(按 sort_order)
|
||||
*/
|
||||
public static function peopleOf(array $row): array
|
||||
{
|
||||
static $cache = [];
|
||||
$aid = (int)($row['id'] ?? 0);
|
||||
if ($aid <= 0) {
|
||||
return [];
|
||||
}
|
||||
if (array_key_exists($aid, $cache)) {
|
||||
return $cache[$aid];
|
||||
}
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT person_id FROM anniversary_people WHERE anniversary_id = ? ORDER BY sort_order, person_id'
|
||||
);
|
||||
$st->execute([$aid]);
|
||||
$out = array_map('intval', array_column($st->fetchAll(), 'person_id'));
|
||||
if (!$out) {
|
||||
$one = (int)($row['person_id'] ?? 0);
|
||||
if ($one > 0) {
|
||||
$out = [$one];
|
||||
}
|
||||
}
|
||||
$cache[$aid] = $out;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 一批纪念日的关联人:`anniversary_id => [person_id, ...]`(避免逐行查库)。
|
||||
*/
|
||||
public static function peopleMap(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_filter(array_map('intval', $ids)));
|
||||
if (!$ids) {
|
||||
return [];
|
||||
}
|
||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||
$st = Database::get()->prepare(
|
||||
"SELECT anniversary_id, person_id FROM anniversary_people
|
||||
WHERE anniversary_id IN ($in) ORDER BY anniversary_id, sort_order, person_id"
|
||||
);
|
||||
$st->execute($ids);
|
||||
$out = [];
|
||||
foreach ($st->fetchAll() as $r) {
|
||||
$out[(int)$r['anniversary_id']][] = (int)$r['person_id'];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function remove(int $familyId, int $id): void
|
||||
{
|
||||
Attachment::removeAll(self::ATTACH, $id);
|
||||
// 关联表由外键 ON DELETE CASCADE 一起走;这里不必手动删
|
||||
Database::get()->prepare('DELETE FROM anniversaries WHERE id = ? AND family_id = ?')
|
||||
->execute([$id, $familyId]);
|
||||
}
|
||||
@@ -189,13 +311,30 @@ final class Anniversary
|
||||
*/
|
||||
public static function split(int $familyId, int $viewerId): array
|
||||
{
|
||||
$rows = self::list($familyId);
|
||||
// 一次把所有人的关联人查出来(避免逐行查库)——v24 第 25 批 用户第 2 条
|
||||
$pmap = self::peopleMap(array_column($rows, 'id'));
|
||||
$custom = [];
|
||||
foreach (self::list($familyId) as $r) {
|
||||
foreach ($rows as $r) {
|
||||
$r['source'] = 'custom';
|
||||
$r['person_name'] = '';
|
||||
$aid = (int)$r['id'];
|
||||
if (!empty($pmap[$aid])) {
|
||||
$r['person_ids'] = $pmap[$aid];
|
||||
} elseif ((int)($r['person_id'] ?? 0) > 0) {
|
||||
// 兜底:新表没有行就看老列(升级过渡期)
|
||||
$r['person_ids'] = [(int)$r['person_id']];
|
||||
} else {
|
||||
$r['person_ids'] = [];
|
||||
}
|
||||
$custom[] = $r;
|
||||
}
|
||||
$derived = self::birthdays($familyId, $viewerId);
|
||||
// 派生行(名册生日)本来就知道是谁,统一挂上 person_ids,模板无须分两条路
|
||||
foreach ($derived as &$dv) {
|
||||
$dv['person_ids'] = (int)($dv['person_id'] ?? 0) > 0 ? [(int)$dv['person_id']] : [];
|
||||
}
|
||||
unset($dv);
|
||||
|
||||
$byMD = static function (array $a, array $b): int {
|
||||
return ((int)($a['month'] ?? 0) <=> (int)($b['month'] ?? 0))
|
||||
|
||||
Reference in New Issue
Block a user