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>
This commit is contained in:
浅念
2026-09-22 22:42:00 +08:00
parent dbf958613f
commit 37d88da4bc
85 changed files with 1149 additions and 3 deletions
@@ -0,0 +1,123 @@
# 用户列表分页与同步信息展示 实施计划
> **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 先例):
```sql
-- G4 后续:映射表增 profile JSON 列——同步源档案快照(工号/邮箱/公司/头像),
-- 纯展示维度:不参与同步决策(引擎幂等仍只看 sourceVersion),users 表零改动
--(源身份隔离红线)。nullable:mock 源/历史行无快照。
ALTER TABLE `external_user_mappings` ADD COLUMN `profile` JSON NULL;
```
- [ ] user-source.port.ts:
```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 映射加:
```ts
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 栈/观众页任何改动。