Files
geo/docs/superpowers/specs/2026-04-18-kol-profile-page-design.md
T
root 26dd75e548 docs: add KOL profile page design spec
- KOL 工作台新增「个人主页」独立页,明确与租户/SaaS 账号设置区分
- 定位为精调模版展示页 + KOL 人设自助编辑
- 页面内联编辑、头像复用 image_service、仅展示已发布模版
2026-04-18 13:55:20 +08:00

14 KiB
Raw Blame History

KOL 个人主页(对外人设信息,可编辑)设计

  • 日期:2026-04-18
  • 涉及端:apps/admin-webserver/internal/tenant
  • 负责:自服务(KOL 本人操作)

1. 背景与目标

现有「KOL 工作台」(kolWorkspace 导航组,仅 requiresKol 用户可见)包含两页:

  • /kol/manage:管理 Package / Prompt(精调模版)
  • /kol/dashboard:订阅与消耗数据看板

KOL 当前在「KOL 市场」上展示的人设(display_name / bio / avatar_url)没有任何自助编辑入口,必须依赖后台管理员或脚本改库。

本次新增一个 KOL 个人主页:展示并允许 KOL 自助编辑自己对外的人设信息。页面只做这一件事。

非目标(明确不做)

  • 不涉及租户/SaaS 账号设置:不改密码、邮箱、租户名、用户昵称等;这些归属于账号系统,本页与之物理与语义都隔离。
  • 不做访客态的公开主页(/u/:kol_id 这类 public URL),本次仅做 KOL 自己进入的自助页。
  • 不做已发布精调模版的展示/预览(用户明确不要),如需查看作品入口仍在 /kol/manage
  • 不做市场可见状态(market_enabled)开关,避免与后台审批逻辑冲突;本期连只读展示也不做,保持纯粹。
  • 不做订阅者、消耗等统计(已在 /kol/dashboard)。

2. 与"租户/SaaS 个人信息"的区分

维度 本次新增:KOL 个人主页 租户/SaaS 账号设置(现有/未来)
定位 对外人设(KOL 市场可见的身份信息) 登录账号与租户系统设置
数据源 kol_profile 表(display_name / bio / avatar_url users / tenants
路由 /kol/profile 与本页无关,保持现状
导航分组 kolWorkspacerequiresKol: true 顶部头像/设置入口
可访问性 仅当前用户是 active KOL 所有登录用户
页面副标题 "此处信息公开展示在 KOL 市场,与账号设置无关" ——

页面内通过副标题显式告知用户:"这不是你的账号设置,这是你在 KOL 市场上别人看到的你"。

3. 页面布局

路由:/kol/profile,名 kol-profilemeta.requiresKol = truemeta.navKey = "/kol/profile"

页面极简,只有一张人设卡片:

┌─────────────────────────────────────────────────────────┐
│  KOL 个人主页                                            │
│  此处信息公开展示在 KOL 市场,与账号设置无关             │
├─────────────────────────────────────────────────────────┤
│  ┌─ 人设卡片 ───────────────────────────────────────┐   │
│  │                                                    │   │
│  │   [ 头像 ]      显示名(hover 出现铅笔,点击编辑)│   │
│  │                                                    │   │
│  │                 简介(hover 出现铅笔,点击编辑)  │   │
│  │                 多行,支持换行                    │   │
│  │                                                    │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

3.1 标题区

  • 标题:kol.profile.title = "KOL 个人主页"
  • 副标题:kol.profile.subtitle = "此处信息公开展示在 KOL 市场,与账号设置无关"

3.2 人设卡片(页面内联编辑)

三个可编辑字段,使用 Ant Design Vue 的 a-typography-paragrapheditable 能力或等价自研方案;交互要点:

字段 控件 校验 保存时机
avatar_url 头像圆形 120×120,悬浮"更换头像"遮罩,点击触发文件选择器 仅允许 image/*;最大 2MB;对象存储白名单域 上传完成后立即触发 updateProfile 落库
display_name hover 铅笔 → 单行 input 必填;trim().length 1-40 失焦 或 回车
bio hover 铅笔 → textareaautosize 2-6 行) 选填;≤ 500 字符 失焦 或 ⌘/Ctrl + Enter

细节:

  • 校验失败:inline 红字,不退出编辑态,保留用户输入。
  • 保存成功:message.success('已更新'),卡片局部视觉无跳动。
  • 保存失败:message.error(formatError(err)),退回编辑前值,但保留用户当前输入给用户重试。
  • 保存期间:当前字段 disabled + loading 小 spinner。
  • 头像上传失败:遮罩隐藏、提示错误,avatar_url 不变。
  • 头像上传成功但 updateProfile 失败:文件留在对象存储(孤儿),UI 回滚到原 avatar_url;接受该小量孤儿(与现有文章图片策略一致)。

交互选择的理由:页面只有一张卡片,不需要统一"保存"按钮;每字段各自失焦保存最贴近"我就改一个地方"的使用场景,路径最短。

4. 后端改动

4.1 新增接口:更新 KOL 人设

  • MethodPUT
  • Path/api/tenant/kol/manage/profile
  • 认证:requiresAuth + handler 内调用 profileSvc.RequireActiveKol
  • 入参 JSON
    {
      "display_name": "string, required, 1-40",
      "bio": "string|null, 0-500",
      "avatar_url": "string|null, must belong to our image_service domain"
    }
    
  • 出参:更新后的 KolProfileResponse
  • 实现要点:
    • KolProfileService 新增 UpdateProfile(ctx, actor, input) (*KolProfile, error)
      • RequireActiveKol 拿到 profile(保证 user_id → profile_id 映射 + status=active
      • repo.Update(ctx, actor.TenantID, UpdateKolProfileInput{ID: profile.ID, DisplayName, Bio, AvatarURL})repo 方法已存在,见 kol_profile_repo.go:94
      • 重新 GetByID 返回最新值
    • Handlerkol_manage_handler.go 新增 UpdateProfile(c *gin.Context),复用 actorFromRequestresponse.Success 风格。
    • Routerserver/internal/tenant/transport/router.goGET /tenant/kol/manage/profile 旁新增 PUT 绑定(同一路径 + 不同方法)。

校验错误码:

  • 40001 kol_profile_invalid_display_namedisplay_name 去空白后为空或 > 40
  • 40002 kol_profile_invalid_biobio > 500
  • 40003 kol_profile_invalid_avatar_urlavatar_url 非空且不在白名单域

白名单域:沿用 image_service 已有配置;具体字段名 Plan 阶段确认。

4.2 头像上传:复用 image_service

  • 新增 POST /api/tenant/kol/manage/profile/avatarmultipart/form-data,字段 file):
    • 前端先用已有 convertImageFileToWebp 压成 webp 后再上传(与 articlesApi.uploadImage 同路径)。
    • 后端复用 image_service,归类 folder = "kol-avatar" / ownerType = "kol_profile";返回 { url, object_key, content_type, size_bytes }
    • 本接口不自动写 avatar_url 入库:前端拿到 url 后,立刻发 PUT /api/tenant/kol/manage/profile { avatar_url } 落库。单一写入点,避免部分成功。

Plan 阶段核查:image_service 是否已支持"非 article 归属"的归类;若不支持,补一个最小化 scoped-to-kol-profile 的上传路径。

4.3 不改动

  • 不新增市场端接口。
  • 不改 kol_profile 表结构。
  • 不改 market_enabled 语义。

5. 前端改动

5.1 路由

apps/admin-web/src/router/index.ts:在 kol/dashboard 之后插入:

{
  path: "kol/profile",
  name: "kol-profile",
  component: () => import("@/views/KolProfileView.vue"),
  meta: {
    titleKey: "kol.profile.title",
    navKey: "/kol/profile",
    requiresKol: true,
  },
},

5.2 导航

apps/admin-web/src/layouts/AppShell.vuekolWorkspace 分组末尾追加一项:

{ key: "/kol/profile", label: t("nav.kolProfile") }

5.3 API 客户端

apps/admin-web/src/lib/api.tskolManageApi 新增:

updateProfile(body: {
  display_name: string;
  bio?: string | null;
  avatar_url?: string | null;
}): Promise<KolProfile>

uploadAvatar(file: File): Promise<{
  url: string;
  object_key: string;
  content_type: string;
  size_bytes: number;
}>

uploadAvatar 内部调用已有 convertImageFileToWebp,与 articlesApi.uploadImage 同构。

5.4 新组件

  • apps/admin-web/src/views/KolProfileView.vue(主页,薄壳:header + 一张卡片)
  • apps/admin-web/src/components/kol/KolPersonaCard.vue(人设卡片 + 三字段内联编辑;单一职责,便于单测)

页面总体不超过 ~250 行代码。

5.5 状态

  • useQuery(["kol", "profile"], kolManageApi.profile) 读取。
  • useMutation(kolManageApi.updateProfile) 写入,成功后:
    • invalidateQueries({ queryKey: ["kol", "profile"] })
    • invalidateQueries({ queryKey: ["workspace", "kol-cards"] })(工作台 KOL 卡片用到 kol_display_name / kol_avatar_url
    • invalidateQueries({ queryKey: ["kol", "packages"] })(包列表响应里带了 kol_display_name/kol_avatar_url
  • useMutation(kolManageApi.uploadAvatar) 独立:上传成功后立即链式触发 updateProfile({ avatar_url: url, display_name: current, bio: current })
  • 内联编辑的"正在编辑哪个字段"用组件内局部 ref<'display_name' | 'bio' | null> 管理,不落全局 store。

6. i18n

apps/admin-web/src/i18n/messages/{en-US,zh-CN}.ts 新增:

nav.kolProfile
kol.profile.title
kol.profile.subtitle
kol.profile.persona.displayNameLabel
kol.profile.persona.displayNamePlaceholder
kol.profile.persona.bioLabel
kol.profile.persona.bioPlaceholder
kol.profile.persona.avatarLabel
kol.profile.persona.avatarUploading
kol.profile.persona.saveSuccess
kol.profile.persona.saveError
kol.profile.persona.displayNameRequired
kol.profile.persona.displayNameTooLong
kol.profile.persona.bioTooLong
kol.profile.persona.avatarInvalidType
kol.profile.persona.avatarTooLarge
kol.profile.persona.notActive

两种语言同步添加;具体文案 Plan 阶段定稿。

7. 数据流

 Browser                              Server
 ───────                              ──────
 进入 /kol/profile
   │
   └── GET /api/tenant/kol/manage/profile   → KolProfile

 用户点击头像 → 选择文件
   ├── 前端 convertImageFileToWebp
   ├── POST /api/tenant/kol/manage/profile/avatar (form-data)
   │        → { url }                  (落对象存储,未写 DB)
   └── PUT  /api/tenant/kol/manage/profile
            { avatar_url, display_name: current, bio: current }
            → KolProfile
   ⇢ invalidate ["kol","profile"] / ["workspace","kol-cards"] / ["kol","packages"]

 用户编辑 display_name / bio → 失焦(或回车)
   └── PUT  /api/tenant/kol/manage/profile
            { display_name, bio, avatar_url: current }
            → KolProfile
   ⇢ 同上 invalidate

PUT 总是发送三字段的全量值,后端 repo.Update 对应 SQL 也是整行覆写,避免部分字段被旧值覆盖。

8. 错误处理

  • 网络失败:message.error(formatError(err)),回滚到编辑前值,保留当前输入。
  • 校验失败(后端):按错误码定位到字段,inline 红字;不退出编辑态。
  • requiresKol 守卫:router.beforeEach 已处理。
  • 后端运行时返回 ErrNotActiveKol:前端兜底 message.error(t('kol.profile.persona.notActive')) 并路由回 /workspace

9. 测试

后端

  • kol_profile_service 新增单测:
    • UpdateProfile 成功
    • 非 active KOL 拒绝
    • display_name 空 / 超长各自返对应错误码
    • bio 超长返对应错误码
    • avatar_url 非白名单返对应错误码
  • kol_manage_handler 新增 handler 测试(遵循现有 handler_test 风格)
  • 若新增 avatar 上传路径:image_service 覆盖"非 article 归属"分支

前端

  • KolPersonaCard 单测:显示名为空时报错、超长时报错、保存成功后退出编辑态、保存失败回滚。
  • KolProfileView 页面级测试:加载态、加载失败态、正常态。
  • 若项目已有 E2E:覆盖"换头像 → 改显示名 → 改 bio → 刷新工作台 KOL 卡片同步";无则留作后续。

手动验收

  1. 普通用户访问 /kol/profile → 跳回 /workspace
  2. active KOL 访问 → 正常渲染人设卡片
  3. 改头像 / 显示名 / bio 各自触发保存 → 成功
  4. /workspace 面板中的 KOL 卡片 kol_display_name / kol_avatar_url 同步更新
  5. 控制台无报错、无 i18n 缺失告警

10. 风险与权衡

  • 内联编辑 vs Modal:页面只一张卡片,Modal 冗余;内联编辑 + 失焦保存最短路径,代价是对校验文案与回滚逻辑要求较高。采用"错误不退出编辑态 + 失败回滚编辑前值"的 UX。
  • 每字段独立保存 vs 全局"保存"按钮:独立保存减少心智负担;全局按钮更"事务"。选前者,因为本页目的是"顺手改一处",不是一次性录入。
  • 头像白名单:严格白名单可防盗链/XSS,但对象存储域名变更时需同步改后端配置;风险可控。
  • 缓存同步display_name / avatar_url 改动后需手动 invalidateQueries 多个 keyPlan 阶段勿漏。
  • 孤儿头像文件:头像上传成功但随后 updateProfile 失败时会遗留对象存储文件;与现有文章图片策略一致,接受。

11. 成功判据

  • KOL 可在不经后台的情况下自助更新 display_name / bio / avatar_url
  • 页面语义上与账号设置清晰隔离(文案、路由、导航分组、数据源四位一体)。
  • 更新后,工作台、KOL 市场等现有页面引用到的 display_name / avatar_url 实时一致。
  • 页面代码简洁(单主页 + 单卡片组件),不堆砌无关功能。