名册每人一份 · 分组两层 · 拖动排序 · 表单合一 · 裁剪可缩放
按用户十点反馈逐条落地(v5,Schema::VERSION = 5): - 名册改成每人一份:新表 person_roster(owner_id, person_id, group_id, sort_order, source)。默认只看「我添加的 + 别人分享给我的」; source='seeded' 是升级时从老 persons.group_id 播进来的(先播数据再删列)。 - 排序不再手填:名册页直接拖动卡片(整版提交,一次 applyLayout 落库), 无 JS 时卡片上的「调整分组」表单照旧可用。 - 分组分两层:管理员在「系统设置」维护公共分组(owner_id=0), 用户在「个人设置」加自己的分组;显示顺序各人各定、互不影响。 - 名册可共享:把名片分享给别的用户(people/share);添加成员时若系统里 已有这个人,可直接复用(people/pick),不必重复录入。 - 「编辑成员」与「改我自己的资料」合并为同一张表(templates/people/form.php), 两个入口 href 完全相同,h1 按改谁切换文案;个人设置页只留 资料入口 + 称呼 + 我的分组三块,删除 templates/settings/_profile.php。 - 系统设置收进头像下拉,顶栏只剩 首页/家谱/名册。 - 裁剪器加缩放(滑块 1-4x、Ctrl+滚轮、还原):缩放只改视图大小, 落库的归一化矩形保持不变。 - 权限:管理员可以改任何人的权限(含自己),只拦「最后一位管理员」被降级。 - 去掉首页「我的一句话」后的链接、家谱说明段、以及「还没归到任何分组」提示。 迁移安全:数据回填(PersonRoster::seedFromPersons)先于删列执行、且不进 tryStep 容错;失败步骤记入 settings.schema_upgrade_warning 并在系统设置页提示。 线上已验证:schema_upgrade_warning 为空,person_roster/person_groups.owner_id 就位,persons.group_id / sort_order 已删。 验证:php -l 48 文件;本地 e2e 277/0、老库升级 81/0、桌面几何 108/0、 移动端几何 180/0、交互 96/0;反向验证 38 条注入 + 2 条缩放注入全部命中; 部署静态断言 53/0,站点自检 7/7(--resolve 锁本机走 443,探针真的进 PHP)。
This commit is contained in:
+48
-26
@@ -41,7 +41,44 @@ final class Person
|
||||
$args[] = $like;
|
||||
$args[] = $like;
|
||||
}
|
||||
$sql .= ' ORDER BY p.sort_order ASC, p.id ASC';
|
||||
$sql .= ' ORDER BY p.id ASC';
|
||||
$st = Database::get()->prepare($sql);
|
||||
$st->execute($args);
|
||||
return $st->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* **我的名册**:只列我自己的名册里的家庭成员(带分组与顺序)。
|
||||
*
|
||||
* 与 all() 的分工(v5 起两者不再是一回事):
|
||||
* · all() —— 全家庭的成员,不管是谁的名册(首页徽标、邀请访客等内部用途);
|
||||
* · roster() —— 名册页该显示的那些:我加的人 + 别人分享给我的,顺序是我的。
|
||||
*
|
||||
* 分组与顺序都从 person_roster 取(列名故意与老库的 persons.group_id /
|
||||
* persons.sort_order 保持一致,调用方几乎不用改)。
|
||||
*/
|
||||
public static function roster(int $familyId, int $viewerId, string $kind = 'member', string $q = ''): array
|
||||
{
|
||||
$sql = 'SELECT p.*, r.group_id, r.sort_order, r.source AS roster_source,
|
||||
a.id AS account_id, a.role AS account_role, a.status AS account_status,
|
||||
a.last_login_at, n.nickname AS my_nickname
|
||||
FROM person_roster r
|
||||
JOIN persons p ON p.id = r.person_id
|
||||
LEFT JOIN accounts a ON a.person_id = p.id
|
||||
LEFT JOIN nicknames n ON n.target_id = p.id AND n.viewer_id = ?';
|
||||
$args = [$viewerId];
|
||||
$sql .= ' WHERE r.family_id = ? AND r.owner_id = ? AND p.kind = ?';
|
||||
$args[] = $familyId;
|
||||
$args[] = $viewerId;
|
||||
$args[] = $kind;
|
||||
if ($q !== '') {
|
||||
$sql .= ' AND (p.name LIKE ? OR p.email LIKE ? OR n.nickname LIKE ?)';
|
||||
$like = '%' . $q . '%';
|
||||
$args[] = $like;
|
||||
$args[] = $like;
|
||||
$args[] = $like;
|
||||
}
|
||||
$sql .= ' ORDER BY r.sort_order ASC, p.id ASC';
|
||||
$st = Database::get()->prepare($sql);
|
||||
$st->execute($args);
|
||||
return $st->fetchAll();
|
||||
@@ -91,8 +128,8 @@ final class Person
|
||||
(family_id, kind, name, gender, birth_rank, birthday,
|
||||
birthday_id_card, birthday_lunar, birthday_lunar_leap, slogan, avatar,
|
||||
phone_hash, phone_encrypted, id_card_encrypted, email, address, note,
|
||||
is_placeholder, group_id, sort_order)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
||||
is_placeholder)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
||||
$st = Database::get()->prepare($sql);
|
||||
$st->execute([
|
||||
$familyId,
|
||||
@@ -113,8 +150,6 @@ final class Person
|
||||
($d['address'] ?? '') !== '' ? $d['address'] : null,
|
||||
($d['note'] ?? '') !== '' ? $d['note'] : null,
|
||||
!empty($d['is_placeholder']) ? 1 : 0,
|
||||
!empty($d['group_id']) ? (int)$d['group_id'] : null,
|
||||
(int)($d['sort_order'] ?? 1000),
|
||||
]);
|
||||
return (int)Database::get()->lastInsertId();
|
||||
}
|
||||
@@ -127,7 +162,7 @@ final class Person
|
||||
name = ?, gender = ?, birth_rank = ?, birthday = ?,
|
||||
birthday_id_card = ?, birthday_lunar = ?, birthday_lunar_leap = ?, slogan = ?, avatar = ?,
|
||||
phone_hash = ?, phone_encrypted = ?, id_card_encrypted = ?,
|
||||
email = ?, address = ?, note = ?, is_placeholder = ?, group_id = ?, sort_order = ?
|
||||
email = ?, address = ?, note = ?, is_placeholder = ?
|
||||
WHERE id = ?';
|
||||
Database::get()->prepare($sql)->execute([
|
||||
$d['name'],
|
||||
@@ -146,32 +181,19 @@ final class Person
|
||||
($d['address'] ?? '') !== '' ? $d['address'] : null,
|
||||
($d['note'] ?? '') !== '' ? $d['note'] : null,
|
||||
!empty($d['is_placeholder']) ? 1 : 0,
|
||||
// ⚠️ 这里不能用 `!empty($d['group_id'])` 之外的花样:
|
||||
// "清空分组"(改成未分类)也是一个合法意图,所以 0/'' 一律落成 NULL。
|
||||
!empty($d['group_id']) ? (int)$d['group_id'] : null,
|
||||
(int)($d['sort_order'] ?? 1000),
|
||||
$id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 只改「这个人属于哪一组」(名册上的快捷归类用)。
|
||||
* 自助改自己的资料。
|
||||
*
|
||||
* 单独一个方法,是为了让名册那一小块表单**不必**把整条记录回传 ——
|
||||
* 否则一次归类提交就要带上姓名/生日/身份证号,任何一处没带全就会把数据改坏。
|
||||
*/
|
||||
public static function setGroup(int $id, ?int $groupId): void
|
||||
{
|
||||
Database::get()->prepare('UPDATE persons SET group_id = ? WHERE id = ?')
|
||||
->execute([$groupId ?: null, $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自助改自己的资料(个人信息页用)。
|
||||
*
|
||||
* 与上面的 update() 分开是有意的:那个是"管理员编名册",字段全、权限高;
|
||||
* 与上面的 update() 分开是有意的:那个是"能写的人编名册",字段全、权限高;
|
||||
* 这个只允许改**属于自己的**东西 —— 姓名、性别、三个生日、一句话、头像、联系方式。
|
||||
* 排行、排序号、虚拟人物标记、以及身份证号(会牵动身份证生日)都只能由管理员改名册时动。
|
||||
* 排行、虚拟人物标记、以及身份证号(会牵动身份证生日)都只能由管理员改名册时动。
|
||||
*
|
||||
* ⚠️ v5 起这里**不再有**分组:分组是「我和他的关系」,挂在 person_roster 上,
|
||||
* 只能在名册里拖或者在那张卡片的「调整分组」里改(见 PersonRoster::assignGroup)。
|
||||
*/
|
||||
public static function updateSelf(int $id, array $d): void
|
||||
{
|
||||
@@ -230,7 +252,7 @@ final class Person
|
||||
FROM persons p
|
||||
LEFT JOIN nicknames n ON n.target_id = p.id AND n.viewer_id = ?
|
||||
WHERE p.family_id = ? AND p.kind = "member" AND p.id <> ?
|
||||
ORDER BY p.sort_order ASC, p.id ASC'
|
||||
ORDER BY p.id ASC'
|
||||
);
|
||||
$st->execute([$viewerId, $familyId, $exceptId]);
|
||||
return $st->fetchAll();
|
||||
|
||||
Reference in New Issue
Block a user