7.1 KiB
设计:公开「关于我」详情页 + 资料媒体引用(ownerType)
日期:2026-04-18
状态:已定稿(与产品选择一致:方案 B + 公开路由 ①)
1. 背景与目标
- 公开主页
/@:publicSlug已展示users.bio_markdown(在bioVisibility === public时经聚合接口下发),但没有独立路由承载「生平」长文的阅读体验。 - 文章图片的引用真相在
post_media_refs;个人简介 Markdown 与头像 URL 若包含本站/public/assets/...资源,当前不会同步到引用表,孤儿审查可能误判或漏判。
目标:
- 新增公开路由
/@:publicSlug/about,用于专注展示已公开的生平 Markdown(与现有bio数据同源,不复制到posts)。 - 扩展媒体引用模型:用
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=<postId>的全部行(再删 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。