Files
photo-live/docs/plans/2026-09-22-用户列表分页与同步信息展示-实施计划.md
T
浅念 37d88da4bc docs(ui05,ui06): admin analysis+HTML preview, app-preview evidence, UI06 unified visual plan
- admin-preview-2026-09-22/ 20页新版后台HTML原型+截图+validation(UI05 交付)
- app-preview-2026-09-22/ 截图/validation/check 脚本入库及小改
- admin-redesign-concept 概念图
- UI05 管理后台分析与新版设计说明、用户列表分页计划文档
- UI06 全端视觉统一实施方案(以两个 HTML 预览为唯一视觉基准)
- services/api g4-sync-once 手动同步脚本

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-22 22:42:00 +08:00

10 KiB
Raw Blame History

用户列表分页与同步信息展示 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 管理后台用户列表换成标准页码分页(页码/每页条数/总数),并把同步源带来的工号/邮箱/公司/头像等档案信息落库展示。

Architecture: 同步快照存 external_user_mappings.profile(新增 nullable JSON 列——守「users 表不加源身份列」红线,源数据隔离在映射表);端口 ExternalUserRecord 增可选 sourceProfile,引擎建/更映射时随版本写入。listAdminUsers 管理列表模式新增 page/pageSize 偏移分页并返回 total(游标模式原样保留,契约只增不改);AdminUser 增可选 synced 对象。前端用户列表换 ElPagination 页码分页 + 新列(工号/邮箱/公司,头像并入姓名列)。

Tech Stack: Prisma 手写迁移(utf8mb4_090_bin 约定)、openapi.yaml + pnpm gen、NestJS、Vue3 + Element Plus、i18n(apps/admin/src/locales/langs/{zh,en}.json)。

红线遵循: users 表零改动;同步决策绝不依赖 profile(纯展示快照,坏 profile 不影响行应用);契约字段全部 optional/nullable(旧客户端零 break);ov 栈不涉及(用户域 cn 单域)。

文件结构

文件 动作
packages/db-cn/prisma/schema.prisma + 新迁移目录 external_user_mappings += profile Json?
services/api/src/user-sync/user-source.port.ts ExternalUserProfile + sourceProfile?
services/api/src/user-sync/http-user-source.ts 填充 sourceProfile
services/api/src/user-sync/mock-user-source.ts MockExternalRecord += sourceProfile?(透传)
services/api/src/user-sync/user-sync.service.ts 映射 create/update 写 profile
packages/contracts/openapi.yaml + gen AdminUser.synced、AdminUserList.total、page/pageSize
services/api/src/admin/admin-users.service.ts page 模式(COUNT+OFFSET)+ synced 合并 helper;治理回显带 synced
services/api/src/admin/users-lookup.controller.ts page/pageSize 解析透传
services/api/test/user-sync.e2e-spec.ts profile 落库/随版本刷新断言
services/api/test/admin-users.e2e-spec.ts(或 users-lookup) page 分页 + synced 展示断言
apps/admin/src/types/api/adminUser.d.ts、api/adminUsers.ts 类型与参数
apps/admin/src/views/system/users/index.vue ElPagination + 新列
apps/admin/src/locales/langs/{zh,en}.json 新文案

Task 1: DB 迁移 + 端口/引擎/适配器写入 profile

  • schema.prisma external_user_mappings 加 profile Json?(注释:同步源档案快照,纯展示,不参与同步决策)
  • 新迁移 packages/db-cn/prisma/migrations/20260922<HHMMSS>_mapping_profile/migration.sql(手写,格式对齐 20260920114149 先例):
-- G4 后续:映射表增 profile JSON 列——同步源档案快照(工号/邮箱/公司/头像),
-- 纯展示维度:不参与同步决策(引擎幂等仍只看 sourceVersion),users 表零改动
--(源身份隔离红线)。nullable:mock 源/历史行无快照。
ALTER TABLE `external_user_mappings` ADD COLUMN `profile` JSON NULL;
  • user-source.port.ts:
/** 同步源档案快照(纯展示,不参与同步决策;http 源填充,mock 可不填)。 */
export interface ExternalUserProfile {
  /** 工号。 */
  sn?: string;
  /** 企业邮箱。 */
  email?: string;
  /** 公司全称。 */
  companyName?: string;
  /** 公司简称。 */
  companyAbbr?: string;
  /** 头像 URL。 */
  avatar?: string;
}

ExternalUserRecord 加 sourceProfile?: ExternalUserProfile;(注释:引擎原样落 mapping.profile,绝不据此做同步决策)。

  • http-user-source.ts records 映射加:
      sourceProfile: {
        sn: raw.sn || undefined,
        email: raw.email || undefined,
        companyName: raw.company_name || undefined,
        companyAbbr: raw.company_abbr || undefined,
        avatar: raw.avatar || undefined,
      },
  • mock-user-source.ts:MockExternalRecord 加 sourceProfile?: ExternalUserProfile;items 映射透传 sourceProfile: user.sourceProfile。
  • user-sync.service.ts 四处映射写入(新建关联 / 既有更新 / 重建改指 / 全新用户建映射)统一加 profile: item.sourceProfile ?? undefined(undefined=Prisma 不改列)。
  • 幂等跳过路径的 profile 回填(存量 230 用户已处于当前版本,版本门直接 return——不补写则 profile 永远 NULL):validated.version <= mapping.sourceVersion 分支内,若 item.sourceProfile 非空且与 mapping.profile 深比较不同 → 仅 external_user_mappings.update({ profile })(不动 sourceVersion/users 行/会话/counts,bucket 仍 null——展示维度补偿写,不违反「同步决策不依赖 profile」红线);文件头登记。
  • 验证:pnpm --filter @photo/api-cn typecheck;本地库应用迁移(prisma migrate dev 或 db push 对齐项目流程后跑 user-sync e2e)。
  • Commit: feat(api,db-cn): persist synced source profile snapshot on mappings

Task 2: 契约扩展 + 服务端 page 分页与 synced 合并

  • openapi.yaml:
    • AdminUser.properties 加 synced(type: [object, 'null'],readOnly,字段 sn/email/companyName/companyAbbr/avatar 均 string;描述:同步源档案快照,非同步用户为 null)。
    • AdminUserList allOf 内对象加 total(type: [integer, 'null'],描述:仅 page 模式返回的总数;游标模式为 null)。
    • listAdminUsers parameters 加 page(integer ≥1)与 pageSize(integer 1..100)均 optional;描述注明:提供 cursor 走游标模式(忽略 page);提供 page(无 cursor)走偏移分页并返回 total(此时 nextCursor=null 且 hasMore=false——守 CursorPageMeta「hasMore⟺nextCursor≠null」不变式,翻页由 total 驱动)。
    • pnpm --filter @photo/contracts gen 重新生成类型(根上无 gen 脚本)。
  • users-lookup.controller.ts:parsePage(未提供 → undefined,提供则 ≥1)/parsePageSize(未提供 → undefined,提供则 1..100);page 模式内的缺省值(1/20)在服务层 page 模式选定后才应用——保证「未带 page 的旧客户端仍走游标模式」。
  • admin-users.service.ts:
    • list(user, query, status, cursor, limit, page?: number, pageSize?: number):page !== undefined && cursor === undefined → page 模式:同一 WHERE(去游标条件)COUNT(*) + LIMIT pageSize OFFSET (page-1)*pageSize,nextCursor=null、hasMore=false、total;否则原游标逻辑(total=null)。
    • 导出 attachSyncedProfiles(items: AdminUserItem[]): Promise<AdminUserItem[]>:按 id 批查 external_user_mappings(userId IN),每用户取 sourceVersion 最大行,profile → synced(无映射/profile → synced: null);list 两种模式与治理回显(update/activate/deactivate)统一套用;前缀 lookup 模式不套(邀请候选无需档案,文件头注明)。
  • e2e:admin-users/users-lookup spec 增用例——直种 2 用户 + 1 映射带 profile → ?page=1&pageSize=1 返回 total=2、单条且 synced 字段正确;?page=2 返回第二条、hasMore=false;游标模式(不带 page)total 为 null;无映射用户 synced null;前缀模式(accountPrefix)响应干净不含 synced/total。user-sync e2e:U2 场景行加 sourceProfile → ① 断言 mapping.profile 落库;② 改名场景换 companyAbbr → 断言 profile 随版本刷新;③ 同版本重跑(版本门跳过)+ profile 此前为空 → profile 被回填且 counts 全 0(存量用户回填路径)。
  • 验证:typecheck + test -- http-user-source + test:e2e -- user-sync admin-users users-lookup。
  • Commit: feat(contracts,api): page-mode pagination + synced profile on admin users list

Task 3: 管理后台前端(分页器 + 新列)

  • types/api/adminUser.d.ts:Item 加 synced?: {...} | null;ListResponse 加 total?: number | null;ListParams 加 page?/pageSize?。
  • api/adminUsers.ts:params 直传(类型已覆盖)。
  • views/system/users/index.vue:
    • 去 useCursorList/loadMore,改 items/total/page/pageSize/loading/error + loadPage();search()/resetFilters() 重置 page=1;分页器 ElPagination(layout total, sizes, prev, pager, next,sizes [20,50,100])。
    • 列调整:姓名列并入头像(ElAvatar src=synced?.avatar,无则首字符);新增 工号(synced.sn)、邮箱(synced.email)、公司(companyAbbr || companyName)三列;「—」占位非同步用户。
    • 行内治理操作(编辑/重置/激活/停用/解锁)原样保留,回显 replaceItem 兼容。
  • locales zh/en:usersManage.list.colSn/colEmail/colCompany、分页相关沿用 Element 内建文案。
  • 验证:admin 前端 typecheck/build(pnpm --filter admin build 或项目等价脚本)。
  • Commit: feat(admin): standard pagination + synced profile columns on users list

Task 4: 部署测试服务器(server + cn web 双镜像 + 迁移)

  • server 镜像 git archive HEAD | docker build -f docker/server.Dockerfile,tag <sha>-20260922b;web 镜像同法 docker/web.Dockerfile,tag admin-<sha>-20260922b(ov 栈 web 保持 viewer-fix 不动——本任务无 viewer 改动,避免吞掉未提交 viewer 工作)。
  • save/gzip → scp → load。
  • 服务器先迁移:docker run --rm --network 1panel-network --env-file .env.production-cn photo-live-server:<tag> migrate-deploy all。
  • cn 栈 SERVER_IMAGE=<新> WEB_IMAGE=photo-live-web:admin-<sha>-20260922b up -d;ov 栈仅换 SERVER_IMAGE(WEB_IMAGE=viewer-fix)。
  • 验证:http://219.157.255.213:32005/admin/ 用户列表分页可翻页、工号/邮箱/公司/头像显示(230 真实用户);/v1/admin/users?page=1&pageSize=2 带 total;user-sync status 正常(下一 cron 批不因 profile 列失败)。
  • 补验收记录(G4 记录追加或新条目),Commit:docs: ...

明确不做(YAGNI)

  • 相册等其他列表的分页交互改造(同为游标"加载更多"模式)——用户列表先行,需要再说。
  • synced 信息进用户编辑弹窗(列表展示已覆盖诉求)。
  • ov 栈/观众页任何改动。