,没有 JS 也能开合(app.js 不依赖它)。 * * ⚠️ 与「名册」的分工:名册记录的是**这个人是谁**(生日、电话、关系); * 资源记录的是**这个人名下有什么东西**(证书、账号、照片)。不要混。 */ declare(strict_types=1); namespace App; final class Resource { /** 资源的宿主类型(附件表用) */ public const ATTACH = 'resource'; /** * 我能看到的资源 = 我名下的 + 别人共享给我的。 * * 返回的行里带 `perm`:'owner' / 'write' / 'read'(模板据此决定能不能改)。 * * @param int $personId 看的人(决定"我能看什么") * @param int $ownerId 0=不限归属;非 0=只看这个人(且必须是我能看到的) * @param string $keyword 标题 + 正文里搜 */ public static function all(int $familyId, int $personId, int $ownerId = 0, string $keyword = ''): array { $sql = 'SELECT r.*, p.name AS owner_name, p.avatar, p.avatar_color, n.nickname AS owner_nick, CASE WHEN r.owner_id = ? THEN "owner" ELSE COALESCE((SELECT s.perm FROM resource_shares s WHERE s.resource_id = r.id AND s.person_id = ?), "read") END AS perm FROM resources r JOIN persons p ON p.id = r.owner_id LEFT JOIN nicknames n ON n.target_id = r.owner_id AND n.viewer_id = ? LEFT JOIN resource_shares sh ON sh.resource_id = r.id AND sh.person_id = ? WHERE r.family_id = ? AND (r.owner_id = ? OR sh.person_id IS NOT NULL)'; $bind = [$personId, $personId, $personId, $personId, $familyId, $personId]; if ($ownerId > 0) { $sql .= ' AND r.owner_id = ?'; $bind[] = $ownerId; } $kw = trim($keyword); if ($kw !== '') { $sql .= ' AND (r.title LIKE ? OR r.content LIKE ?)'; $like = '%' . $kw . '%'; array_push($bind, $like, $like); } $sql .= ' ORDER BY r.updated_at DESC, r.id DESC'; $st = Database::get()->prepare($sql); $st->execute($bind); return $st->fetchAll(); } /** 一条资源(带归属人与我对它的权限;看不到返回 null) */ public static function find(int $familyId, int $id, int $personId): ?array { $st = Database::get()->prepare( 'SELECT r.*, p.name AS owner_name, p.avatar, p.avatar_color, n.nickname AS owner_nick, CASE WHEN r.owner_id = ? THEN "owner" ELSE COALESCE((SELECT s.perm FROM resource_shares s WHERE s.resource_id = r.id AND s.person_id = ?), "read") END AS perm FROM resources r JOIN persons p ON p.id = r.owner_id LEFT JOIN nicknames n ON n.target_id = r.owner_id AND n.viewer_id = ? WHERE r.id = ? AND r.family_id = ?' ); $st->execute([$personId, $personId, $personId, $id, $familyId]); $r = $st->fetch(); if (!$r) { return null; } // ⚠️ 权限在**这一层**判死:调用方拿到记录就一定是能看的。 // 曾经想让模板自己判,结果是每条列表都要重复一遍 CASE WHEN。 $mine = (int)$r['owner_id'] === $personId || self::isShared((int)$r['id'], $personId); if (!$mine) { return null; } return $r; } public static function canWrite(array $r, int $personId): bool { if ((int)($r['owner_id'] ?? 0) === $personId) { return true; } return ($r['perm'] ?? '') === 'write'; } public static function isShared(int $resourceId, int $personId): bool { $st = Database::get()->prepare( 'SELECT 1 FROM resource_shares WHERE resource_id = ? AND person_id = ?' ); $st->execute([$resourceId, $personId]); return $st->fetchColumn() !== false; } /** @return int 新资源 id */ public static function create(int $familyId, int $ownerId, ?int $createdBy, array $d): int { $db = Database::get(); $db->prepare( 'INSERT INTO resources (family_id, owner_id, title, content, created_by) VALUES (?,?,?,?,?)' )->execute([ $familyId, $ownerId, self::title($d['title'] ?? ''), self::textOrNull($d['content'] ?? ''), $createdBy ?: null, ]); return (int)$db->lastInsertId(); } public static function update(int $familyId, int $id, array $d): void { Database::get()->prepare( 'UPDATE resources SET title = ?, content = ? WHERE id = ? AND family_id = ?' )->execute([ self::title($d['title'] ?? ''), self::textOrNull($d['content'] ?? ''), $id, $familyId, ]); } public static function destroy(int $familyId, int $id): void { // 附件表没有外键(target_type 表达不了),删宿主时自己清存储对象 \App\Attachment::removeAll(self::ATTACH, $id); Database::get()->prepare('DELETE FROM resources WHERE id = ? AND family_id = ?') ->execute([$id, $familyId]); } // ---------------- 共享 ---------------- public static function shares(int $resourceId): array { $st = Database::get()->prepare( 'SELECT s.person_id, s.perm, p.name, p.avatar, p.avatar_color FROM resource_shares s JOIN persons p ON p.id = s.person_id WHERE s.resource_id = ? ORDER BY p.id' ); $st->execute([$resourceId]); return $st->fetchAll(); } /** * 设共享。 * * ⚠️ owner 自己**不进**共享表 —— 他本来就有全部权限, * 记进去只是多一条要维护的行(还会让"共享给谁"的列表里冒出自己)。 */ public static function setShare(int $resourceId, int $ownerId, int $personId, string $perm): void { if ($personId <= 0 || $personId === $ownerId) { self::dropShare($resourceId, $personId); return; } Database::get()->prepare( 'INSERT INTO resource_shares (resource_id, person_id, perm) VALUES (?,?,?) ON DUPLICATE KEY UPDATE perm = VALUES(perm)' )->execute([$resourceId, $personId, $perm === 'write' ? 'write' : 'read']); } public static function dropShare(int $resourceId, int $personId): void { Database::get()->prepare('DELETE FROM resource_shares WHERE resource_id = ? AND person_id = ?') ->execute([$resourceId, $personId]); } public static function permWord(string $perm): string { return $perm === 'owner' ? '本人' : ($perm === 'write' ? '可读写' : '只读'); } // ---------------- 小工具 ---------------- private static function title(string $v): string { return mb_substr(trim($v), 0, 120, 'UTF-8'); } private static function textOrNull(string $v): ?string { $v = trim($v); return $v !== '' ? $v : null; } }