You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

7.1 KiB

设计:公开「关于我」详情页 + 资料媒体引用(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/aboutlayout: 'public',与现有 @[publicSlug]/* 一致。
可访问性 仅当 bioVisibility === 'public'bioMarkdown 非空(经 trim) 时返回页面 200;否则 404(与「无公开简介则不提供该 URL」一致,避免空壳与权限歧义)。
publicSlug 无效、用户非 active404,与现公开 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 integerNOT NULL,外键 media_assets.idON DELETE CASCADE(与现行为一致)。

主键(owner_type, owner_id, asset_id)

索引:保留 asset_id 上索引,供「某 asset 是否仍被引用」批量查询。

外键说明:SQLite 难以对 owner_id 做多态外键;owner_id 声明指向 posts/users 的双向外键。参照完整性由应用层保证:

  • postowner_id 必须对应当前存在的 posts.id,且在 删除文章必须删除 owner_type='post' AND owner_id=<postId> 的全部行(再删 post 或依赖事务顺序),并照常对涉及 asset_id 调用 reconcileAssetTimestampsAfterRefChange
  • profileowner_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_assetsuser_id=userId 下解析 asset_id
  • 插入新行;对变更涉及的 asset_id 调用 reconcileAssetTimestampsAfterRefChange

updateProfile(或 PUT /api/me/profile 成功路径)中,当 bioMarkdownavatar 任一参与更新时,在事务提交后调用上述同步(顺序与现 syncPostMediaRefs 一致:先落库用户字段再同步引用)。

3.3 孤儿与删除

  • 孤儿定义media_assets 中某行 不存在 任意 media_refs 行以其为 asset_id(与现「无 post 引用」等价,扩展为「无任何 owner 引用」)。
  • reconcileAssetTimestampsAfterRefChange:对每个 assetId 统计 全表 media_refsasset_id 计数,替代现仅查 post_media_refs 的逻辑。
  • 手动删除媒体 / 全局清扫:删除前「仍被引用」判定改为 任意 media_refs 计数 > 0;错误文案可泛化为「资源仍被引用」或区分 owner(可选,非必须)。

3.4 数据迁移

  • 将现有 post_media_refs 全量迁移为 owner_type='post'owner_id=post_idasset_id 不变。
  • 部署后可选:后台任务或懒执行 对已有用户跑一次 syncProfileMediaRefs,修复历史简介/头像中的站内图未登记问题(实现计划写明是否必须上线即跑)。

4. API

推荐新增轻量接口(避免 about 页拉全量 profile 聚合):

方法 路径 行为
GET /api/public/profile/:publicSlug/about 当且仅当 slug 有效、用户 active、且公开 bio 非空时返回渲染所需字段(如 user 展示子集、bio: { markdown });否则 404

字段形状与现 GET .../profile/:publicSluguser / 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_idprofile 下等于 users.id;在 post 下等于 posts.id。实现须在代码层用枚举约束 owner_type