# 设计:公开「关于我」详情页 + 资料媒体引用(ownerType) **日期**:2026-04-18 **状态**:已定稿(与产品选择一致:方案 B + 公开路由 ①) ## 1. 背景与目标 - 公开主页 `/@:publicSlug` 已展示 `users.bio_markdown`(在 `bioVisibility === public` 时经聚合接口下发),但**没有独立路由**承载「生平」长文的阅读体验。 - 文章图片的引用真相在 `post_media_refs`;**个人简介 Markdown 与头像 URL** 若包含本站 `/public/assets/...` 资源,当前**不会**同步到引用表,孤儿审查可能误判或漏判。 **目标**: 1. 新增公开路由 **`/@:publicSlug/about`**,用于专注展示已公开的生平 Markdown(与现有 `bio` 数据同源,不复制到 `posts`)。 2. 扩展媒体引用模型:用 **`ownerType` + `ownerId`** 统一表示「引用主体」,使 **文章** 与 **个人资料(简介 + 头像)** 共用同一套孤儿判定与时间戳调和逻辑。 ## 2. 产品规则 | 维度 | 规则 | |------|------| | 路由 | `/@:publicSlug/about`,`layout: 'public'`,与现有 `@[publicSlug]/*` 一致。 | | 可访问性 | 仅当 **`bioVisibility === 'public'`** 且 **`bioMarkdown` 非空(经 trim)** 时返回页面 **200**;否则 **404**(与「无公开简介则不提供该 URL」一致,避免空壳与权限歧义)。 | | `publicSlug` | 无效、用户非 `active` 时 **404**,与现公开 profile 行为一致。 | | 数据真相 | 生平内容仍为 **`users.bio_markdown`**;about 页不做第二份存储。 | | 主页关系 | 公开主页可继续展示当前 bio 区块;**建议**在 bio 区域增加指向 **`/about` 的入口**(文案如「查看全文」),当且仅当 about 页本会 200 时显示。具体文案与组件位置在实现计划中锁定。 | ## 3. 媒体引用模型(方案 B) ### 3.1 表结构方向 将现有 **`post_media_refs`** 演进为通用引用表(实现可选用 **重命名** 为 `media_refs` 或保留表名但扩展列;**推荐重命名**以减少「post_」误导)。 建议列: | 列 | 说明 | |----|------| | `owner_type` | `text`,取值 **`post`** \| **`profile`**(实现可用常量 + 校验;禁止其他值写入)。 | | `owner_id` | `integer`,与 `owner_type` 联合解释:`'post'` → `posts.id`;`'profile'` → **`users.id`**(该用户的资料级引用集合,每个用户至多一条 profile 引用集合,由 `(owner_type, owner_id)` 唯一标识)。 | | `asset_id` | `integer`,`NOT NULL`,外键 **`media_assets.id`**,`ON DELETE CASCADE`(与现行为一致)。 | **主键**:`(owner_type, owner_id, asset_id)`。 **索引**:保留 **`asset_id`** 上索引,供「某 asset 是否仍被引用」批量查询。 **外键说明**:SQLite 难以对 `owner_id` 做多态外键;**不**为 `owner_id` 声明指向 `posts`/`users` 的双向外键。参照完整性由应用层保证: - **`post`**:`owner_id` 必须对应当前存在的 `posts.id`,且在 **删除文章** 时**必须**删除 `owner_type='post' AND owner_id=` 的全部行(再删 post 或依赖事务顺序),并照常对涉及 `asset_id` 调用 **`reconcileAssetTimestampsAfterRefChange`**。 - **`profile`**:`owner_id` 必须等于该资料所属 `users.id`;用户删除若已级联删除其 `media_assets`,则依赖 `asset_id` 级联清理 ref 行即可。 ### 3.2 URL 解析范围(profile) 与文章一致:从 **`bioMarkdown`** 中按现有规则提取本站图片 URL;从 **`avatar`** 字段提取封面式 URL(与 `mergePostMediaUrls` / `extractMediaUrlsFromCover` 同一前缀与规范化规则)。**外链忽略**,不写入引用。 新增 **`syncProfileMediaRefs(userId, bioMarkdown, avatarUrl)`**(命名以实现为准): - 删除 `owner_type='profile' AND owner_id=userId` 的旧行; - 解析 URL → `storageKey` → 在 **`media_assets` 且 `user_id=userId`** 下解析 `asset_id`; - 插入新行;对变更涉及的 `asset_id` 调用 **`reconcileAssetTimestampsAfterRefChange`**。 在 **`updateProfile`**(或 `PUT /api/me/profile` 成功路径)中,当 **`bioMarkdown` 或 `avatar`** 任一参与更新时,在事务提交后调用上述同步(顺序与现 `syncPostMediaRefs` 一致:先落库用户字段再同步引用)。 ### 3.3 孤儿与删除 - **孤儿定义**:`media_assets` 中某行 **不存在** 任意 `media_refs` 行以其为 `asset_id`(与现「无 post 引用」等价,扩展为「无任何 owner 引用」)。 - **`reconcileAssetTimestampsAfterRefChange`**:对每个 `assetId` 统计 **全表** `media_refs` 中 `asset_id` 计数,替代现仅查 `post_media_refs` 的逻辑。 - **手动删除媒体 / 全局清扫**:删除前「仍被引用」判定改为 **任意 `media_refs`** 计数 > 0;错误文案可泛化为「资源仍被引用」或区分 owner(可选,非必须)。 ### 3.4 数据迁移 - 将现有 `post_media_refs` 全量迁移为 `owner_type='post'`,`owner_id=post_id`,`asset_id` 不变。 - 部署后可选:**后台任务或懒执行** 对已有用户跑一次 `syncProfileMediaRefs`,修复历史简介/头像中的站内图未登记问题(实现计划写明是否必须上线即跑)。 ## 4. API **推荐**新增轻量接口(避免 about 页拉全量 profile 聚合): | 方法 | 路径 | 行为 | |------|------|------| | GET | `/api/public/profile/:publicSlug/about` | 当且仅当 slug 有效、用户 active、且公开 bio 非空时返回渲染所需字段(如 `user` 展示子集、`bio: { markdown }`);否则 **404**。 | 字段形状与现 `GET .../profile/:publicSlug` 中 `user` / `bio` 对齐即可,减少前端类型分叉。 ## 5. 前端 | 文件 | 职责 | |------|------| | `app/pages/@[publicSlug]/about/index.vue` | 请求上述 GET;渲染 Markdown(与主页 bio 使用同一套渲染组件或工具,避免两套样式分叉);处理 404(`UAlert` / 现站一致)。 | | `app/pages/@[publicSlug]/index.vue` | 条件展示链向 `/${slug}/about` 的入口(见 §2)。 | **SEO**:轻量 `title` / `description`(如昵称 + 「关于」),不做重投入。 ## 6. 非目标 - 时光机 `body_markdown`、评论等非 posts/profile 的引用扩展(可另开 spec)。 - 将生平迁入 `posts` 表或单独 slug 文章。 - 公开 bio 为空时仍展示占位 about 页。 ## 7. 测试与验收 - **手工**:公开用户含站内图的 bio,保存资料后孤儿列表**不**将该图标为孤儿;从 bio 中移除图片并保存后,按宽限期规则进入孤儿流。 - **手工**:`/@slug/about` 在 bio 公开且非空时可访问;bio 改 private 或清空后该 URL **404**。 - **回归**:文章创建/更新/删除后引用与孤儿行为与改版前一致;删除 post 后对应 `owner_type=post` 引用清空。 ## 8. 自检记录 - **占位符**:无 TBD。 - **一致性**:about 与 bio 同源;404 规则与「仅公开非空」一致;引用与孤儿定义全局统一。 - **范围**:单页 + 引用表演进 + profile 同步;不含其他 owner 类型。 - **歧义**:`owner_id` 在 `profile` 下等于 `users.id`;在 `post` 下等于 `posts.id`。实现须在代码层用枚举约束 `owner_type`。