flnxi-Wu/docs/商业化地基排查问题清单_20260524.md
2026-05-30 20:17:36 +08:00

156 KiB
Raw Blame History

商业化地基排查问题清单

更新时间:2026-05-30

本文件是当前项目面向企业/团队私有化交付前的历史整改台账。它不是对外销售文案,也不是旧参考源码功能吸收清单,更不是当前默认事实入口。已修复、已完成和待整改条目会同时保留,阅读时必须先看每条状态,不能把整份文件理解成纯待办清单。

本次排查以当前仓库代码、配置、部署文件和现存文档为准。gongxiangp2-source-reference-old-style-20260513-132942 只作为只读参考目录,本次没有把旧项目 UI 或旧功能直接作为目标。

1. 当前项目真相

  • 当前主应用是 Next.js + Prisma + PostgreSQL + MinIO/S3 的私有化素材库。
  • 客户侧目标环境默认是 NAS 或 Linux 服务器,通过 Docker/Podman 运行,普通用户只通过浏览器使用系统。
  • 当前标准部署入口是 docker-compose.prod.yml + .env,会拉起 PostgreSQL、MinIO、MinIO 初始化容器和主应用。
  • 许可证服务在 license-server/,主应用通过许可证接口做激活、校验、心跳和功能限制。
  • 上传模板是真实存在的核心能力,模板和字段有数据库表,素材表有固定列,也有 metadata 保存自定义字段。
  • 当前前端已经有自己的风格系统,主要在 lib/ui-styles.ts、lib/hooks/useThemeStyles.ts、components/ui/*,不应照搬旧参考源码的老界面。

不作为当前吸收目标的旧功能:

  • 任务号管理
  • 白底图
  • 条码绑定
  • 成套完整性判断
  • 外部系统对接

2. 优先级口径

  • P0:会直接影响客户数据安全、客户安装升级、核心功能验收,必须优先处理。
  • P1:不会马上让系统不可用,但会造成商业交付不稳、体验割裂、权限边界混乱,应进入下一轮整改。
  • P2:维护性、体验一致性、长期质量问题,可以排期处理。

3. P0 问题

P0-01 系统设置接口曾暴露对象存储密钥(已修复)

客户影响:

这个问题之前会让对象存储的 Access Key / Secret Key 暴露给非管理员读取者,属于直接的数据安全风险。当前代码已继续收紧:公开读取只返回站点名、Logo、背景色等展示字段;普通管理员可读取后台必要开关和非密钥配置;对象存储 Access Key / Secret Key 只有最高权限账号才能读取。

证据:

  • app/api/system-settings/route.ts:现在会按会话区分公开字段、普通管理员字段和最高权限账号字段。
  • app/api/system-settings/route.ts:普通管理员响应会移除 s3AccessKey 和 s3SecretKey,最高权限账号才返回这两个字段。
  • app/api/system-settings/route.ts:GET 现在只读,缺少设置记录时只返回内存默认值,不再创建默认行;旧 Logo 也只在响应中整理,不再通过 GET 写回数据库。
  • app/api/system-settings/route.ts:未登录公开设置不再返回 enableRegistration 等功能开关;登录页和注册页不再从公开接口暴露普通自助注册策略,带邀请码注册仍由注册接口按数据库设置和邀请链接校验。
  • app/api/system-settings/__tests__/route.test.ts:测试锁定未登录只返回公开字段、普通管理员不返回对象存储密钥、最高权限账号才返回对象存储密钥,并确认 GET 不再补写对象存储密钥、不创建默认设置、不回写 Logo、不执行运行期 DDL、不公开功能开关。
  • app/api/auth/register/__tests__/route.test.ts:已有测试锁定关闭公开注册时无邀请码拒绝、有有效邀请码仍可注册。
  • prisma/schema.prisma:320:底层表结构仍明文保存 s3SecretKey,所以密钥读取/修改继续按最高权限账号处理。

建议动作:

  • 保持当前三层读取方式,不要回退到公开或普通管理员返回密钥。
  • 后续如果需要更严格,可把最高权限账号视图也改成脱敏展示,仅允许通过“重填新密钥”方式修改。

P0-02 API 可以绕过登录读取私有素材数据(已修复)

客户影响:

这个问题之前会让外部绕过页面登录,直接请求接口拿到素材列表、素材详情、预览地址、上传者信息和模板信息。这和企业私有化系统的基本边界冲突。当前已先把内部工作台数据接口收回到登录态之后,公开分享仍保留独立接口,不复用内部素材 API。

证据:

  • app/api/materials/route.ts:素材列表 GET 现在未登录直接返回 401,不再进入素材查询。
  • app/api/materials/[id]/route.ts:素材详情 GET 现在未登录直接返回 401,不再读取单个素材详情。
  • app/api/templates/route.ts、app/api/upload-templates/route.ts、app/api/home-filter-fields/route.ts:模板列表、上传模板和首页筛选字段 GET 现在都要求登录。
  • app/author/[authorName]/page.tsx:作者页现在也跟随私有素材库口径,未登录会跳转登录页,避免页面依赖已收紧的内部素材接口后出现空白。
  • app/api/materials/share/route.ts:公开分享已走独立分享票据接口,不再只靠裸素材 ID 读取分享数据。
  • app/api/materials/__tests__/materials-list-auth.test.ts、app/api/materials/__tests__/template-fields-routes.test.ts、app/api/templates/__tests__/templates-route-auth.test.ts、app/api/upload-templates/__tests__/route-auth.test.ts、app/api/home-filter-fields/__tests__/route-auth.test.ts:新增/补充测试,锁定未登录拒绝访问并且不触发数据库读取或字段池同步。

建议动作:

  • 保持当前规则:私有素材数据默认必须登录。
  • 分享页已在 P0-03 中接入第一版分享票据,后续仍要补撤销管理和更细的分享权限策略;下载和登录后播放已按私有化边界先行收紧。
  • 后续如果确实要做公开作者页,应单独设计公开接口和可控字段,不再复用内部 /api/materials。

P0-03 MinIO/S3 对象当前按公开读取设计(已完成默认私有化、分享票据、前端撤销和 MinIO 匿名访问收口)

客户影响:

即使业务接口加了登录,如果对象存储本身公开下载,拿到文件 URL 的人仍能绕过系统直接访问素材文件。当前已完成默认私有化、内部播放、分享票据、前端撤销、系统级公开分享开关和 MinIO 匿名访问收口:新部署和升级初始化默认会把素材 bucket 的匿名访问设为 none;新上传不再默认设置 public-read;内部素材预览类 URL 默认改为应用内代理地址;视频/音频流接口默认要求登录;公开分享必须携带短期分享票据;素材分享链接管理已复用统一素材管理范围,当前负责人、个人归属人、最高权限账号和管理部门范围内管理员可以创建、查看和撤销该素材的分享链接,普通管理员不能跨管理部门操作;最高权限账号可以在系统设置里关闭公开分享,关闭后不能新建外部分享链接,已有分享链接也不能继续公开访问。

证据:

  • docker-compose.prod.yml:69:MinIO 初始化时设置 alias。
  • docker-compose.prod.yml:70:创建素材 bucket。
  • docker-compose.prod.yml:MinIO 初始化会默认执行 mc anonymous set none,新安装和升级初始化都会主动关闭素材 bucket 匿名下载。
  • docker-compose.prod.yml、.env.example:S3_SET_ACL 默认改为 0,只有显式开启才走兼容的公开对象模式。
  • docker-compose.prod.yml:只有显式把 S3_SET_ACL 或旧兼容变量 S3_PUBLIC_READ 设置为 1/true/yes 时,MinIO 初始化才会执行 mc anonymous set download,并打印公开读取兼容警告。
  • lib/s3.ts:uploadToS3() 默认不再写 ACL: 'public-read';代码自动创建 bucket 时默认也不再写公开读取 policy。
  • app/api/materials/asset/route.ts:新增登录后的素材文件代理,只允许读取素材表中已记录的文件地址,避免变成通用文件读取口。
  • lib/url-rewrite.ts:内部素材接口返回给前端的 previewUrl、displayImages、videoCoverUrl、videoPreview540Url、videoPreviewClipUrl、audioUrl、zipFilePath 默认改成 /api/materials/asset?...。
  • app/api/user/favorites/route.ts、app/api/user/likes/route.ts、app/api/user/history/route.ts:个人中心相关素材列表也接入代理 URL。
  • prisma/schema.prisma、prisma/migrations/20260524102000_add_material_share_tokens/migration.sql:新增素材分享票据表,只保存 token hash,并记录素材、创建人、过期时间和撤销时间。
  • lib/material-share-token.ts:新增分享票据创建和校验逻辑,公开访问只接受未过期、未撤销且素材未软删除的票据。
  • lib/material-share-permissions.ts:分享管理权限已复用 lib/material-permissions.ts 的统一素材管理范围,不再单独放行所有 ADMIN。
  • app/api/materials/share/route.ts:创建分享链接需要登录且有统一素材管理权;公开读取分享数据必须携带有效分享票据;返回的预览图、展示图、视频/音频地址会改写为带分享票据的应用代理地址。
  • app/api/materials/share/tokens/route.ts:新增分享链接列表接口,可查看链接创建人、创建时间、过期时间、撤销时间和状态;不会返回 token 明文或 hash;普通管理员只能查看管理范围内素材的分享链接。
  • app/api/materials/share/tokens/[id]/route.ts:新增撤销分享链接接口,通过写入 revokedAt 让链接失效,不硬删除记录;普通管理员不能跨管理部门撤销分享链接。
  • app/page.tsx:分享弹窗已接入已有分享链接列表、到期时间、撤销状态、刷新和撤销操作。
  • app/share/[id]/page.tsx:公开分享页已隐藏下载按钮,避免外部访问者点击一个后端必然要求登录的下载入口;当前公开分享定位为可预览,不直接开放源文件下载。
  • prisma/schema.prisma、prisma/migrations/20260526090000_add_public_share_toggle/migration.sql:系统设置新增 enablePublicShare,默认开启,旧库升级会自动补列。
  • app/admin/_components/AppearanceSettingsPanel.tsx:后台“功能开关”新增公开分享链接开关,仅最高权限账号可修改。
  • lib/public-share-settings.ts:新增公开分享开关读取函数,读取失败时按关闭处理,避免数据库异常时误放开外部分享。
  • app/api/materials/share/route.ts:公开分享开关关闭时拒绝创建新分享票据,也拒绝旧分享票据继续读取分享数据。
  • app/api/materials/asset/route.ts:素材文件代理默认要求登录;未登录时必须带有效分享票据,且文件地址必须属于对应素材记录。
  • app/api/materials/asset/route.ts、app/api/materials/stream/route.ts:公开分享开关关闭时,未登录用户不能再通过旧分享票据读取素材文件或播放音视频;登录用户内部预览不受影响。
  • app/api/materials/stream/route.ts:视频/音频流接口默认要求登录;未登录时必须带有效分享票据,并只查询未软删除素材。
  • app/api/materials/stream/route.ts:S3 和本地 legacy 流响应已改为 Cache-Control: private, max-age=300,不再返回 Access-Control-Allow-Origin: *。
  • app/api/materials/share/__tests__/route.test.ts、app/api/materials/share/tokens/__tests__/route.test.ts、app/api/materials/share/tokens/[id]/__tests__/route.test.ts、app/api/materials/asset/__tests__/route.test.ts、app/api/materials/stream/__tests__/route.test.ts、lib/__tests__/material-share-token.test.ts、lib/__tests__/s3.test.ts、lib/__tests__/url-rewrite.test.ts:新增/更新测试,锁定默认私有上传、素材代理 URL、代理接口权限边界、分享票据校验、分享列表不泄露 token、撤销链接后失效、无票据播放拒绝、有票据播放可用且不返回通配 CORS。

建议动作:

  • 已完成:新安装和新上传默认不再主动公开素材对象。
  • 已完成:MinIO 初始化默认主动关闭素材 bucket 匿名访问,旧环境升级时可把历史 bucket policy 从公开下载拉回私有。
  • 已完成:内部视频/音频播放不再允许未登录裸 ID 访问,改为登录态内同源播放。
  • 已完成:公开分享不再只靠裸素材 ID,第一版改为“有权限用户创建分享票据,公开访问携带票据,票据短期有效并可后端撤销”。
  • 已完成:分享链接创建、列表查看和撤销已复用统一素材管理范围;普通管理员不能跨管理部门操作公开分享链接。
  • 已完成:主页分享弹窗已接入分享链接列表、有效期展示、撤销入口和刷新入口。
  • 已完成:后台新增系统级公开分享开关,关闭后不能新建外部分享链接,已有分享链接也会停止公开访问。
  • 已完成:公开分享页不再展示下载按钮;源文件下载继续走登录态和用户下载权限。
  • 仍需继续:外部 S3 或历史对象级 ACL 需要单独运维审计;MinIO 初始化只能处理当前 Compose 管理的 MinIO bucket 匿名策略。
  • 仍需继续:Logo、头像等公共资源和私有素材需要拆分策略;登录页 Logo 不能简单套用登录后的素材代理。

P0-04 CORS 反射任意来源且允许携带凭据(已修复主应用 API 和素材流)

客户影响:

如果浏览器里已有登录态,恶意网页可能更容易跨站调用本系统接口。它会放大其他公开接口和权限漏洞的风险。当前主应用 API 中间件已收紧:只允许同源、ALLOWED_ORIGINS、APP_PUBLIC_URL、NEXTAUTH_URL 中的来源拿到可携带凭据的 CORS 响应;素材流接口内部的通配 CORS 也已移除。

证据:

  • middleware.ts:新增 normalizeOrigin()、getAllowedCorsOrigin(),不再把请求里的 Origin 原样写回。
  • middleware.ts:陌生来源的 OPTIONS 预检请求返回 403。
  • middleware.ts:陌生来源的普通 API 响应不再返回 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials。
  • __tests__/middleware-cors.test.ts:新增测试,锁定白名单来源可用、陌生来源预检被拒、陌生来源普通响应不带 CORS 凭据头。
  • app/api/materials/stream/route.ts:已移除接口内部 Access-Control-Allow-Origin: *、Access-Control-Allow-Methods、Access-Control-Allow-Headers 和 Access-Control-Expose-Headers,内部播放回到同源登录态。
  • app/api/materials/stream/__tests__/route.test.ts:新增测试,锁定未登录且无分享票据时拒绝播放,携带有效分享票据时允许播放,且响应不返回通配 CORS。

建议动作:

  • 保持主应用 API 默认同源或白名单来源访问。
  • 部署文档中要求客户把真实入口写入 NEXTAUTH_URL / APP_PUBLIC_URL,额外跨域入口写入 ALLOWED_ORIGINS。
  • 对确有跨域需求的 Agent、许可证、分享场景单独开白名单。
  • 后续分享、Agent、许可证等确有跨域需求的入口要单独设计白名单和 token,不要在私有素材流接口里恢复 *。

P0-05 钉钉登录有固定自动登录密码和硬编码密钥(已修复)

客户影响:

这个问题之前会让钉钉账号绑定后使用同一套固定自动登录密码,甚至覆盖老用户原密码。当前已改为服务端一次性登录票据:钉钉验证成功后只返回短期、一次性 token 给当前前端换取系统会话,不再把固定密码写入用户表。钉钉应用编号和密钥也不再使用代码或 compose 里的硬编码默认值,缺少配置时后端直接拒绝钉钉登录。

证据:

  • lib/dingtalk-config.ts:钉钉配置只读取环境变量,缺少 DINGTALK_CLIENT_ID 或 DINGTALK_CLIENT_SECRET 时抛出配置错误。
  • app/api/dingtalk/login/route.ts:钉钉登录不再覆盖已有用户密码;新钉钉用户只写入随机不可知密码哈希;已激活用户返回一次性 loginToken;首次完善资料用户返回一次性 profileToken。
  • lib/dingtalk-login-token.ts:新增钉钉一次性票据,数据库只保存 token hash,消费时会检查用途、过期时间和是否已使用。
  • lib/auth.ts:NextAuth 的 credentials 登录入口新增 dingtalkToken 分支,只接受一次性登录票据,不接受固定密码。
  • app/login/page.tsx、app/dingtalk-callback/page.tsx、app/complete-profile/page.tsx:前端钉钉登录链路已改为使用一次性 token,不再调用 dingtalk_auto_login。
  • app/api/dingtalk/complete-profile/route.ts:完善资料不再接受裸 userId,必须带有效 profileToken。
  • docker-compose.yml:钉钉配置改为从 .env 读取,不再写默认应用编号和密钥。
  • prisma/schema.prisma、prisma/migrations/20260524000000_add_dingtalk_login_tokens/migration.sql:新增一次性票据表。
  • app/api/dingtalk/login/__tests__/route.test.ts、app/api/dingtalk/complete-profile/__tests__/route.test.ts、app/api/dingtalk/config/__tests__/route.test.ts、lib/__tests__/dingtalk-login-token.test.ts、lib/__tests__/auth-dingtalk-token.test.ts:新增测试,锁定未配置钉钉拒绝、老用户密码不被覆盖、新用户不写固定密码、前端配置接口不暴露密钥、完善资料必须使用 profile token、票据只存 hash 且只能消费一次、认证入口能用一次性票据换会话用户。

验证:

  • npm test -- app/api/dingtalk/login/__tests__/route.test.ts app/api/dingtalk/config/__tests__/route.test.ts app/api/dingtalk/complete-profile/__tests__/route.test.ts lib/__tests__/dingtalk-login-token.test.ts lib/__tests__/auth-dingtalk-token.test.ts --runInBand
  • npm run type-check
  • npx prisma validate
  • npx prisma generate --no-engine:通过。完整 npx prisma generate 在本机因 Windows Prisma 引擎 DLL 被占用失败,非模型错误。

建议动作:

  • 保持当前规则:钉钉登录只能通过服务端一次性票据换 session,不能再回到固定密码。
  • 客户环境必须显式配置自己的 DINGTALK_CLIENT_ID 和 DINGTALK_CLIENT_SECRET;未配置时钉钉入口应视为未启用。
  • 后续建议单独评估“按钉钉姓名自动绑定同名老用户”的冒领风险,商业环境更稳的做法是管理员确认绑定或用户用原账号登录后自行绑定。

P0-06 仓库仍存在敏感环境文件和弱默认值(已修复主要风险)

客户影响:

真实密钥、旧服务器配置或弱默认值留在仓库里,会被误用、泄露或写进交付包。当前已把仓库里的生产环境文件移除,把生产 compose 的关键密钥改为必填,并清理开发 compose / README 中容易被复制到客户环境的固定密钥示例。

证据:

  • .env.production:已从仓库删除;.gitignore 已加入 .env.production,避免后续再次误提交生产环境文件。
  • docker-compose.prod.yml:NEXTAUTH_SECRET 已改为必填校验,不再提供弱默认值。
  • docker-compose.yml:固定第三方 API key、固定内部密钥和钉钉默认密钥已改为从本机 .env 读取;未配置时为空。
  • README.md:本地环境示例已改为占位符格式,并明确不要把示例占位值用于客户环境。
  • .env.example:继续只保留占位值和空值,不写真实密钥。
  • deploy-new-fixed.ps1、deploy-new-fixed.ps1.bak-20260516-164124:临时占位配置中的旧对象存储地址和密钥已替换为明显不可用的占位值。
  • .gitignore、.dockerignore:已忽略 .env.production、本地 .env.*、脚本备份文件,避免后续误提交或进入 Docker 构建上下文。
  • 复扫结果:旧钉钉默认值、旧对象存储 access key / secret key、旧第三方 API key 片段在代码和交付配置中已无命中;剩余 s3.1080p.hk 命中为旧默认图片资源域名,不是访问密钥,后续可按默认资源治理单独处理。

建议动作:

  • 后续交付只允许客户按 .env.example 或安装脚本生成 .env,不要提交真实 .env / .env.production。
  • 历史上已经出现过的旧数据库、对象存储、NextAuth、第三方 API key 等敏感值,应视为已泄露并轮换。
  • 发布包、安装包和文档复扫时继续避免出现真实服务器地址、真实密码、真实 API key。

P0-07 数据库迁移失败后容器仍继续启动(已修复启动阻断和主发布脚本第一轮升级保护)

客户影响:

商业升级时最怕“服务看起来启动了,但数据库没升级”。这会导致客户打开页面后随机报错,排查成本很高。当前容器启动脚本已改为:数据库迁移失败时直接停止启动,避免应用在错误库结构上继续运行;主发布脚本也已补第一轮升级保护,远程部署前会备份 .env 和 Compose 管理的 PostgreSQL,失败时默认尝试恢复旧 .env 与旧应用镜像,部署后会校验运行镜像、版本文件和 Prisma 迁移状态。

证据:

  • scripts/docker-entrypoint.sh:37:启动时执行数据库迁移。
  • scripts/docker-entrypoint.sh:39:执行 prisma migrate deploy。
  • scripts/docker-entrypoint.sh:迁移失败分支现在打印停止原因并 exit 1,不再继续启动 Next.js。
  • scripts/__tests__/docker-entrypoint.test.ts:新增测试,锁定迁移失败必须停止容器启动;测试会在可用 shell 环境中模拟 prisma migrate deploy 失败,确认不会继续执行 server.js。
  • app/api/materials/route.ts:业务查询已经依赖 deletedAt 等迁移字段,因此迁移失败必须阻断启动。
  • deploy-registry.ps1:DeployApp 远程脚本会创建 backups/deploy-时间戳/,保存升级前 .env,并在发现 Compose 管理的 postgres 服务时执行 pg_dump。
  • deploy-registry.ps1:远程脚本默认启用 trap rollback ERR,部署失败时恢复升级前 .env,并按旧运行镜像或旧 APP_IMAGE 尝试重建应用容器;只有显式 -NoRollback 才关闭。
  • deploy-registry.ps1:远程脚本部署后校验 app 容器实际镜像、镜像内 VERSION.txt 和 npx prisma migrate status --schema=prisma/schema.prisma。
  • scripts/__tests__/deploy-registry-script.test.ts:新增测试,锁定远程数据库备份、失败回滚、运行镜像/版本/迁移状态校验和远端 shell 变量转义规则。

建议动作:

  • 已完成:生产容器迁移失败会阻断启动。
  • 已完成:主发布脚本 DeployApp 已完成第一轮“先备份、再部署、失败回滚、版本/镜像/迁移状态校验”的自动保护。
  • 验证已执行:npm test -- scripts/__tests__/docker-entrypoint.test.ts --runInBand、npm test -- --runTestsByPath scripts/__tests__/deploy-registry-script.test.ts --runInBand、npm run type-check。
  • 仍需继续:外部数据库、外部对象存储和大体量客户数据需要单独备份策略与恢复演练;脚本只能自动处理当前 Compose 管理的 PostgreSQL。
  • 仍需继续:后续可把备份文件清理策略、对象存储快照、升级报告导出做成更完整的交付工具。

P0-08 自定义字段“可搜索”没有真正打通(已完成第一版)

客户影响:

客户在后台创建自定义字段并勾选可搜索后,首页搜索之前仍搜不到这些字段。当前已先用 metadata.templateFields 的 JSON 查询打通第一版:标准字段继续走固定列,勾选 isSearchable 的自定义字段会参与首页搜索。

证据:

  • app/admin/_components/template-manager/editor/TemplateFieldsEditor.tsx:504:前端存在 isSearchable 配置入口。
  • app/api/admin/upload-templates/route.ts:224:创建模板时会保存 isSearchable。
  • lib/field-data-storage.ts:95:自定义字段存到 metadata.templateFields。
  • app/api/materials/route.ts:现在会读取 UploadField.isSearchable=true 的字段;标准字段继续走固定列;自定义字段通过 metadata.templateFields 查询命中的素材 ID,再纳入搜索结果。
  • app/api/materials/__tests__/materials-list-auth.test.ts:新增测试,锁定搜索时会读取可搜索自定义字段并把命中素材纳入查询。

建议动作:

  • 已完成:先用 JSON 查询打通自定义字段搜索闭环,解决“界面承诺但不可用”的验收问题。
  • 仍需继续:如果客户素材量明显增大,应评估独立 MaterialFieldValue 表或表达式索引,避免 JSON 查询长期成为性能瓶颈。
  • 仍需继续:前端字段类型需要进一步区分,复杂对象、多选字段等应有清晰支持边界。

P0-09 自定义字段首页筛选没有真正打通(已完成第一版)

客户影响:

客户新增自定义下拉字段后,即使它出现在字段池或筛选配置里,首页计数和筛选之前也不可靠。当前已先打通第一版:字段池启用的自定义字段会从 metadata.templateFields 统计选项数量,首页传入 homeField_xxx 时也会按自定义字段值过滤素材。

证据:

  • app/api/home-filter-fields/route.ts:标准字段仍按固定列统计;自定义字段改为从 metadata.templateFields 统计总数和选项数量。
  • app/api/home-filter-fields/route.ts:自定义字段计数已改为先在子查询中取出 metadata.templateFields 字段值,再按 value 统计,避免真实 PostgreSQL 下出现 column "materials.metadata" must appear in the GROUP BY clause 导致首页筛选接口 500。
  • app/api/materials/route.ts:homeField_xxx 现在会先确认字段池中该字段已启用;标准字段走固定列,自定义字段走 metadata.templateFields.xxx = 选中值。
  • app/api/home-filter-fields/__tests__/route-auth.test.ts:补充测试,锁定自定义首页筛选字段会从 metadata 统计数量,SQL 形状使用子查询统计,并确认普通用户读取首页筛选时不触发字段池同步写入。
  • app/api/materials/__tests__/materials-list-auth.test.ts:新增测试,锁定首页自定义字段筛选会按 metadata 字段值过滤素材。

建议动作:

  • 已完成:自定义字段首页计数和筛选先用 JSON 查询打通。
  • 已完成:修复真实 PostgreSQL 下自定义字段首页计数 SQL 分组报错;本地浏览器进入首页后服务端记录 GET /api/home-filter-fields 200,未再出现 P2010 / 42803。
  • 已完成:普通首页筛选读取不再触发字段池同步写入,GET 只读取现有字段池和素材统计。
  • 已完成:模板分类选项保存只会同步已加入分类管理的同 key 下拉字段,不再把同名但未勾选“添加到分类”的模板字段强行改选项或拉入分类管理。
  • 已完成:字段池同步改为非破坏式。模板字段不再作为分类来源时,只清空来源记录并从普通读取接口隐藏,不物理删除 FieldDefinition;模板下拉选项减少时也不物理删除旧 FieldOption,避免客户已经配置过的首页筛选名称、排序、启用状态和历史选项丢失。
  • 仍需继续:字段池同步目前主要围绕 addToCategory + select 字段;后台“首页筛选”和“添加到分类/选项管理”的产品口径还需要再统一。
  • 仍需继续:多选、复杂字段和大数据量性能需要后续单独设计。

P0-10 许可证信息接口曾匿名暴露授权状态和激活码(已修复)

客户影响:

这个接口之前不需要登录就能读到许可证类型、是否激活、使用量和激活码,外部可以直接看出系统规模和授权情况。当前已收回到管理员登录后访问,并把普通管理员能看到的激活码做了脱敏,只有最高权限账号才能看到完整值。

证据:

  • app/api/license/info/route.ts:现在先检查登录和管理员角色,未登录返回 401,普通用户返回 403。
  • app/api/license/info/route.ts:普通管理员只能看到脱敏激活码,最高权限账号才返回完整激活码。
  • app/api/license/info/__tests__/route.test.ts:新增测试,锁定未登录拒绝、普通管理员脱敏、最高权限账号完整返回、未激活状态正常返回。

建议动作:

  • 已完成:许可证信息接口不再匿名公开。
  • 保持当前规则:普通管理员看工作状态,最高权限账号看完整授权标识。

4. P1 问题

P1-01 普通管理员和部门管理员边界混乱(组织、模板、维护和全局配置已完成阶段性收口)

客户影响:

当前只有 USER 和 ADMIN,部门管理员本质上也是管理员。此前很多组织、模板和删除类写操作只判断 role === ADMIN,会导致普通管理员或部门管理员做全局管理动作。当前已完成阶段性收口:删除用户、调整用户部门、任免部门管理员、部门结构增删改、模板创建/编辑/删除、模板迁移、素材永久删除、过期清理、系统维护、全局字典/字段配置和转码恢复维护,都改为最高权限账号才能执行;统一用户更新接口也已禁止非最高权限账号修改角色、部门、启用状态和下载权限;部门管理员改成员姓名时不再批量改写该成员历史素材作者名,只有最高权限账号显式要求时才允许同步;素材编辑、导出、分享链接管理和单个素材转码触发已接入统一素材管理范围。这些敏感写操作会回查数据库里的 isSuperAdmin 或使用数据库回查结果,不只信旧会话。 邀请链接管理也已收口到最高权限账号。邀请链接会直接影响谁能注册进入私有化系统,不应由普通管理员或部门管理员创建、启停或删除;接口现在会回查数据库里的 isSuperAdmin,前端管理入口也只给最高权限账号显示。

证据:

  • prisma/schema.prisma:16:角色只有 USER / ADMIN。
  • lib/admin-permissions.ts:isCurrentSuperAdmin() 会按当前 session 用户 ID 回查数据库里的 isSuperAdmin。
  • app/api/admin/users/route.ts:删除用户现在必须是最高权限账号;不能删除自己,也不能删除最高权限账号。
  • app/api/admin/users/set-role/route.ts、app/api/admin/users/toggle-active/route.ts、app/api/admin/users/toggle-download/route.ts、app/api/admin/users/update-hierarchy/route.ts:旧用户管理入口现在也必须是最高权限账号,避免绕过统一用户更新接口。
  • app/api/admin/users/update/route.ts:部门管理员只能在部门范围内改普通资料,不能通过统一更新接口提升角色、调整部门、启用状态或下载权限;修改姓名不会批量改历史素材作者名,最高权限账号必须显式传入同步意图才会同步。
  • app/api/admin/users/update-department/route.ts:调整用户部门现在必须是最高权限账号;不能调整最高权限账号的部门。
  • app/api/admin/users/set-department-manager/route.ts:设置/取消部门管理员现在必须是最高权限账号;不能把最高权限账号设置成部门管理员或调整其部门管理员状态。
  • app/api/departments/route.ts:创建、重命名、删除部门结构现在必须是最高权限账号;普通管理员不会进入许可证检查和数据库写入逻辑。
  • app/api/admin/users/route.ts:用户列表 GET 现在按最高权限/部门范围/本人范围过滤;分页总数和搜索条件使用同一权限范围,避免普通管理员看全量用户或通过总数泄露全量。
  • lib/material-permissions.ts:部门管理员素材管理范围只认 managedDepartment,不再用用户当前 department 兜底,避免取消部门管理员后仍继承部门管理权。
  • app/api/admin/users/set-department-manager/route.ts、app/api/admin/users/set-role/route.ts:取消部门管理员或把用户降级为普通用户时会同步清空 managedDepartment。
  • app/admin/_hooks/usePermissions.ts:前端管理范围和后端保持一致,不再用当前部门模拟管理部门。
  • app/api/admin/upload-templates/route.ts:创建上传模板现在必须是最高权限账号。
  • app/api/admin/upload-templates/[id]/route.ts:更新、删除上传模板现在必须是最高权限账号;模板读取仍保留给管理员,方便后台只读查看。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:模板素材迁移现在必须是最高权限账号。
  • app/api/materials/route.ts:普通管理员仍可按自己/管理部门范围软删除素材,但 permanent=true 永久删除必须是数据库确认后的最高权限账号。
  • app/api/materials/cleanup/route.ts:清理 7 天前已软删除素材现在必须是最高权限账号。
  • app/api/admin/migrate-material-fields/route.ts:素材字段迁移维护现在必须是最高权限账号。
  • app/api/system/integrity/route.ts:完整性检查 GET 仍允许管理员查看;POST 修复动作必须是最高权限账号。
  • app/api/tunnel/start/route.ts、app/api/tunnel/stop/route.ts、app/api/tunnel/status/route.ts:内网穿透启动、停止和状态维护会回查最高权限账号,不只相信旧会话。
  • app/api/materials/restore/route.ts:恢复软删除素材按删除同一范围控制,最高权限账号可恢复任意素材,普通管理员只能恢复自己或管理部门范围内素材。
  • app/api/categories/route.ts、app/api/scenes/route.ts:新增、修改和删除店铺/场景分类现在必须是最高权限账号。
  • app/api/platforms/route.ts:新增、修改和删除平台分类现在必须是最高权限账号。
  • app/api/category-fields/route.ts:分类字段 GET 现在只读;手动同步字段池、分类字段启用、显示名和排序写入现在必须是最高权限账号。
  • app/api/template-categories/route.ts、app/api/template-categories/[fieldKey]/route.ts:读取模板分类和单字段选项现在必须登录,避免未登录枚举客户业务分类、品牌和选项;批量修改模板分类选项现在必须是最高权限账号。
  • app/api/template-categories/[fieldKey]/route.ts:保存模板分类选项时只更新 addToCategory=true 的同 key 下拉字段,不再隐式修改未加入分类管理的模板字段。
  • app/api/category-fields/route.ts、app/api/template-categories/route.ts、app/api/template-categories/[fieldKey]/route.ts、app/api/home-filter-fields/route.ts:分类字段、模板分类和首页筛选 GET 现在只读,不再触发字段池同步写入;字段池同步改为最高权限账号显式维护动作。
  • app/api/system-settings/route.ts、lib/public-share-settings.ts、app/api/materials/download/route.ts、app/api/material-rankings/route.ts、app/api/admin/reset-material-rankings/route.ts:系统设置、公开分享开关、下载、排行榜和排行榜重置不再执行运行期 DDL;缺少设置记录时只返回内存默认值,读接口不再创建或更新 system_settings。
  • lib/material-download-log.ts:已删除旧的下载日志运行期建表 helper,下载日志表依赖 Prisma 正式迁移创建。
  • prisma/migrations/20260528090000_add_home_delete_button_mode/migration.sql:补齐 homeDeleteButtonMode 正式迁移,避免读接口靠 ALTER TABLE 兜底。
  • app/api/system/init/route.ts:系统尚无最高权限账号时仍允许携带 SYSTEM_INIT_TOKEN 完成首次初始化;一旦已有最高权限账号,后续初始化必须由数据库确认过的最高权限账号触发,普通管理员不能再触发。
  • app/api/materials/transcode/route.ts:GET/POST 转码状态查询和单个素材转码触发已接入统一素材管理范围,普通管理员只能操作管理部门范围内素材;PATCH action=recover 转码恢复维护现在必须是最高权限账号。
  • app/api/invitations/route.ts、app/api/invitations/[id]/route.ts:邀请链接查看、创建、启停和删除现在必须是最高权限账号;旧会话里残留的最高权限标记不会绕过数据库回查。
  • lib/material-permissions.ts:新增统一素材管理范围,当前负责人、个人归属人、最高权限账号和管理部门范围内管理员可管理对应素材,历史上传者只保留追溯含义。
  • lib/material-share-permissions.ts、app/api/materials/share/route.ts、app/api/materials/share/tokens/route.ts、app/api/materials/share/tokens/[id]/route.ts:素材分享链接创建、查看列表和撤销已复用统一素材管理范围,普通管理员不能跨管理部门操作公开分享链接。
  • app/admin/page.tsx:邀请管理 Tab 只对最高权限账号显示,避免普通管理员看到入口后被后端拒绝。
  • app/api/admin/users/__tests__/organization-permissions.test.ts:新增/补充测试覆盖普通管理员被拒绝、部门管理员不能通过统一更新接口提升用户角色或调整部门/下载权限、部门管理员改成员姓名不批量改历史素材作者名、最高权限账号显式同步历史作者名、用户列表范围过滤、最高权限账号可继续执行、最高权限账号不能被删除或被纳入部门管理员调整。
  • app/api/departments/__tests__/route.test.ts:补充部门写操作普通管理员拒绝、最高权限账号继续受许可证门槛约束的测试。
  • app/api/admin/upload-templates/__tests__/template-crud.test.ts:补充普通管理员不能创建/更新/删除模板,最高权限账号原有成功路径继续可用。
  • app/api/admin/upload-templates/[id]/migrate/__tests__/template-migrate.test.ts:补充普通管理员和旧会话最高权限已撤销时不能迁移模板素材。
  • app/api/materials/__tests__/delete-storage-safety.test.ts:补充普通管理员不能永久删除、旧会话最高权限已撤销时不能永久删除、普通管理员仍可软删除自己素材、普通管理员不能清理过期删除素材。
  • app/api/admin/__tests__/destructive-actions-superadmin.test.ts:补充素材字段迁移、完整性修复等维护动作必须是最高权限账号。
  • app/api/admin/__tests__/global-dictionaries-superadmin.test.ts:补充店铺、场景、平台、分类字段和模板分类选项的普通管理员拒绝与最高权限账号成功路径;新增店铺/场景分类也已覆盖。
  • app/api/tunnel/__tests__/route.test.ts:补充内网穿透维护接口普通管理员拒绝、最高权限账号可执行。
  • app/api/materials/transcode/__tests__/route.test.ts:补充当前负责人可查询、历史上传者不再自动拥有转码管理权、部门管理员范围内可触发、普通管理员跨部门不可触发、普通管理员不能恢复卡住的转码任务、最高权限账号可恢复。
  • app/api/invitations/__tests__/route.test.ts:新增测试覆盖普通管理员不能查看/创建/启停/删除邀请链接,旧会话最高权限已撤销时不能创建邀请链接,最高权限账号可继续管理邀请链接。
  • app/api/materials/share/__tests__/route.test.ts、app/api/materials/share/tokens/__tests__/route.test.ts、app/api/materials/share/tokens/[id]/__tests__/route.test.ts:补充部门管理员范围内可操作、普通管理员跨部门不可操作的分享链接权限测试。
  • app/api/category-fields/__tests__/route.test.ts、app/api/template-categories/__tests__/route.test.ts、app/api/home-filter-fields/__tests__/route-auth.test.ts、app/api/system-settings/__tests__/route.test.ts、app/api/materials/download/__tests__/route.test.ts、app/api/material-rankings/__tests__/route.test.ts、lib/__tests__/public-share-settings.test.ts:补充 GET 读接口不触发字段池同步、运行期 DDL、默认设置创建、旧 Logo 回写、未登录分类枚举、分类选项不误改未加入分类字段或失败下载计数的测试。
  • app/api/system/init/__tests__/route.test.ts:补充已有最高权限账号后普通管理员不能触发初始化、首次初始化必须携带初始化令牌、正确令牌仍可完成首次初始化的测试。
  • app/api/admin/users/__tests__/organization-permissions.test.ts、app/api/materials/__tests__/template-fields-routes.test.ts:补充取消部门管理员、降级普通用户和素材管理范围只认 managedDepartment 的回归测试。

验证:

  • npm test -- app/api/admin/users/__tests__/organization-permissions.test.ts app/api/departments/__tests__/route.test.ts --runInBand
  • npm test -- --runTestsByPath app/api/admin/upload-templates/__tests__/template-crud.test.ts app/api/admin/upload-templates/[id]/migrate/__tests__/template-migrate.test.ts app/api/materials/__tests__/delete-storage-safety.test.ts --runInBand
  • npm test -- --runTestsByPath app/api/admin/users/__tests__/organization-permissions.test.ts app/api/departments/__tests__/route.test.ts app/api/admin/upload-templates/__tests__/template-crud.test.ts app/api/admin/upload-templates/[id]/migrate/__tests__/template-migrate.test.ts app/api/materials/__tests__/delete-storage-safety.test.ts --runInBand
  • npm test -- --runTestsByPath app/api/admin/users/__tests__/organization-permissions.test.ts app/api/admin/__tests__/destructive-actions-superadmin.test.ts app/api/admin/__tests__/global-dictionaries-superadmin.test.ts app/api/tunnel/__tests__/route.test.ts app/api/materials/__tests__/delete-storage-safety.test.ts app/api/materials/transcode/__tests__/route.test.ts --runInBand
  • npm run type-check
  • npm run lint
  • npm run prisma:validate
  • git diff --check -- app/api/admin/users/route.ts app/api/admin/upload-templates/route.ts app/api/admin/upload-templates/[id]/route.ts app/api/admin/upload-templates/[id]/migrate/route.ts app/api/admin/upload-templates/__tests__/template-crud.test.ts app/api/admin/upload-templates/[id]/migrate/__tests__/template-migrate.test.ts app/api/materials/route.ts app/api/materials/cleanup/route.ts app/api/materials/__tests__/delete-storage-safety.test.ts docs/商业化地基排查问题清单_20260524.md
  • npm test -- --runTestsByPath app/api/invitations/__tests__/route.test.ts app/api/admin/users/__tests__/organization-permissions.test.ts app/api/admin/__tests__/destructive-actions-superadmin.test.ts app/api/admin/__tests__/global-dictionaries-superadmin.test.ts app/api/materials/__tests__/template-fields-routes.test.ts app/api/admin/materials/export/__tests__/route.test.ts --runInBand
  • npm test -- --runTestsByPath "app/api/admin/__tests__/global-dictionaries-superadmin.test.ts" "app/api/materials/share/__tests__/route.test.ts" "app/api/materials/share/tokens/__tests__/route.test.ts" "app/api/materials/share/tokens/[id]/__tests__/route.test.ts" --runInBand
  • npm test -- --runTestsByPath "lib/__tests__/public-share-settings.test.ts" "app/api/system-settings/__tests__/route.test.ts" "app/api/template-categories/__tests__/route.test.ts" "app/api/home-filter-fields/__tests__/route-auth.test.ts" --runInBand
  • npm test -- --runTestsByPath "app/api/system/init/__tests__/route.test.ts" --runInBand
  • npx prisma validate
  • npm run type-check

建议动作:

  • 已完成:组织结构、用户归属、统一用户更新、用户列表范围、模板写操作、模板迁移、素材永久删除、过期清理、系统维护、内网穿透维护、素材恢复、全局字典、分类字段、模板选项和转码恢复维护的敏感权限已阶段性收紧。
  • 已完成:邀请链接管理已收紧到最高权限账号;普通管理员不能创建、启停、删除或查看邀请链接列表,前端入口也不再显示给普通管理员。
  • 已完成验证:department 只表示用户所属部门,managedDepartment 才表示管理范围;取消部门管理员会同时清空 managedDepartment 并把角色降回 USER,把用户角色降为 USER 时也会清空旧管理部门,避免旧权限复活。验证命令:npm test -- --runTestsByPath "app/api/admin/users/__tests__/organization-permissions.test.ts" --runInBand、npm test -- --runTestsByPath "app/api/materials/__tests__/template-fields-routes.test.ts" "app/api/materials/__tests__/delete-storage-safety.test.ts" --runInBand、npm run type-check。
  • 已完成验证:素材编辑入口和后台素材导出已接入统一素材管理范围;当前负责人、个人归属人、最高权限账号和管理部门范围内管理员可管理对应素材,历史上传者只保留追溯含义,不再自动代表当前管理权;普通管理员不能跨管理部门编辑或导出全量素材。验证命令:npm test -- --runTestsByPath "app/api/materials/__tests__/template-fields-routes.test.ts" "app/api/admin/materials/export/__tests__/route.test.ts" --runInBand、npm run type-check。
  • 已完成验证:素材分享链接创建、列表查看和撤销已接入统一素材管理范围;普通管理员只能操作管理部门范围内素材的分享链接,不能跨部门创建、查看或撤销。验证命令:npm test -- --runTestsByPath "app/api/materials/share/__tests__/route.test.ts" "app/api/materials/share/tokens/__tests__/route.test.ts" "app/api/materials/share/tokens/[id]/__tests__/route.test.ts" --runInBand、npm run type-check。
  • 已完成验证:店铺/场景分类新增已收紧到最高权限账号;普通用户、普通管理员和部门管理员不能新增全局店铺/场景字典项,上传流程仍可读取已有选项。验证命令:npm test -- --runTestsByPath "app/api/admin/__tests__/global-dictionaries-superadmin.test.ts" --runInBand、npm run type-check。
  • 已完成验证:转码状态查询和单个素材转码触发已接入统一素材管理范围;历史上传者不再自动拥有转码管理权,普通管理员不能跨部门触发转码任务。验证命令:npm test -- --runTestsByPath "app/api/materials/transcode/__tests__/route.test.ts" --runInBand、npm run type-check。
  • 已完成验证:后台分类字段、模板分类、首页筛选、系统设置、公开分享开关、下载和排行榜的主要读取路径已阶段性收口;普通用户、普通管理员或未登录访问不再触发字段池同步、运行期 DDL、默认设置创建、旧 Logo 回写或模板分类枚举;字段池同步改为后台分类管理里的最高权限账号显式维护动作;下载日志表依赖正式迁移,不再在 GET 中补建表;本地文件不存在时不再写下载日志或增加下载次数。验证命令:npm test -- --runTestsByPath "app/api/materials/download/__tests__/route.test.ts" "app/api/material-rankings/__tests__/route.test.ts" "app/api/admin/users/__tests__/organization-permissions.test.ts" "app/api/template-categories/__tests__/route.test.ts" "app/api/category-fields/__tests__/route.test.ts" --runInBand、npm run type-check、npx prisma validate。
  • 已完成验证:部门管理员修改成员姓名不会批量改写历史素材作者名,最高权限账号必须显式要求才会同步历史作者名。验证命令:npm test -- --runTestsByPath "app/api/admin/users/__tests__/organization-permissions.test.ts" --runInBand。
  • 已完成验证:模板分类选项保存不再误改同 key 但未加入分类管理的模板字段。验证命令:npm test -- --runTestsByPath "app/api/template-categories/__tests__/route.test.ts" "app/api/admin/__tests__/global-dictionaries-superadmin.test.ts" --runInBand。
  • 已完成验证:字段池同步已改为非破坏式,模板字段移出分类或选项减少时不再物理删除字段池记录和旧选项;普通读取接口只展示标准字段和仍有关联模板来源的字段。验证命令:npm test -- --runTestsByPath "lib/__tests__/field-pool.test.ts" "app/api/category-fields/__tests__/route.test.ts" "app/api/template-categories/__tests__/route.test.ts" "app/api/home-filter-fields/__tests__/route-auth.test.ts" --runInBand、npm run type-check、npx prisma validate。
  • 已完成验证:系统初始化入口在已有最高权限账号后必须由最高权限账号触发;首次初始化仍保留 SYSTEM_INIT_TOKEN 保护的未登录安装路径。验证命令:npm test -- --runTestsByPath "app/api/system/init/__tests__/route.test.ts" --runInBand。
  • 已完成验证:主应用许可证激活、免费激活、手动校验和手动心跳入口已收口;首次安装无最高权限账号时仍可激活并创建初始账号,已有最高权限账号后必须最高权限登录才允许改写或清除本地授权状态。验证命令:npm test -- --runTestsByPath "app/api/license/activate/__tests__/route.test.ts" "app/api/license/activate-free/__tests__/route.test.ts" "app/api/license/verify/__tests__/route.test.ts" "app/api/license/heartbeat/__tests__/route.test.ts" "app/api/license/check/__tests__/route.test.ts" "lib/__tests__/license-service.test.ts" --runInBand、npm run type-check。
  • 仍需继续:继续复核剩余 ADMIN 写接口,尤其是批量、导入类入口,避免新功能绕开最高权限边界。

P1-02 普通管理员可触发全局破坏性操作(已完成权限收口)

客户影响:

清空素材库、重置统计、批量移交等动作不应只靠普通管理员权限。客户环境一旦误点或被盗号,会造成严重数据事故。当前已先把这三类全局破坏性接口收紧到最高权限账号,并且每次操作前重新读取数据库确认当前账号仍是最高权限,避免只依赖旧会话里的权限标记。

证据:

  • lib/admin-permissions.ts:新增最高权限确认 helper,会用当前会话用户 ID 回查数据库里的 isSuperAdmin。
  • app/api/admin/cleanup-database/route.ts:清理数据库现在只允许最高权限账号,普通管理员不会进入删除素材记录和文件的逻辑。
  • app/api/admin/reset-material-rankings/route.ts:重置点赞/下载排行现在只允许最高权限账号。
  • app/api/admin/materials/transfer-owner/route.ts:批量移交素材归属现在只允许最高权限账号。
  • app/api/admin/__tests__/destructive-actions-superadmin.test.ts:新增测试覆盖普通管理员被拒绝、最高权限账号可执行三类操作,以及旧会话仍带最高权限标记但数据库已撤销时必须拒绝。

验证:

  • npm test -- app/api/admin/__tests__/destructive-actions-superadmin.test.ts --runInBand
  • npx prisma generate --no-engine
  • npm run type-check

建议动作:

  • 已完成:全局破坏性操作必须限最高权限账号。
  • 后续增强:增加二次确认、审计日志、备份或可回滚机制。

P1-03 系统设置修改权限过宽(已完成写入收口)

客户影响:

普通管理员之前可修改站点名、Logo、S3 存储配置等出厂级设置。对商业交付来说,这类设置更接近“超管/运维”权限。当前已先把系统设置写入和系统 LOGO 上传收紧到最高权限账号,并且每次写入前回查数据库确认当前用户仍是最高权限,避免旧会话残留权限;LOGO 上传也已增加格式、大小和压缩处理,避免把 SVG、超大图或异常图片原样保存为公共展示资源。

证据:

  • app/api/system-settings/route.ts:PUT 现在使用 isCurrentSuperAdmin(),普通管理员不能修改站点名、注册开关、S3 endpoint、access key、secret key 等系统设置。
  • app/api/system-settings/upload-logo/route.ts:系统 LOGO 上传也改为最高权限账号可操作,避免通过上传接口绕过外观设置权限;只允许 JPG、PNG、WebP,限制 5MB,并压缩转换为 WebP 后上传。
  • app/api/system-settings/__tests__/route.test.ts:新增/补充测试覆盖普通管理员不能修改系统设置、旧会话权限已撤销时不能修改、最高权限账号可修改、普通管理员不能上传 LOGO、拒绝 SVG 和拒绝超过 5MB 的 LOGO。

验证:

  • npm test -- app/api/system-settings/__tests__/route.test.ts --runInBand
  • npm run type-check

建议动作:

  • 已完成:系统设置写入口和系统 LOGO 上传入口必须限最高权限账号。
  • 已完成:系统 LOGO 上传只接受 JPG、PNG、WebP 且不超过 5MB,服务端压缩为 WebP 后再保存。
  • 后续增强:如果后续确实需要普通管理员改某些业务级外观项,应拆出单独接口和字段白名单,不能复用包含对象存储密钥的全量系统设置写入口。

P1-04 许可证过期和降级没有贯穿所有业务 API(已完成底层收口)

客户影响:

许可证过期、降级或在线租约过期后,某些功能可能因为本地缓存仍继续可用。商业化授权边界会变弱。当前已先在许可证底层检查入口收口:业务 API 调用的功能开关和数量限制每次都会读取本地已验签许可证,并检查许可证总到期和在线租约到期,不再只相信当前进程里残留的旧缓存。

证据:

  • lib/license/license-service.ts:新增当前有效许可证读取逻辑,isFeatureEnabled()、checkLimit()、getLicenseType() 都会从本地已验签许可证重新取当前配置,并把过期许可证视为不可用。
  • lib/license/types.ts、license-server/lib/types.ts:签名许可证新增 leaseExpiresAt 在线租约字段;新发和续发许可证都会带租约。默认策略为免费版 3 天、标准版 30 天、专业版 60 天;如果许可证总到期早于默认租约,则租约会被截断到总到期时间。
  • lib/license/license-service.ts:saveLicenseLocally()、updateLocalLicense() 保存前会再次验签,并同步更新内存缓存;如果新许可证已过期或在线租约已过期,则不会继续保留可用缓存。
  • lib/license/license-service.ts、app/api/license/verify/route.ts:远端验证失败进入本地宽限期前,也会先检查签名许可证里的 leaseExpiresAt,避免租约已经过期还继续靠本地宽限期放行。
  • app/api/license/check/route.ts:恢复 license_activated cookie 前会检查租约有效性,不能再靠一份过期本地许可证恢复激活状态。
  • app/api/license/heartbeat/route.ts:心跳拿到验证服务器续发许可证后会先验签再写入本地缓存。
  • lib/license/license-route-guards.ts:新增许可证入口守卫;无最高权限账号时保留首次安装激活窗口,已有最高权限账号后必须最高权限登录才允许触发授权状态写入。
  • app/api/license/activate/route.ts、app/api/license/activate-free/route.ts:重新激活和免费激活在已有最高权限账号后不再允许匿名触发,避免外部请求切换客户本地授权。
  • app/api/license/verify/route.ts、app/api/license/heartbeat/route.ts:HTTP 手动校验和手动心跳入口已要求最高权限账号,未授权请求不会触发 updateLocalLicense() 或 clearLocalLicense()。
  • lib/license/license-service.ts:新增 runLicenseVerification()、runLicenseHeartbeat() 服务端内部函数;启动时初始化和定时心跳直接调用内部函数,不再通过需要浏览器登录态的 HTTP 路由,避免权限收口后打断机器自己的许可证心跳。
  • app/api/license/check/route.ts:改为使用只读许可证读取函数,只恢复 cookie,不刷新 lastSeenAt 或写回本地时间状态。
  • lib/license/license-service.ts:本地许可证新增 lastSeenAt、clockRollbackDetectedAt 和 clockIntegrity 时间状态;旧许可证首次读取会自动补齐时间锚点。系统时间小幅校时允许 10 分钟容差,明显回拨会拒绝功能、额度、许可证类型和本地宽限期放行。
  • lib/license/crypto-service.ts:新增本地运行状态 HMAC 签名,时间锚点被直接改库篡改时会被视为无效许可证,降低通过改数据库字段绕过租约和回拨检测的成本。
  • app/api/license/info/route.ts:管理端许可证信息改为读取 getLocalLicense() 的已校验本地许可证,不再只看进程内缓存,避免回拨阻断后管理页仍显示正常激活。
  • lib/license/request-signature.ts、license-server/lib/request-signature.ts:主应用转发到验证服务器的 activate / register-free / verify / heartbeat 请求新增 HMAC 签名、nonce 和 5 分钟时间窗口;验证服务器会拒绝无签名、签名错误、过期和重复 nonce 请求,降低抓包重放和脚本伪造成本。
  • .env.example、docker-compose.prod.yml、license-server/docker-compose.yml:新增并强制要求 LICENSE_REQUEST_SIGNING_SECRET,主应用和验证服务器必须使用同一个共享签名密钥,且不能写入 Git。
  • license-server/docker-compose.yml、license-server/nginx/nginx.conf:许可证服务默认 Compose 已改为由 Nginx 对外暴露 9527,应用容器只在内部网络暴露 3000;现有登录和 API 限流不再只是孤立配置。
  • license-server/lib/abuse-detection.ts、license-server/app/api/license/[projectCode]/register-free/route.ts、activate/verify/heartbeat:免费版新增同设备重复注册、多实例/多指纹复用阻断;付费版多设备疑似复用先告警,不直接误伤客户。
  • license-server/app/api/license/[projectCode]/register-free/route.ts:免费注册复用已有激活前会检查许可证禁用和过期状态;首条心跳记录统一写入硬件指纹哈希,避免后续被误判为多设备。
  • license-server/app/api/license/[projectCode]/premium-modules/route.ts、license-server/app/api/license/[projectCode]/premium-modules/[moduleId]/route.ts、license-server/lib/premium-module-manifest.ts:新增付费模块清单和单模块下载接口,免费版拒绝,付费版按许可证类型返回签名 manifest,并下发绑定当前实例的加密模块包;硬件指纹明显不匹配时拒绝并告警。
  • app/api/license/activate/route.ts、app/api/license/verify/route.ts、app/api/license/heartbeat/route.ts、lib/license/premium-module-service.ts:主应用在付费激活、验证和心跳续证成功后同步付费模块 manifest,再逐个下载模块包,本地校验 RSA 签名、租约、当前许可证类型、包大小和 SHA256 后加密缓存,避免降级后继续使用旧 PRO 模块包。
  • lib/license/check-limits.ts:自定义模板和内网穿透已升级为“许可证功能开关 + 签名付费模块清单 + 本地有效模块包”三重门槛;没有验证服务器下发并校验通过的模块包时,即使本地许可证字段被篡改为开启,也不会通过服务端功能检查。
  • lib/__tests__/license-route-coverage.test.ts:新增关键业务入口覆盖测试,防止用户数、素材数、自定义模板、部门隔离、系统设置、内网穿透等服务端许可证检查被后续改动漏掉;同时锁定高价值付费能力必须依赖 hasPremiumModule()。
  • lib/__tests__/license-service.test.ts:新增测试覆盖许可证过期、在线租约过期、高级功能不可用、数量限制不放行、本地许可证从 Pro 降级到 Free 后业务检查立即按 Free 生效,以及旧许可证缺少 leaseExpiresAt 时的兼容行为。
  • lib/__tests__/license-types.test.ts、app/api/license/check/__tests__/route.test.ts:新增测试锁定默认租约天数、租约截断逻辑,以及租约过期时不恢复激活 cookie。
  • app/api/license/activate/__tests__/route.test.ts、app/api/license/activate-free/__tests__/route.test.ts、app/api/license/verify/__tests__/route.test.ts、app/api/license/heartbeat/__tests__/route.test.ts、app/api/license/check/__tests__/route.test.ts、lib/__tests__/license-service.test.ts:新增测试锁定已有最高权限账号后未授权请求不能改写本地授权状态、服务端定时心跳不依赖 HTTP 登录态、许可证 check 保持只读。
  • lib/__tests__/license-request-signature.test.ts、license-server/lib/__tests__/request-signature.test.ts:新增测试覆盖签名生成、篡改拒绝、重复 nonce 拒绝、过期拒绝和缺少服务端共享密钥时拒绝静默降级。
  • lib/__tests__/license-service.test.ts:新增测试覆盖旧本地许可证自动补时间锚点、小幅回拨不误伤、明显回拨拒绝功能和额度、本地时间状态被篡改后拒绝,以及网络失败宽限期前也先检查回拨状态。
  • lib/__tests__/check-limits.test.ts:功能检查 helper 已全部改为异步调用底层许可证检查。
  • lib/__tests__/premium-module-service.test.ts、license-server/lib/__tests__/premium-module-manifest.test.ts、license-server/lib/__tests__/free-registration-route-coverage.test.ts:新增测试覆盖付费模块 manifest 同步/验签/降级失效、验证服务器 manifest 生成,以及免费注册入口的防误判和禁用/过期护栏。

验证:

  • npm test -- lib/__tests__/license-service.test.ts lib/__tests__/check-limits.test.ts --runInBand
  • npm test -- lib/__tests__/license-types.test.ts lib/__tests__/license-service.test.ts app/api/license/check/__tests__/route.test.ts app/api/license/info/__tests__/route.test.ts --runInBand
  • npm test -- lib/__tests__/license-request-signature.test.ts app/api/license/activate/__tests__/route.test.ts app/api/license/activate-free/__tests__/route.test.ts --runInBand
  • npm test -- lib/__tests__/license-service.test.ts app/api/license/check/__tests__/route.test.ts app/api/license/info/__tests__/route.test.ts --runInBand
  • npm test -- license-server/lib/__tests__/request-signature.test.ts --runInBand
  • npm test -- lib/__tests__/premium-module-service.test.ts lib/__tests__/license-route-coverage.test.ts --runInBand
  • npm test -- license-server/lib/__tests__/abuse-detection.test.ts license-server/lib/__tests__/premium-module-manifest.test.ts license-server/lib/__tests__/free-registration-route-coverage.test.ts --runInBand
  • npx tsc --noEmit --project license-server/tsconfig.json
  • npm run type-check

建议动作:

  • 已完成:受限业务 API 继续通过 canUseCustomTemplates()、canUseDepartmentIsolation()、canUseSystemSettings()、canUploadMaterial()、canCreateUser() 等 helper 检查;这些 helper 底层不再信旧缓存。
  • 已完成:免费版采用更短在线租约,适合主要防破解和防白嫖;付费版采用更长租约,优先避免误伤正常客户,同时仍能限制付费凭证长期滥用。
  • 已完成:主应用到验证服务器的公开许可证请求已有轻量签名和防重放,不再只依赖项目编码、时间戳和硬件指纹。
  • 已完成:本地许可证读取已加入轻量时间回拨检测;免费版和租约能力不能再靠把系统时间往回调来继续走本地宽限期。
  • 已完成:验证服务器默认入口接入 Nginx 限流,免费版重复注册/多设备复用会阻断,付费版异常多设备以告警为主。
  • 已完成:主应用关键授权入口已有防漏口测试,后续改注册、上传、模板、部门、系统设置、内网穿透接口时会更早暴露许可证检查缺失。
  • 已完成:付费模块 manifest 和实例绑定加密模块包链路已落地,适合低配置验证服务器和约 5M 带宽;自定义模板和内网穿透已经接入本地有效模块包门槛,验证服务器不承担重计算,只做轻量签名、加密包生成和下载。
  • 后续增强:如果要做到更强的“核心业务实现完全不在镜像里”,还需要继续把自定义模板/内网穿透内部服务端实现逐步移动到模块加载层;当前本轮已经完成的是清单、下载、hash 校验、加密缓存和服务端入口强制依赖模块包的闭环。

P1-05 免费版激活保存空签名,后续读取疑似失败(已修复)

客户影响:

用户之前可能看到免费版激活成功,但后续本地许可证读取失败,导致功能判断异常。当前免费版激活已改为必须从许可证服务拿到带签名的许可证,本地保存前先验签,不再保存空签名。

证据:

  • app/api/license/activate-free/route.ts:免费版注册返回值必须包含 activationCode、signedLicense.data 和 signedLicense.signature。
  • app/api/license/activate-free/route.ts:保存本地许可证前会调用 verifyLicenseSignature() 验证签名。
  • app/api/license/activate-free/__tests__/route.test.ts:测试已锁定免费版保存的签名来自许可证服务返回值。
  • lib/license/license-service.ts:本地许可证读取仍要求存在 data 和 signature,并会验证签名。

建议动作:

  • 已完成:免费版也必须由许可证服务返回可验签许可证。
  • 保持当前规则:免费版、标准版、专业版都走同一套签名校验,不在主应用内生成不可验证许可证。

P1-06 内网穿透功能没有服务端许可证强制(已完成服务端收口)

客户影响:

如果内网穿透是 Pro 功能,只靠前端按钮和系统开关不够。当前 start/status/stop 三个服务端接口已统一检查许可证能力,免费版和标准版即使看到按钮,也不能绕过接口启动或操作内网穿透。

证据:

  • lib/license/check-limits.ts:72:存在 canUseIntranetPenetration()。
  • app/api/tunnel/start/route.ts:启动接口在登录、最高权限和系统开关通过后,还会调用 canUseIntranetPenetration()。
  • app/api/tunnel/status/route.ts:状态接口在系统开关通过后,也会检查许可证,不允许无授权环境查看可用状态。
  • app/api/tunnel/stop/route.ts:停止接口同样检查许可证。
  • app/api/tunnel/__tests__/route.test.ts:新增测试覆盖未登录、无许可证启动/状态/停止被拒,以及专业版可进入占位启动逻辑。

建议动作:

  • 已完成:tunnel start/status/stop 都统一检查许可证功能。
  • 保持当前规则:内网穿透即使还只是占位能力,也不能只靠前端按钮限制。

P1-07 上传先写对象存储再写数据库,失败会留下孤儿文件(已完成补偿清理)

客户影响:

客户 NAS 磁盘容量有限。上传过程中数据库创建失败,文件已经进了 MinIO,但系统看不到,长期会浪费空间。

现状:

  • 已修复:上传链路现在通过 uploadedArtifactUrls 记录本次已经写入的对象。
  • 已修复:failWithCleanup() 会在模板校验、数据库写入等失败路径调用 cleanupUploadedArtifacts()。
  • 已修复:成功创建素材记录后会清空待清理列表,避免误删正常素材文件。
  • 已修复:上传入口会在处理预览图、展示图和原始素材文件前先确认模板存在且启用。模板已停用或不存在时会直接拒绝,不再让客户等大文件上传完才失败,也不会写入对象存储。
  • 仍需后续处理:大文件内存风险已在 P1-11 做过三轮流式收口;当前剩余重点是预览图、PSD 展示图限制,以及后续分片上传/断点续传。

建议动作:

  • 保持现有补偿清理测试覆盖。
  • 保持“模板不可用时不上传文件”的测试覆盖,后续改上传方式时不能退回到文件写入后才校验模板。
  • 后续如引入分片/直传上传,要继续保留“数据库失败自动清理已上传对象”的规则。

P1-08 永久删除和清理失败时仍删除数据库记录(已完成第一轮修复)

客户影响:

如果对象存储文件删除失败,但数据库记录已经没了,后续无法可靠定位和补偿,最终留下对象存储垃圾。

修复现状:

  • 新增 lib/material-storage-cleanup.ts:统一收集并删除素材源文件、预览图、多预览图、展示图和视频相关文件。
  • app/api/materials/route.ts:永久删除时,任意文件删除失败就返回错误,并保留素材数据库记录。
  • app/api/materials/cleanup/route.ts:自动清理软删除素材时,只硬删文件清理成功的素材;失败素材保留记录和失败清单。
  • app/api/admin/cleanup-database/route.ts:最高权限清库时,如果素材文件删除失败,返回错误并保留素材记录,避免清库后无法追踪残留对象。
  • app/api/materials/__tests__/delete-storage-safety.test.ts:覆盖“文件删除失败不硬删数据库、不写删除日志”和“文件删除成功后才写日志并硬删记录”。

建议动作:

  • 后续增强:可增加 storage_cleanup_tasks 这类补偿任务表,让管理员能在存储恢复后批量重试。
  • 当前第一轮修复已经能避免“对象还在、数据库记录没了”的不可追踪状态。

P1-09 删除审计日志会随素材硬删除一起消失(已完成第一轮修复)

客户影响:

商业交付需要基本追责能力。当前永久删除前写日志,但硬删素材会级联删日志,等于日志留不住。

修复现状:

  • prisma/schema.prisma:MaterialLog 不再通过外键强依赖 Material,materialId 作为审计字段保留。
  • prisma/schema.prisma:新增 materialSnapshot,用于保存素材硬删除后的最小展示信息。
  • prisma/migrations/20260525010000_preserve_material_audit_logs/migration.sql:为老库补 materialSnapshot,回填现有日志快照,并移除 material_logs_materialId_fkey。
  • lib/material-log.ts:日志表结构已交给 Prisma migration 管理,运行期兼容入口不再建表/改表,避免普通读取或写日志顺手执行 DDL。
  • app/api/admin/material-logs/route.ts:改为最高权限账号可查看;使用 LEFT JOIN 查询日志,素材已硬删除时优先显示快照,否则显示“已删除素材”。
  • lib/__tests__/material-log.test.ts:覆盖运行期不再兜底建表/改表,并验证日志会保存素材快照。
  • app/api/admin/material-logs/__tests__/route.test.ts:覆盖非最高权限账号不能查看素材日志,最高权限账号查看日志时不会触发运行期建表/改表。

验证:

  • npm test -- --runTestsByPath "lib/__tests__/material-log.test.ts" "app/api/admin/material-logs/__tests__/route.test.ts" --runInBand:通过。

建议动作:

  • 后续增强:如需更强合规追责,再评估 userId -> users.id 是否也应取消级联,并保存操作者用户名/邮箱快照。
  • 最高权限清库是否保留全部审计日志,需要结合“重置系统”和“保留审计”两种产品语义再定。

P1-10 视频转码队列是内存态,重启会丢任务(已完成第一轮持久化恢复)

客户影响:

NAS 断电、容器重启、升级重启后,正在处理的视频可能永久停在 pending 或 processing。

原始问题:

  • 上传后和手动触发转码都只依赖进程内队列,容器重启后队列消失。
  • processing 状态的素材如果没有 forceRetry,会被接口当成“正在处理”,实际可能已经没有任何后台任务。

修复现状:

  • prisma/schema.prisma:新增 VideoTranscodeJob,记录素材转码任务、状态、尝试次数、锁定时间、失败原因和完成时间。
  • prisma/migrations/20260525020000_add_video_transcode_jobs/migration.sql:新增 video_transcode_jobs 表和索引。
  • lib/video-transcode-service.ts:新增持久化入队、任务执行、失败重试、卡住任务恢复逻辑。
  • app/api/materials/upload/route.ts:视频上传后改为创建持久化转码任务,再加入本进程队列。
  • app/api/materials/transcode/route.ts:手动触发转码改为创建/重置持久化任务;最高权限账号可通过 PATCH action=recover 触发恢复扫描。
  • instrumentation.ts:服务端启动时执行转码恢复检查,自动恢复 queued、超时 processing、以及没有 job 的 pending/processing 视频。
  • lib/video-transcode-queue.ts:增加 queued 去重,避免同一素材重复排队。
  • app/api/admin/video-transcode-stats/route.ts:统计中增加持久化 job 的 queued/processing/failed 数量。
  • lib/__tests__/video-transcode-service.test.ts、app/api/materials/transcode/__tests__/route.test.ts:新增/更新测试覆盖持久化入队、恢复卡住任务和管理员恢复接口。

建议动作:

  • 后续增强:任务抢锁目前仍是单应用进程优先的轻量方案;如果客户部署多副本或独立 worker,再升级成更严格的数据库锁/worker 模型。
  • 后续增强:管理后台可把失败原因和手动重试按钮做成可视化入口。

P1-11 大文件上传、播放、下载有整文件进内存问题(已完成第三轮流式收口)

客户影响:

低配 NAS 内存有限。大视频、大音频、大 PSD 上传、在线播放、下载时如果整文件进应用内存,容易内存飙升,甚至容器崩溃。

原始问题:

  • 上传入口曾使用 req.formData(),完整 multipart 请求会先由 Next.js 解析,入口层还不是彻底流式。
  • 上传路由曾对原始素材统一 arrayBuffer(),大视频/大包会在服务端二次复制成完整 Buffer 后再上传。
  • 旧视频转码逻辑会把 S3 源视频拼成完整 Buffer 后写临时文件,转码产物也 readFile() 后再上传。
  • legacy 本地文件播放、下载、素材代理曾有 readFile() 路径。

修复现状:

  • lib/s3.ts:新增 uploadStreamToS3(),支持把 Node stream 直接作为 S3 上传 Body,并保留原 Buffer 上传接口用于小文件/图片。
  • app/api/materials/upload/route.ts:真实 multipart 请求已改为 formidable 临时文件解析,不再在入口层调用 req.formData();测试兼容路径仍保留旧 FormData 包装。
  • app/api/materials/upload/route.ts:原始素材和音频素材都改为临时文件 createReadStream() + uploadStreamToS3(),大视频、大压缩包、PSD 原始文件和大音频不再在上传路由里二次复制成完整 Buffer 后上传。
  • app/api/materials/upload/route.ts:上传处理结束后会清理 formidable 临时文件,避免客户机器长期运行后堆积临时文件。
  • lib/audio-processor.ts:新增 parseAudioMetadataFromFile(),音频时长优先从临时文件解析;只有测试兼容路径里没有临时文件时才回退到 Buffer 解析。
  • lib/video-processor.ts:S3 源视频下载改为 pipeline(readable, createWriteStream(tempPath)),不再 chunks + Buffer.concat。
  • lib/video-processor.ts:540P 预览、10 秒片段和封面图改为 createReadStream() 上传,不再先 readFile() 成完整 Buffer。
  • lib/local-file-stream.ts:新增本地文件流式响应 helper,支持完整文件、普通 Range、后缀 Range 和非法 Range 416。
  • app/api/materials/stream/route.ts:legacy 本地视频/音频播放改为 createReadStream(),Range 请求只读指定范围。
  • app/api/materials/download/route.ts:legacy 本地下载改为 createReadStream(),普通下载和 Range 下载都不再先 readFile()。
  • app/api/materials/asset/route.ts:legacy 本地素材代理改为流式响应,不再整文件读入内存。
  • lib/__tests__/s3.test.ts:新增流式上传测试,锁定 Body 使用 stream 且传递 ContentLength。
  • app/api/materials/__tests__/template-fields-routes.test.ts:新增上传路由测试,锁定非音频和音频原始素材都走 uploadStreamToS3();音频生产路径从临时文件解析时长,不再为了时长解析整文件进 Buffer。
  • lib/__tests__/audio-processor.test.ts:新增临时文件路径解析音频元数据测试。
  • lib/__tests__/local-file-stream.test.ts:新增本地文件完整响应、Range、后缀 Range、非法 Range 测试。
  • app/api/materials/download/__tests__/route.test.ts:新增 legacy 本地下载和 Range 下载测试。

验证:

  • npm test -- --runTestsByPath "app/api/materials/__tests__/template-fields-routes.test.ts" "lib/__tests__/audio-processor.test.ts" --runInBand:通过。
  • npm test -- --runTestsByPath "app/api/materials/__tests__/template-fields-routes.test.ts" "app/api/materials/__tests__/delete-storage-safety.test.ts" --runInBand:通过。

建议动作:

  • 仍需继续:预览图和 PSD 展示图仍会进入内存处理;预览图已有 50MB 限制,PSD 展示图后续应补更明确的大小/像素限制或改为 sharp 从临时文件逐片处理。
  • 仍需继续:当前 S3 流式上传仍是单次 PutObject;如果客户会上传数 GB 文件,后续应升级成分片上传、进度、失败重试和断点续传。
  • 仍需继续:上传大小上限当前可通过 MAX_UPLOAD_FILE_SIZE_BYTES、MAX_UPLOAD_TOTAL_FILE_SIZE_BYTES、MAX_UPLOAD_FIELD_SIZE_BYTES 配置,正式交付前应按客户机器等级写入安装说明。

P1-12 模板迁移不是事务,失败会半迁移(已修复:小批量事务化)

客户影响:

历史风险是模板迁移中某条素材失败,可能出现一部分素材新模板、一部分旧模板,客户很难修;如果把回收站素材也一起迁移,后续恢复时也会和用户预期不一致。当前迁移写库阶段已包进数据库事务,并在事务内校验素材仍属于源模板且未删除;任一素材失败、已被其他操作迁走或进入回收站时,迁移会整体失败,不再留下半迁移状态。

证据:

  • app/api/admin/upload-templates/[id]/migrate/route.ts:先完成必填字段、metadata 大小和迁移数据预检查。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:执行阶段通过 prisma.$transaction() 统一提交。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:迁移查询默认只读取 deletedAt: null 的未删除素材,不处理回收站素材。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:事务内使用 id + sourceTemplateId + deletedAt: null 更新素材,素材已不属于源模板或已进入回收站时抛错并整体回滚。
  • app/api/admin/upload-templates/[id]/migrate/__tests__/template-migrate.test.ts:新增事务路径、事务中途失败、源模板变更失败和只迁移未删除素材的测试。

建议动作:

  • 已完成:小批量迁移使用数据库事务,失败整体回滚;默认跳过回收站素材。
  • 仍需继续:大批量应改成迁移任务,提供进度、失败清单和回滚方案。

P1-13 模板迁移会丢非目标模板字段(已修复:旧字段备份保留)

客户影响:

历史风险是旧模板字段迁移到新模板后,目标模板没有接收的字段会从当前模板字段里消失,客户可能认为历史数据丢了。当前迁移会把目标模板不接收的旧自定义字段和标准字段写入 metadata.templateMigrationBackups 备份区;可安全清空的旧标准字段会同时清空,避免继续污染新模板展示和筛选。

证据:

  • app/api/admin/upload-templates/[id]/migrate/route.ts:迁移时只把目标模板字段写入当前 templateFields。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:目标模板不接收的旧字段进入 templateMigrationBackups。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:旧 previewUrls 继续保留。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:新增 previewOnly: true 迁移预览模式,复用真实迁移前的同一套字段计算和校验,但不进入事务、不写数据库;返回目标模板当前字段、会进入备份区的旧字段统计和受影响素材样例。
  • app/api/admin/upload-templates/[id]/migrate/__tests__/template-migrate.test.ts:新增旧自定义字段、旧标准字段备份、可清空字段清理和迁移前预览测试。

建议动作:

  • 已完成:默认保留旧字段到备份命名空间,避免历史字段无声丢失。
  • 已完成:迁移前可通过 previewOnly: true 获取预览报告,列出哪些字段会进入备份区,哪些字段会成为目标模板当前字段。
  • 已完成:前端模板管理页的迁移删除弹窗已接入预览确认流程;管理员必须先生成当前目标模板的迁移预览,确认字段影响后才能点击“确认迁移并删除”。

P1-14 上传会截断 metadata,可能截坏 JSON(已修复上传、编辑和模板迁移入口)

客户影响:

自定义字段多时,metadata 被字符串截断后可能不是合法 JSON。当前上传、编辑和模板迁移入口都已改为保存前检查 metadata 大小,超限直接返回明确错误,不再把 JSON 截断后写入数据库。模板迁移若遇到超限,会在事务前拒绝,不会写入半成品 metadata。

证据:

  • lib/field-data-storage.ts:192:已有 checkMetadataSize()。
  • app/api/materials/upload/route.ts:构建并压缩 metadata 后调用 checkMetadataSize()。
  • app/api/materials/upload/route.ts:metadata 超限会返回 METADATA_TOO_LARGE,不会继续创建素材记录。
  • app/api/materials/upload/route.ts:保存数据库时直接写入已验证的 metadata,不再对 metadata 做 substring。
  • app/api/materials/__tests__/template-fields-routes.test.ts:新增测试,锁定自定义字段过大时拒绝上传且不会创建素材记录。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:模板迁移生成新 metadata 后调用 checkMetadataSize()。
  • app/api/admin/upload-templates/[id]/migrate/route.ts:迁移 metadata 超限返回 METADATA_TOO_LARGE,不会进入事务。
  • app/api/admin/upload-templates/[id]/migrate/__tests__/template-migrate.test.ts:新增英文大字段和中文多字节内容超限测试。
  • app/api/materials/update/route.ts:编辑入口重写 metadata 前调用 checkMetadataSize(),超限返回 METADATA_TOO_LARGE。
  • app/api/materials/[id]/route.ts:旧 JSON 更新入口也执行 metadata 大小检查,避免绕过主编辑接口。
  • app/api/materials/__tests__/template-fields-routes.test.ts:新增编辑 metadata 超限拒绝测试。

建议动作:

  • 已完成:上传保存前用 checkMetadataSize() 拦截超限 metadata。
  • 已完成:模板迁移保存前用 checkMetadataSize() 拦截超限 metadata。
  • 已完成:表单编辑入口和旧 JSON 更新入口保存前用 checkMetadataSize() 拦截超限 metadata。
  • 仍需继续:后续若新增素材 metadata 写入入口,必须复用同一套大小检查。

P1-15 模板条件显示只配置未执行(已完成第一轮前后端闭环)

客户影响:

历史风险是后台能配置字段条件,但上传和编辑时字段不会按条件隐藏或跳过必填。当前第一轮已把条件显示接入上传、编辑和后端校验:条件不满足的字段不显示、不要求必填、不随本次提交写入;条件满足后会正常显示并参与必填校验。

证据:

  • lib/field-conditions.ts:提供 getVisibleTemplateFields()、filterVisibleFieldValues()、hasFieldValue(),前后端共用同一套可见字段判断。
  • lib/template-field-validation.ts:后端只对可见字段执行必填校验,并返回过滤后的 templateFields。
  • app/admin/_components/template-manager/editor/FieldConditionsConfig.tsx:31:存在条件配置界面。
  • components/UploadModal.tsx:上传弹窗按可见字段渲染、校验和构建 templateFields;材质模板保持文件和预览图可选。
  • components/EditMaterialModal.tsx:编辑弹窗按可见字段渲染、校验和提交;表单使用系统中文校验,不再依赖浏览器原生必填提示。
  • app/api/materials/upload/route.ts:上传入口使用后端模板校验返回的过滤字段保存 metadata。
  • app/api/materials/update/route.ts:表单编辑入口用现有素材值和本次提交值合并判断条件规则。
  • app/api/materials/[id]/route.ts:旧 JSON 更新入口也接入同一套模板条件校验,避免绕过主编辑接口。
  • lib/__tests__/template-field-validation.test.ts:覆盖条件不满足时跳过必填、条件满足时拦截、checkbox false 不误判。
  • app/api/materials/__tests__/template-fields-routes.test.ts:覆盖上传、表单编辑、旧 JSON 更新入口的条件字段行为。
  • components/__tests__/UploadModal.test.tsx、components/__tests__/EditMaterialModal.test.tsx:覆盖条件字段隐藏不提交、依赖字段变化后显示并参与必填。

建议动作:

  • 已完成:前端渲染、前端校验、提交 payload 和后端校验都执行同一套条件规则。
  • 已完成:旧 JSON 更新入口补同样校验,不再作为模板规则旁路。
  • 仍需继续:复杂循环条件和跨字段依赖要在模板编辑阶段给出更清晰提示。
  • 仍需继续:隐藏字段历史旧值当前按“保留历史、不随本次提交覆盖”处理;如果产品要自动清空隐藏值,需要单独做确认和迁移策略。

P1-16 前后端编辑权限不一致(已修复:上传者本人和管理员可编辑)

客户影响:

历史风险是首页给上传者显示编辑入口,但表单更新接口只允许管理员,普通用户会看到按钮却保存失败。当前已统一为:素材上传者本人和管理员都可以编辑,其他普通用户即使绕过前端入口也会被后端拒绝。

证据:

  • app/page.tsx:首页素材卡片和预览弹窗的编辑入口按“上传者本人或管理员”展示。
  • app/api/materials/update/route.ts:表单更新接口已从“仅管理员”改为“上传者本人或管理员”,未登录返回 401,非上传者普通用户返回 403。
  • app/api/materials/[id]/route.ts:旧 JSON 更新入口同样按“上传者本人或管理员”判断,和表单更新入口保持一致。
  • components/EditMaterialModal.tsx:有 templateId 时优先从普通用户可访问的 /api/upload-templates 加载模板,避免普通上传者被管理员模板详情接口误拦;只有普通列表找不到时才回退到管理员详情接口。
  • app/api/materials/__tests__/template-fields-routes.test.ts:覆盖表单更新入口和旧 JSON 更新入口的本人可编辑、管理员可编辑别人素材、非上传者普通用户不能编辑别人素材。
  • components/__tests__/EditMaterialModal.test.tsx:覆盖编辑弹窗优先使用普通模板列表加载并提交模板字段、普通列表找不到时才回退管理员模板详情、管理员详情 403 时降级到基础字段、保存接口 403 时展示后端无权限提示。

建议动作:

  • 已完成:前端入口、表单更新接口和旧 JSON 更新入口已统一到“上传者本人或管理员可编辑”。
  • 已完成:普通上传者编辑带模板素材时,模板加载主路径不再依赖管理员接口。
  • 仍需继续:如果后续要支持部门管理员、组织管理员或素材归属转移,需要抽出统一的素材权限 helper,避免各入口再次分叉。
  • 仍需继续:?edit=id 直达入口当前主要依赖后端拦截,后续可以补成前端打开前先做权限提示,减少普通用户误操作。

P1-17 发布链路版本 tag 和回滚口径(主链路和第一轮升级保护已收口,历史入口仍需隔离)

客户影响:

客户升级需要可预测、可回滚。当前主发布脚本、生产 Compose、源码测试 Compose 和主要运行依赖已经按版本化口径收口;DeployApp 也已补第一轮升级保护,默认会做远端备份、失败回滚、运行镜像校验、版本文件校验和迁移状态校验。剩余风险主要来自历史内部脚本、Node 版旧 Agent、本地离线镜像缓存说明,以及外部数据库/对象存储场景的备份演练。

证据:

  • deploy-registry.ps1:当前主发布脚本已经按新 Registry 流程组织为多动作版本化发布;DeployApp 默认正式通道,客户部署示例显式使用 -DeployChannel release -Version x.x.x。
  • deploy-registry.ps1:DeployApp 远程部署默认做升级前 .env 备份、Compose PostgreSQL 备份、失败自动回滚、运行镜像校验、版本文件校验和 Prisma 迁移状态校验。
  • docker-compose.prod.yml:主应用镜像已改成必填完整 APP_IMAGE,不再默认回退到 Harbor 或 latest。
  • .env.example:已声明客户交付必须固定完整 APP_IMAGE,并给出 POSTGRES_IMAGE、MINIO_IMAGE、MINIO_MC_IMAGE 的固定版本示例。
  • docker-compose.prod.yml、deploy-registry.ps1、.env.example:minio/mc 已固定到 RELEASE.2025-04-16T18-13-26Z;Runtime 同步清单不再同步 minio/mc:latest 或 alpine:latest。
  • docker-compose.source-test.yml:源码测试 Compose 中的 MinIO 和 MinIO Client 默认值已改为固定版本,避免测试入口继续传播 latest。
  • scripts/build-material-image.ps1:旧构建脚本仍保留 Harbor / latest 旧口径,但已加历史/内部备用警告;推送旧 Harbor 必须显式追加确认参数。
  • deploy-new-fixed.ps1:历史内部部署脚本已默认禁止执行,且清空旧默认服务器、旧 Harbor 地址、旧默认 Tag 和默认 Harbor 密码;确需排查历史链路必须显式追加确认参数。
  • 发布管理.ps1:历史内部发布控制台已默认禁止执行,且清空旧默认服务器、旧 Harbor 地址和旧许可证服务器地址;确需排查历史链路必须显式追加确认参数。
  • license-server/deploy.ps1:许可证服务历史部署脚本已默认禁止执行,且必须显式传入目标服务器和 SSH key。
  • flnxi-agent/README.md:Node 版 Agent 第一屏已改为历史实现说明,不再写成新客户默认安装入口。
  • docker-images/README.md、docker-images/minio-mc/VERSION.txt:本地 minio/mc:latest 缓存已标为历史本地缓存和不可交付版本,避免被误认为当前离线包事实。
  • docs/deployment/部署说明.md:部署说明已经切到版本化 Registry 发布口径,明确客户现场使用 release + 版本号,并把发布方构建、客户首次安装、已有客户升级拆开说明。

建议动作:

  • 客户部署只认版本 tag,不用 latest 作为部署依据。
  • 客户交付包默认排除 deploy-new-fixed.ps1、发布管理.ps1、license-server/deploy.ps1、flnxi-agent/ 和旧 .bak 文件;仓库内保留时必须继续保持默认禁止执行。
  • 正式做离线包前,重新生成或归档 docker-images/minio-mc/ 的旧 latest 本地缓存,避免把历史缓存直接发给客户。
  • 已完成第一轮:主发布脚本默认覆盖备份、拉镜像、迁移阻断、运行镜像/版本校验、健康检查和失败回滚。
  • 仍需继续:外部数据库、外部对象存储、大数据量客户环境和离线包升级需要单独演练和说明。

P1-18 健康检查曾把启动初始化失败误报为可用(已修复)

客户影响:

客户安装或升级时,如果启动检查创建内置模板失败,旧 /api/health 仍会返回 200 + status: ok。Docker/NAS 健康检查会误以为系统可用,但客户打开页面后可能遇到模板或初始化数据不完整。

证据:

  • app/api/health/route.ts:现在会复用启动检查结果;systemCheck.success=false 时,GET 返回 503 + status: unhealthy,HEAD 也返回 503。
  • app/api/health/__tests__/route.test.ts:新增测试覆盖首次执行启动检查、失败返回 503、HEAD 不再固定返回 200。
  • lib/startup-check.ts:启动检查继续负责创建内置模板并记录 success/errors,健康检查只读取结果,不重复实现初始化逻辑。
  • docker-compose.prod.yml:主应用健康检查继续访问 /api/health,现在可以正确阻断初始化失败的容器进入健康状态。

建议动作:

  • 已完成:健康检查失败时返回 503,避免部署脚本和 Docker 把未就绪系统判定为可用。
  • 已完成:对外响应只返回错误数量,不公开内部错误明细;详细原因继续看容器日志。

P1-19 视频转码接口曾可匿名查询和触发(已修复权限边界)

客户影响:

视频转码会消耗 CPU、内存和对象存储读写。之前接口没有登录校验,外部请求只要知道素材 ID,就可能查询转码状态或反复触发转码任务;后续又发现普通管理员和历史上传者权限过宽。对 NAS 私有化交付来说,这会带来后台资源被滥用、素材处理状态被误改的风险。

证据:

  • app/api/materials/transcode/route.ts:GET 现在必须登录并通过统一素材管理范围;当前负责人、个人归属人、最高权限账号和管理部门范围内管理员可查看,历史上传者不再自动放行。
  • app/api/materials/transcode/route.ts:POST 现在必须登录并通过统一素材管理范围;普通管理员不能跨部门触发转码任务,未授权用户不会进入队列和状态更新逻辑。
  • app/api/materials/transcode/route.ts:PATCH 恢复卡住转码任务属于全局维护动作,现在必须是最高权限账号。
  • app/api/materials/transcode/__tests__/route.test.ts:新增/更新测试覆盖未登录拒绝、普通用户不能操作他人素材、当前负责人可查看、历史上传者不再自动拥有转码管理权、部门管理员范围内可触发、普通管理员跨部门不可触发、最高权限账号可恢复卡住任务。

验证:

  • npm test -- app/api/materials/transcode/__tests__/route.test.ts --runInBand
  • npm test -- --runTestsByPath "app/api/materials/transcode/__tests__/route.test.ts" --runInBand
  • npm test -- app/api/materials/share/__tests__/route.test.ts app/api/materials/asset/__tests__/route.test.ts app/api/materials/stream/__tests__/route.test.ts app/api/materials/transcode/__tests__/route.test.ts lib/__tests__/material-share-token.test.ts --runInBand
  • npm run type-check
  • npm run prisma:validate

建议动作:

  • 已完成:转码查询和触发接口不再匿名开放;转码恢复维护也已收紧到最高权限账号。
  • P1-10 已完成第一轮持久化恢复;后续继续增强失败原因展示和管理端重试入口。

P1-20 AI 改图占位接口缺少素材权限校验(已修复)

客户影响:

AI 改图以后会消耗外部额度、CPU、存储和客户素材数据。即使当前接口还是占位实现,只要系统开关打开,未收口的接口就可能让普通登录用户拿任意素材 ID 创建任务,后续接入真实 AI 时容易把权限洞一起带进生产。

证据:

  • app/api/ai/edit/route.ts:现在会先确认 AI 改图已启用,再读取未删除素材,并复用统一素材管理范围;素材本人、个人归属人、最高权限账号和管理部门范围内管理员可操作,其他用户返回 403。
  • app/api/ai/edit/route.ts:素材不存在或已软删除时返回 404,不会继续创建占位任务。
  • app/api/ai/edit/__tests__/route.test.ts:新增测试覆盖未登录、功能未启用、素材不存在、普通用户无权操作他人素材、素材本人可创建占位任务。

验证:

  • npm test -- --runTestsByPath "app/api/ai/edit/__tests__/route.test.ts" --runInBand

建议动作:

  • 已完成:AI 改图占位入口先按素材管理权限收口。
  • 后续接入真实 AI 服务前,还要增加许可证开关、任务持久化、外部调用失败重试、额度/并发限制和素材出域审计。

P1-21 公开 debug 接口泄露模板结构(已修复:删除生产路由)

客户影响:

私有素材库里的模板名称、素材类型和启用状态可能反映客户内部业务结构。公开 debug 接口如果无需登录即可读取这些信息,外部访问者可能在未登录情况下看到客户模板结构。

证据:

  • app/api/debug/material-types/route.ts:旧公开调试接口已删除,不再作为生产 API 出现在代码路由中。
  • lib/__tests__/production-route-guard.test.ts:新增测试锁定 app/api/debug/**/route.ts 不应存在,避免后续把公开 debug API 带回生产包。

建议动作:

  • 已完成:删除公开 debug API 路由。
  • 已完成:增加生产路由边界测试,后续需要诊断模板结构时应走登录后的管理员页面或受控脚本,不再开放匿名 debug API。

P1-22 历史数据恢复接口是强写入口(已完成默认禁用加固)

客户影响:

/api/restore-data 是旧的一次性数据恢复入口,会创建管理员、默认分类、默认场景,并把本地文件和外部 S3 预览图写回素材表。它不属于普通后台操作,如果客户环境误留恢复令牌,等于留下一个绕过后台登录体系的强写入口。

证据:

  • app/api/restore-data/route.ts:恢复入口现在必须同时满足 RESTORE_DATA_ENABLED=1 和 RESTORE_DATA_TOKEN 正确才会执行;默认未显式启用时直接返回禁用。
  • app/api/restore-data/__tests__/route.test.ts:新增测试覆盖默认禁用、启用后缺少令牌仍拒绝、错误令牌拒绝。

建议动作:

  • 已完成:恢复接口默认显式禁用,避免只因误留 token 就开放历史强写入口。
  • 后续建议:正式交付包如不再需要该历史恢复能力,应单独评估物理移除或迁移到离线脚本。

5. P2 问题

P2-01 监控页普通登录用户可进入(已修复)

客户影响:

监控页会展示用户数、素材数、权限分布、系统设置状态和最近活动。它应属于管理员工作台能力,普通登录用户不应看到这些系统状态。当前已修复:页面、中间件和后端接口都要求管理员身份。

证据:

  • middleware.ts:/admin/monitor 已纳入 /admin 统一管理员页面限制,普通用户访问会被带回首页。
  • app/admin/monitor/page.tsx:客户端页面守卫已补充管理员角色判断,非管理员登录态会跳回首页。
  • app/api/admin/system-status/route.ts:系统状态接口已补充管理员校验,普通用户返回 403,不会继续查询用户、素材和系统状态数据。
  • app/api/admin/__tests__/system-status-auth.test.ts:新增测试锁定未登录 401、普通用户 403 且不查库、管理员可读取。
  • __tests__/middleware-cors.test.ts:新增 middleware 测试,锁定普通用户不能打开 /admin/monitor,管理员可打开。
  • 浏览器验证:当前真实监控入口是 http://localhost:3004/admin/monitor;/admin?tab=monitor 不会自动切到监控页,后台 Tab 注释也写着监控仪表板已移到头像下拉入口。
  • 浏览器验证:以当前最高权限管理员登录访问 http://localhost:3004/admin/monitor,监控页仍能正常加载。

建议动作:

  • 已完成:监控页按管理员能力收口。
  • 后续如果要给普通成员开放部分统计,应单独做低敏概览接口,不复用当前系统状态接口。

P2-02 模板管理许可证限制缺少前置提示(已修复)

客户影响:

历史风险是当前许可证不包含自定义模板时,用户在后台仍看到新建入口,点击后才收到后端许可证错误,体验不完整。当前前端已读取许可证状态,在模板管理页提前展示授权限制提示;受限动作会打开中性的授权提示弹窗,后端仍保留强制拦截。

证据:

  • hooks/useLicenseStatus.ts:前端通过 /api/license/info 读取当前许可证和功能开关。
  • app/admin/_components/TemplateManager.tsx:模板管理页用许可证能力计算自定义模板能力;新建、编辑、复制和从预制模板快速创建都会先检查能力,未授权时打开授权提示弹窗。
  • app/admin/_components/template-manager/CustomTemplateSection.tsx:自定义模板区在未授权时显示“当前许可证未包含自定义模板功能,请联系管理员处理授权”提示;点击受限动作只触发授权提示,不直接打开编辑器。
  • components/ui/UpgradeModal.tsx:授权提示已收口为“当前许可证未包含此功能,请联系管理员处理授权”的私有化工作提示。
  • app/api/admin/upload-templates/route.ts:后端创建模板前仍调用 canUseCustomTemplates(),失败返回 403 + LICENSE_REQUIRED。
  • app/admin/_components/template-manager/__tests__/CustomTemplateSection.test.tsx:新增测试覆盖未授权时显示前置提示并拦截新建、编辑、复制和启停动作;授权允许时新建动作正常执行。
  • app/api/admin/upload-templates/__tests__/template-crud.test.ts:已有后端测试覆盖许可证不足时返回 LICENSE_REQUIRED。

建议动作:

  • 已完成:自定义模板入口有前置授权提示,受限动作不会直接进入编辑流程。
  • 已完成:后端创建、更新、删除和迁移入口继续保留许可证强制,错误信息统一为“当前许可证未包含/联系管理员处理授权”口径。
  • 已完成:前端授权提示、按钮、通用受限开关、数量限制和系统设置/部门隔离/模板 API 的“升级后”话术已统一成私有化交付口径。

P2-03 必填 checkbox 前后端语义不一致(已修复)

客户影响:

历史风险是前端把 false 当未填写,后端把 false 当已填写,客户用 checkbox 字段时会遇到不一致提示。当前上传弹窗、编辑弹窗和后端模板校验都统一使用 hasFieldValue():checkbox 字段只要有明确的 true 或 false 值,就视为已填写。

证据:

  • lib/field-conditions.ts:hasFieldValue() 对 checkbox 使用 value !== undefined && value !== null,未勾选的 false 也算已填写。
  • lib/template-field-validation.ts:后端模板必填校验复用 hasFieldValue()。
  • components/UploadModal.tsx:上传弹窗必填校验复用 hasFieldValue(),checkbox 初始值为 false。
  • components/EditMaterialModal.tsx:编辑弹窗必填校验复用 hasFieldValue()。
  • lib/__tests__/template-field-validation.test.ts:覆盖 checkbox false 后端视为已填写。
  • components/__tests__/UploadModal.test.tsx、components/__tests__/EditMaterialModal.test.tsx:覆盖必填 checkbox 未勾选时仍允许提交 false。

建议动作:

  • 已完成:checkbox 必填语义统一为“必须有明确选择”,未勾选的 false 不再被误判为漏填。
  • 仍需继续:如果未来产品要支持“必须勾选同意”这种字段,应新增单独校验规则,不要复用当前普通 required 语义。

P2-04 窄屏模板编辑器不适合移动端(已完成第一轮响应式收口)

客户影响:

NAS 私有化系统可能在平板或窄屏浏览器里使用。模板编辑器此前固定左右分栏,窄屏会难用。当前已完成第一轮代码收口:窄屏上下排列,宽屏保留左右编辑/预览。

证据:

  • app/admin/_components/TemplateManager.tsx:编辑器弹窗当前为 max-w-6xl,不是旧文档里的 max-w-7xl。
  • app/admin/_components/TemplateManager.tsx:内部布局已改为 flex-col lg:flex-row,窄屏上下排列,宽屏左右排列。
  • app/admin/_components/template-manager/editor/TemplatePreviewPanel.tsx:非预览模式为 w-full lg:w-1/2,预览模式为 w-full max-w-[520px] mx-auto,不再是单一固定宽度。

建议动作:

  • 后续前端整改时补真实窄屏浏览器验证。
  • 若客户实际频繁在平板使用,再评估是否进一步拆成分步骤编辑。

P2-05 前端 UI 存在明显“AI 生成感”和展示页倾向(公共样式层已完成第二轮收口)

客户影响:

不阻断功能,但会影响商业产品的可信度。当前系统是企业/团队内部长期使用的素材库,不是公网 SaaS 营销页。界面里如果到处是大圆角、大阴影、紫蓝渐变、光晕、英文小标题、卡片套卡片和过多解释文案,会让客户感觉“像 AI 生成的演示页面”,而不是一个稳定的工作台。

证据:

  • lib/ui-styles.ts:已把向后兼容导出的 rainbow / glass / neon 降级为普通工作台控件;本轮继续把旧直出 cardStyles / buttonStyles / inputStyles / navItemStyles 从大圆角、靛蓝焦点和 transition-all 收为 rounded-lg、中性焦点和颜色过渡,并把兼容的浮动光球/鼠标光晕函数降级为无装饰效果。
  • lib/ui-styles-base.ts:已把 Hook 样式源头里的卡片、按钮、输入框、导航、弹窗从大圆角、毛玻璃、强阴影和缩放动画收敛为普通边框、普通圆角和轻量状态变化。
  • lib/ui-theme-colors-light.ts、lib/ui-theme-colors-dark.ts:已把彩虹、玻璃、霓虹、强阴影等兼容字段映射为普通背景、普通边框和少量语义色。
  • components/ui/Tag.tsx:已把标签从彩色胶囊和放大动效改为中性后台标签。
  • app/admin/_components/MonitorTab.tsx:系统监控页已完成第二轮收口,移除大圆角重阴影页头、英文 System Monitor 眉标、emoji 标题、彩色发光指标、hover 放大、紫蓝圆头柱状图和靛蓝加载圈。
  • app/admin/_components/CategoriesTab.tsx:分类管理页已完成第二轮收口,页头改为普通 rounded-lg border 工具区,英文 Category Management 眉标已删除;左侧列表、字段编辑区、排序/重命名按钮和添加选项入口不再使用大圆角、圆形按钮、靛蓝拖拽态、重阴影和玻璃效果。
  • app/share/[id]/page.tsx:公开分享页已移除 AI改图(预留) 未完成功能入口,卡片和标签从大圆角、重阴影、彩色胶囊收敛为普通圆角、轻阴影和中性标签。
  • components/Header.tsx:顶部导航和首页收缩态搜索区已移除毛玻璃、重阴影、下拉动画和按压缩放,改成实色背景、普通边框、轻阴影和中性按钮。
  • components/DanmakuWidget.tsx:弹幕浮窗已从蓝紫渐变、重阴影、圆形悬浮按钮和 hover 放大收为普通边框消息面板、矩形头像块和中性浮动按钮。
  • components/AIChatWidget.tsx:聊天助手浮窗已移除蓝紫渐变头部、光圈空状态、毛玻璃处理层、消息气泡重阴影和图片 hover 放大,改成普通工具面板和中性消息气泡。
  • app/admin/_components/AppearanceSettingsPanel.tsx:外观设置页已完成第一轮收口,当前代码里已无旧记录中的动态径向背景、浮动光球和 Appearance Studio 英文标题;本轮继续移除大圆角、内阴影、紫蓝焦点光圈和对象存储提示的彩色展示块。
  • app/admin/_components/MaterialsTab.tsx:素材管理的目标用户选择弹窗已移除毛玻璃遮罩、大圆角、重阴影、紫蓝选中态和轻快底部文案,改为普通后台弹窗和中性状态。
  • app/admin/_components/ui/LoadingState.tsx:后台加载态已从靛蓝色转圈和主题运行时切换改为中性灰色加载态,避免深浅色首屏类名不一致导致浏览器水合警告。
  • app/globals.css:37:已完成第一轮收口,旧按钮、输入框、卡片工具类现在改为中性后台控件,不再 hover 放大或使用 Apple 式重阴影。
  • app/globals.css:138:已完成第一轮收口,旧弹窗动画已改为轻量淡入,不再使用模糊、缩放和反弹动效。
  • 浏览器验证:已在 http://localhost:3004/admin 验证用户管理、模板管理、架构管理三个后台页面,接口正常、控制台无新增错误,未观察到明显重叠。
  • 浏览器验证:已在 http://localhost:3004/admin/monitor 验证监控页正常加载,页面不再出现 System Monitor 英文眉标和 emoji 标题,按钮与数据卡未观察到明显重叠。
  • 浏览器验证:已在 http://localhost:3004/admin 切到分类管理页,页面显示为普通后台分栏,无 Category Management 英文眉标,按钮和卡片未观察到明显重叠;截图保存为 flnxi-categories-ui-cleanup-20260525.png。
  • 浏览器验证:已在 http://127.0.0.1:3002/admin 打开模板管理并进入预制模板预览,模板预览弹窗可正常打开,未观察到明显重叠。
  • npm run type-check:已通过。

建议动作:

  • 继续检查直接写在组件里的渐变、大圆角和悬浮动画,下一批优先处理个人主页素材卡片、编辑素材弹窗和模板管理内层组件。
  • 管理后台单独建立更克制的 admin 样式:纯色背景、弱边框、少阴影,主色只用于选中态和主按钮。
  • 后续前端修复应保持当前项目自己的风格,不复制旧参考源码,也不继续堆“模板化 SaaS 展示页”视觉。

P2-06 后台管理页头部像营销 Hero,不像工作台

客户影响:

用户进入管理后台后,首先看到的是大面积装饰性头部卡片、英文小标题和说明文字。对客户来说,管理后台应优先让人快速完成“查用户、改模板、配权限、看状态”,不需要每个 Tab 都像一个展示页。

证据:

  • app/admin/_components/template-manager/TemplateManagerHeader.tsx:模板页头部已从旧记录中的大圆角/英文展示感收为普通 rounded-lg 后台工具区,标题使用中文“模板管理”。
  • app/admin/_components/UsersTab.tsx:464:用户页同样使用大 Hero 卡片。
  • app/admin/_components/UsersTab.tsx:473:用户页显示英文小标题 User Management。
  • app/admin/_components/MonitorTab.tsx:监控页头部已从大 Hero 卡片收敛为普通后台工具区,英文眉标已删除。
  • app/admin/_components/CategoriesTab.tsx:分类管理页头部已从展示页式大卡片收敛为普通后台工具区,英文眉标已删除。
  • app/admin/_components/AppearanceSettingsPanel.tsx:旧记录中的动态径向渐变背景、浮动光球和 Appearance Studio 已不存在;本轮已把外观设置页头部、主题预览、LOGO 预览、开关行、对象存储提示从大圆角和偏展示的彩色块收敛为普通后台设置区。
  • app/admin/_components/DepartmentsTab.tsx:部门管理头部已从大圆角说明卡和小眉标收为普通 rounded-lg 工具区,说明缩短,搜索框文案改为直接搜索分支名称。

建议动作:

  • 后台 Tab 页头统一改为紧凑工具栏:左侧中文标题,右侧搜索、筛选和主要操作。
  • 删除英文 eyebrow 小标题,例如 Template Management、User Management、Appearance Studio。
  • 删除头部动态光晕、浮动光球、hover 大阴影。
  • 页面说明只在空状态、帮助入口或 tooltip 里出现,不放在每个 Tab 首屏。
  • 已推进:app/admin/_components/UsersTab.tsx 的用户管理头部已从大 Hero 卡片收敛为普通后台工具区;最高权限 / 管理员标识已从紫粉渐变胶囊改为中性徽章。
  • 已推进:app/admin/_components/MonitorTab.tsx 的监控页头部、指标卡和上传趋势图已从展示页式大卡片、彩色圆形/圆头视觉收敛为普通后台工具区、中性竖线标识和矩形灰色柱状条。
  • 已推进:app/admin/_components/CategoriesTab.tsx 的分类管理页头部、左侧列表、字段启用编辑态、排序/重命名工具按钮和可添加字段列表已完成第二轮工作台化收口。
  • 已推进:app/admin/_components/AppearanceSettingsPanel.tsx 的外观设置页已完成第一轮工作台化收口,文档中过期的 Appearance Studio、浮动光球和动态背景证据已修正。
  • 已推进:app/admin/_components/DepartmentsTab.tsx 的部门管理页头部和组织树操作按钮已完成第一轮工作台化收口。
  • 已推进:app/admin/_components/InvitationsTab.tsx 的邀请管理说明改为“成员通过邀请链接加入团队”,与私有化账号入口一致;表头和状态标签也从展示化样式收为普通后台表格样式。

P2-07 管理后台列表和组织树装饰过重

客户影响:

用户管理、组织管理这类页面应该方便扫读、对齐、批量判断。当前很多信息被做成卡片流和渐变徽章,视觉上更像自动生成的漂亮面板,信息密度偏低。

证据:

  • app/admin/_components/UsersTab.tsx:用户行已从 hover 卡片样式收为普通紧凑列表行,减少“卡片套卡片”的管理页噪音。
  • app/admin/_components/UsersTab.tsx:437:最高权限徽章使用紫粉渐变、阴影和 hover 放大。
  • app/admin/_components/UsersTab.tsx:442:管理员徽章使用紫蓝渐变、阴影和 hover 放大。
  • app/admin/_components/UsersTab.tsx:513:用户列表外层又包卡片,形成卡片套列表。
  • app/admin/_components/DepartmentsTab.tsx:组织树节点已改为中性浅底和左边线,重命名/添加分支按钮已从靛蓝强调色收为中性主按钮;后续仍可继续压缩节点行高和操作列密度。

建议动作:

  • 用户管理优先改成表格或紧凑列表:用户名、邮箱、部门、角色、状态、素材数、操作列对齐。
  • 权限标签改成浅底纯色小标签,不用渐变、阴影和缩放。
  • 组织树用缩进、左边线和折叠图标表达层级,不用渐变卡片表达层级。
  • 已推进:app/admin/_components/UsersTab.tsx 的用户行已从独立小卡片收成紧凑列表行,保留原有权限、状态和操作按钮逻辑。
  • 已推进:app/admin/_components/users/DepartmentHierarchy.tsx 已移除组织树里的紫色渐变、hover 放大和强阴影;部门节点改成中性树形行,管理员姓名标识改为中性徽章。
  • 已推进:app/admin/_components/MaterialsTab.tsx 的目标用户选择弹窗已从玻璃感浮层收敛为普通后台弹窗,角色标签和选中标识改为中性小标签。
  • 验证:npm run type-check 通过;浏览器已打开 http://localhost:3004/admin 用户管理页,控制台无新增错误,页面未观察到明显重叠。

P2-08 首页素材区偏内容展示流,不够资产工作台

客户影响:

首页是素材库最常用页面,客户需要快速找素材、比对素材、下载素材。当前素材卡片和排序控件有较强的内容发现/榜单展示味道,会抢素材本身的注意力。

证据:

  • app/page.tsx:400:精品、下载榜、点赞榜仍作为素材卡片状态信息保留,但样式已收为信息区里的中性小标签。
  • app/page.tsx:排序方式已从大号胶囊按钮改为普通矩形按钮组。
  • app/page.tsx:首页素材类型筛选和筛选弹窗内选项已从胶囊按钮、hover 滑动显示数量和阴影状态,收为普通矩形按钮;数量默认稳定显示,不再依赖鼠标悬停。
  • components/HomeFilterTags.tsx:筛选标签组件剩余的展开按钮和加载骨架已从胶囊圆角、transition-all 收为普通 rounded-md 和颜色过渡,避免后续复用时把胶囊样式带回首页。
  • app/page.tsx:首页“上传素材”主按钮已去掉硬编码靛蓝主按钮样式,改为跟随当前主题色的普通矩形主操作按钮。
  • app/page.tsx:首页音频素材卡片已从大圆形播放按钮和圆形频谱背景,收为小图标、文件名、普通进度条和矩形播放按钮。
  • app/page.tsx:首页素材卡片已从大圆角、轻阴影和黑色半透明胶囊收为普通 rounded-lg 边框卡片、中性标签和弱化的素材型号标识。
  • app/profile/page.tsx:个人页“我的素材 / 我的点赞 / 浏览历史”卡片已从大圆角和黑色渐变覆盖层收为普通边框卡片和浅底信息条。
  • app/author/[authorName]/page.tsx:作者页作品卡片已去掉 hover 放大、hover 阴影和大圆角,改为普通边框卡片;加载状态也从蓝色圈收为中性圈。
  • components/RankingPanel.tsx:浮动榜单面板已移除毛玻璃、重阴影、超大圆角和前三名金色卡片感,改为普通边框小面板。
  • app/page.tsx:榜单/精品信息已从素材封面移到卡片信息区,并使用浅底中性小标签,避免长期遮挡素材图。
  • app/page.tsx:素材上传日期已从封面右下角黑色悬浮层移到卡片信息区,封面只保留必要的素材标识和媒体状态。
  • app/page.tsx、components/MaterialActionButtons.tsx:素材卡片悬浮点赞按钮已是矩形轻边框样式;公共素材操作按钮也从 rounded-xl 和蓝色下载进度条收为 rounded-md 与中性进度条。
  • app/page.tsx:首页空状态已从“快来上传第一个吧”改为“暂无素材,请上传素材”;列表底部已改为“加载更多素材 / 暂无更多素材”。

建议动作:

  • 已完成第一轮:素材卡片改为轻边框、少阴影、不上浮,排序控件、筛选按钮和上传按钮改成普通按钮组。
  • 已完成:榜单/精品信息已从素材封面移到卡片信息区,不再长期压在素材图上。
  • 已完成:工作状态文案改为“加载中”“加载更多素材”“暂无更多素材”等朴素表达。

P2-09 上传、编辑、预览弹窗过圆、过亮、说明过多

客户影响:

上传和编辑是高频生产动作,界面应安静、稳定、低干扰。当前弹窗大量使用大圆角、玻璃顶栏、虚线大卡片、emoji 提示、渐变必填标识和发光输入框,会让表单看起来像演示模板,填写效率也会被视觉噪音影响。

证据:

  • components/UploadModal.tsx:50:已完成第一轮收口,动态模板字段统一使用中性输入框样式。
  • components/ui/Modal.tsx:统一弹窗硬编码圆角已从 32px 收到 12px,避免默认弹窗继续呈现大圆角展示页风格。
  • components/UploadModal.tsx:已完成第二轮收口,恢复上传提示去掉 emoji,内部外壳去掉额外重阴影和卡片套卡片,上传区从大圆角虚线卡片收为普通 rounded-lg dropzone。
  • components/UploadModal.tsx:预览图、PSD 展示图和素材文件上传说明已压短,不再重复“点击或拖拽/支持格式”长提示;网络失败提示也改为一句明确结果和下一步。
  • components/UploadModal.tsx:上传进度提示、封面标签、音频解析状态和进度条已从蓝色强调、圆头进度条、transition-all 收为中性文字、普通标签和矩形进度条;调试日志里的 emoji 也已移除。
  • components/UploadModal.tsx:1661、components/UploadModal.tsx:1812:必填字段不再使用蓝色渐变背景和大阴影,改为边框/浅底提示。
  • components/EditMaterialModal.tsx:编辑弹窗必填标识和必填输入框已是中性样式;本轮继续把预览图上传区从大圆角展示模块收为普通 rounded-lg 表单区,并缩短说明文案。
  • components/AudioVisualizer.tsx:音频默认封面和播放频谱已从 Apple Music 式深色渐变、光晕、毛玻璃、大圆形舞台频谱,收为中性封面、普通 rounded-lg 容器和更紧凑的条形频谱。验证命令:npm test -- --runTestsByPath "components/__tests__/AudioVisualizer.test.tsx" --runInBand。
  • components/MaterialPreviewModal.tsx:没有真实说明时不再显示“统一预览弹窗展示”占位文案,底部关闭按钮从“返回上一页”改为“关闭”,避免误导用户以为会跳转页面。
  • components/MaterialPreviewModal.tsx:音频、视频、图片预览外壳已从大圆角/阴影/舞台式容器收为普通 rounded-lg 预览区;视频预览生成中状态改为普通空状态。
  • components/MaterialPreviewModal.tsx:右侧详情标题不再优先显示素材 ID,标题字号从侧栏里的大标题收为 text-lg;转码状态说明已缩短。
  • components/MaterialPreviewModal.tsx:全图查看层已去掉毛玻璃背景和圆形悬浮关闭按钮,改为普通遮罩和矩形图标按钮。

建议动作:

  • 弹窗统一走中性 Modal:8-12px 圆角、普通遮罩、轻阴影。上传弹窗、编辑弹窗预览区和音频预览已先完成第一轮,视频预览状态和图片预览继续观察真实素材效果。
  • 上传区保留拖拽能力,但改成紧凑 dropzone 或普通表单行,hover 只变边框色。
  • 去掉 emoji,提示文案缩短,错误和状态集中显示。上传弹窗已完成第三轮收口,剩余弹窗继续检查。
  • 必填用红色星号或中性小字,不用渐变胶囊。上传弹窗和编辑弹窗已完成第一轮。
  • 输入框必填态只改边框或辅助提示,不用渐变背景。上传弹窗和编辑弹窗已完成第一轮。
  • 预览弹窗减少舞台感,优先突出文件本身和右侧字段信息。音频、视频和图片预览已完成第一轮收口,后续继续检查具体素材尺寸和移动端效果。

P2-10 模板编辑器更像低代码演示器,后续需要工具化(编辑器内层已完成第二轮收口)

客户影响:

自定义模板是重要功能,但当前编辑器视觉负担偏重:大弹窗、固定左右分栏、实时预览面板、拖拽动画、字段卡片层层嵌套。客户在小屏、NAS 浏览器或远程桌面里使用时,容易觉得复杂和不稳。

证据:

  • app/admin/_components/TemplateManager.tsx:模板编辑器弹窗仍偏大,当前为 max-w-6xl,后续还要评估是否进一步拆成更轻的工具面板。
  • app/admin/_components/TemplateManager.tsx:编辑器弹窗和素材迁移弹窗外壳已从大圆角、重阴影和内层卡片套卡片收为普通 rounded-lg、轻阴影和单层内容区。
  • app/admin/_components/TemplateManager.tsx:内部布局已完成第一轮响应式收口,小屏改为上下排列,宽屏仍保持左右编辑/预览。
  • app/admin/_components/template-manager/editor/TemplatePreviewPanel.tsx:预览区已完成第一轮收口,固定右栏改为响应式宽度,预览外框、上传占位、字段卡片、拖拽悬浮层和拖拽占位统一从大圆角收为普通 rounded-lg,拖拽回弹从弹性曲线改为普通 ease-out。
  • app/admin/_components/template-manager/editor/TemplateFieldsEditor.tsx:字段删除确认弹窗已去掉毛玻璃、强阴影和缩放进入动画,改为普通遮罩、普通边框和轻阴影;拖拽态也去掉蓝色发光阴影和缩放。
  • app/admin/_components/template-manager/editor/TemplateBasicsForm.tsx:素材大类和预设模板选择已去掉胶囊按钮、选中小圆点和按钮内长说明,改为普通矩形选择按钮。
  • app/admin/_components/template-manager/editor/FieldValidationConfig.tsx、app/admin/_components/template-manager/editor/FieldConditionsConfig.tsx:校验和条件配置输入框已从蓝紫发光焦点改为中性焦点,条件数量标签改为灰色,底部教程式说明缩短为一行。
  • app/admin/_components/template-manager/editor/TemplateFieldsEditor.tsx:字段块已去掉 rounded-xl、选中阴影、hover 阴影和 transition-all;字段操作按钮改为中性 hover;“下拉选项管理”蓝色提示卡改为一行灰色提示;“添加元素”大圆图标改为小矩形“添加字段”入口。
  • app/admin/_components/template-manager/editor/TemplatePreviewPanel.tsx:预览字段悬浮拖拽/删除按钮已从圆形悬浮按钮改为小矩形按钮,删除不再使用整块红底;预览卡片继续减少阴影和重复边框。
  • app/admin/_components/template-manager/editor/TemplateAdvancedConfigPanel.tsx、app/admin/_components/template-manager/editor/TemplateEditorFooter.tsx:高级配置和底部保存区已去掉大圆角、靛蓝主按钮和部分卡片化样式,统一为中性后台控件。
  • app/admin/_components/template-manager/BuiltInTemplateGrid.tsx、app/admin/_components/template-manager/CustomTemplateSection.tsx:预制模板和自定义模板入口已去掉胶囊按钮、大圆形新建图标、PRO 英文授权标识和技术化 v{version} 展示,改为普通按钮、中性标签和“版本 N”。
  • 代码复扫:app/admin/_components/template-manager/editor 已无 buttonModifiers.pill、focus:ring-blue、focus:border-blue、bg-indigo、transition-all、shadow-md、rounded-xl、rounded-full 和临时 调试: 文案命中。
  • 仍需继续:app/admin/_components/TemplateManager.tsx 的编辑器弹窗尺寸和交互复杂度还没有彻底重构,当前只是先收掉最显眼的舞台化视觉,并解决窄屏左右挤压。

建议动作:

  • 编辑器后续仍应改成真正的后台工具:字段列表、字段详情、预览三块职责清晰。
  • 宽屏可以两栏,窄屏切为上下布局或 tabs。
  • 字段行用紧凑列表或 accordion,减少卡片套卡片。
  • 拖拽只保留虚线占位和轻背景,不使用旋转、放大、bounce、pulse、强阴影。
  • 实时预览默认可以折叠,避免占据过多编辑空间。

P2-11 授权/付费提示偏公网 SaaS 营销话术(已完成第一轮收口)

客户影响:

当前项目目标是私有化交付。许可证限制应该像“当前许可证不包含此能力”的工作提示,而不是“升级后可享受”的营销弹窗。否则客户会感觉产品不是为私有部署准备的。当前第一轮已把前端授权提示、通用受限开关、数量限制和核心授权 API 的可见话术统一成“当前许可证未包含/联系管理员处理授权”。

证据:

  • components/ui/UpgradeModal.tsx:授权提示弹窗已改成左侧小锁图标、限制提示文案、普通描述行和普通按钮,不再显示“升级后可享受”,也不再使用大圆形图标或大圆角内容块。
  • components/ui/RestrictedToggle.tsx:受限开关提示已改为“当前许可证未包含此功能”。
  • components/ui/ActivationModal.tsx:手动激活弹窗已移除“前往购买”官网跳转,改为联系系统交付人员或管理员;弹窗和输入控件同步收为普通圆角、轻阴影和中性色按钮。
  • components/ui/__tests__/ActivationModal.test.tsx:测试已同步私有化许可证口径,不再期待官网购买链接或公网跳转。
  • components/ui/StorageEditConfirmModal.tsx:对象存储编辑二次确认弹窗已从大圆角、重阴影和蓝紫焦点态收为普通安全确认表单。
  • components/LicenseInfo.tsx:许可证信息面板已去掉紫色/蓝色胶囊标签、emoji 到期提醒和“购买授权”官网外链,改为中性状态标签、矩形使用量条和“联系系统管理员或交付人员”的私有化交付口径。
  • lib/license/check-limits.ts:用户数和素材数超限提示已改为联系管理员处理授权。
  • app/api/admin/upload-templates/route.ts、app/api/admin/upload-templates/[id]/route.ts、app/api/admin/upload-templates/[id]/migrate/route.ts、app/api/departments/route.ts、app/api/system-settings/route.ts:授权不足错误已统一成私有化口径。
  • components/ui/ProBadge.tsx:PRO 徽章已改为中性许可证标签,不再使用胶囊圆角、阴影或渐变展示感。
  • components/ui/__tests__/UpgradeModal.test.tsx:测试已改为锁定限制提示和中性按钮文案。

建议动作:

  • 已完成:可见“升级后/请升级”话术第一轮清理,改成许可证状态提示和联系管理员处理授权。
  • 已完成:通用授权弹窗不再展示营销化“升级好处”。
  • 已完成:手动激活入口不再引导客户跳官网购买。
  • 已完成:PRO 徽章改为中性许可证标签,不用紫蓝渐变、胶囊圆角和阴影。
  • 已完成:许可证信息面板不再提供“购买授权”跳转,授权调整统一提示联系系统管理员或交付人员。

P2-12 登录、激活、回调页风格和后台不统一

客户影响:

客户首次安装和日常登录都会看到这些页面。当前登录页偏泛 SaaS 模板,激活页、钉钉回调、完善资料页又各自独立,会削弱“这是同一个产品”的感觉。

证据:

  • app/login/page.tsx:11:默认登录 Logo 不再指向不存在的 /logo/logo2.png,没有配置时直接走文字占位。
  • app/api/system-settings/route.ts、app/login/page.tsx:公共系统设置不再返回注册开关;登录页不展示普通自助注册入口,统一提示联系管理员开通账号或使用邀请链接。
  • app/login/page.tsx:295:登录页已改为浅灰背景和中性表单,不再使用浅蓝渐变背景。
  • app/login/page.tsx、app/register/page.tsx:登录/注册页卡片、输入框、按钮和 Logo 占位已从 rounded-xl 收为 rounded-lg,减少通用 SaaS 模板感。
  • app/register/page.tsx:注册页会读取 URL 上的 invite 参数并随注册请求提交;无邀请参数时提示用户使用邀请链接或联系管理员开通账号。
  • app/register/page.tsx:注册页已和登录页一样读取系统名称和登录 Logo;无邀请码时不展示完整自助注册表单,只提示使用邀请链接或联系管理员。
  • app/api/auth/register/route.ts:关闭公开注册时不再直接挡掉邀请链接;有效邀请码仍可完成注册并记录使用次数,无邀请码仍拒绝公开注册。
  • app/api/auth/register/__tests__/route.test.ts:新增测试覆盖“关闭公开注册时无邀请码拒绝、有有效邀请码允许注册”的私有化账号入口。
  • app/register/page.tsx:21:注册页已改为和登录页一致的浅灰背景。
  • app/register/page.tsx:88:注册页已增加默认 Logo 兜底,没有配置图片时显示文字占位,不再显示破图。
  • app/complete-profile/page.tsx:25:完善资料页已改为和登录页一致的浅灰背景。
  • app/complete-profile/page.tsx:205:完善资料页已增加默认 Logo 兜底。
  • app/complete-profile/page.tsx:完善资料页表单、Logo、提示框和钉钉账号信息卡已从 rounded-xl 收为 rounded-lg,与登录/注册入口保持一致。
  • app/activate/page.tsx:激活页已完成第二轮收口,默认只展示许可证密钥激活,并跟随系统名称和登录 Logo;“免费试用/购买许可证”入口已改为显式环境开关下的“申请临时授权”,客户默认安装现场不再露出公网获客口径。
  • app/dingtalk-callback/page.tsx:钉钉回调页已完成第一轮收口,蓝色渐变整屏背景改为浅灰背景,rounded-2xl shadow-xl 大卡片改为 rounded-lg border shadow-sm 普通状态卡片,加载/成功/失败状态复用同一个中性外壳。
  • 代码复扫:app/dingtalk-callback/page.tsx 已无 bg-gradient、rounded-2xl、shadow-xl、大尺寸状态图标等命中。
  • 验证限制:本轮尝试用 Codex 内置浏览器打开钉钉回调页时返回“没有可用活动面板”,因此该页暂未补截图;已完成 npm run type-check 和 npm run lint 验证。

建议动作:

  • 登录、激活、回调、完善资料页统一成同一套私有化安装/访问风格;登录/注册/激活/完善资料页已完成第一轮圆角、口径和系统名称/Logo 对齐,邀请注册链路已修复;钉钉回调页已完成第一轮视觉收口,激活页公网获客/购买口径已进一步隐藏。
  • 背景用纯色或轻灰,不用大面积渐变。
  • 登录页可以保留品牌感,但减少泛 SaaS 视觉:小 Logo、清晰表单、明确客户系统名称即可。

P2-13 彩色标签体系过多,筛选和元数据容易变成装饰(已完成第一轮公共组件收口)

客户影响:

素材库标签和筛选应帮助用户快速识别信息。这个问题原本会让元数据看起来比素材本身还抢眼。当前公共 Tag 组件已完成第一轮收口:默认变成中性矩形标签,取消毛玻璃、放大、强阴影和彩虹式区分;绿色和橙色仅保留为少量语义提示色。

证据:

  • components/ui/Tag.tsx:注释已改为后台筛选与分类展示口径,不再使用“苹果美学”描述。
  • components/ui/Tag.tsx:blue / purple / indigo / pink / cyan 默认映射为中性色,避免标签区域变成彩虹色块。
  • components/ui/Tag.tsx:默认圆角已从胶囊改为 rounded-md,并移除 backdrop-blur、hover:scale 和选中放大。
  • npm run type-check:已通过,公共标签组件类型无新增错误。

建议动作:

  • 后续继续检查调用处,不再通过传入紫、粉、青等颜色制造装饰感。
  • 如果需要表达状态,优先使用绿色、橙色这类语义色;普通分类继续使用中性色。
  • 首页素材卡片、筛选栏和后台表格需要在浏览器里继续做实际页面验证。

P2-14 下载统计可能虚高(已修复)

客户影响:

历史风险是下载日志和次数在确认文件可读取前就写入,文件不存在或读取失败也可能被统计为下载。当前代码已改成先确认文件可读或对象流可获取,再记录下载日志和增加次数。

证据:

  • app/api/materials/download/route.ts:S3 路径先调用 getS3ObjectStream() 获取对象流,本地 legacy 文件先 access(filePath) 确认可读,然后才调用 recordDownloadIfNeeded()。
  • app/api/materials/download/route.ts:recordDownloadIfNeeded() 仍负责创建下载日志并递增 downloadCount,但只在下载响应准备成功后执行。
  • app/api/materials/download/__tests__/route.test.ts:已覆盖 legacy 本地文件不存在时返回 404,且不记录下载日志、不增加下载次数。

建议动作:

  • 已完成:下载统计不再在文件不存在时虚增。
  • 后续如改成预签名 URL 或后台打包下载,也要保持“确认可交付后再记数”的规则。

P2-14A 监控页导出素材 CSV 复用普通素材列表接口(已补管理员专用接口)

客户影响:

监控页已经按管理员能力收口,“导出素材 CSV”也应属于管理员能力。此前它调用普通素材列表接口,存在两层问题:一是按钮隐藏不等于后端权限边界;二是普通素材列表接口内部最多返回 100 条,客户数据变多后会误以为导出了全部。当前已新增管理员专用导出接口,监控页不再复用普通素材列表接口。

证据:

  • components/Header.tsx:689:监控入口在头像下拉菜单中,定位是后台/管理员入口。
  • app/admin/_components/MonitorTab.tsx:导出素材 CSV 是监控页上的管理员操作按钮。
  • app/admin/_components/MonitorTab.tsx:导出实现已改为调用 /api/admin/materials/export?limit=10000 获取素材数据。
  • app/api/admin/materials/export/route.ts:新增管理员专用素材导出接口,未登录返回 401,普通用户返回 403,并把 BigInt 文件大小和日期转换成前端可安全处理的字符串。
  • app/api/admin/materials/export/__tests__/route.test.ts:覆盖未登录、普通用户和管理员导出路径。
  • app/api/materials/route.ts:普通素材列表 GET 继续服务首页搜索和筛选,仍保留分页上限,不再承担管理员导出的权限含义。

建议动作:

  • 后续如果客户数据超过 10000 条,应把导出改成后台任务或分批流式导出,避免一次性把大量数据塞进浏览器内存。
  • 不要只靠前端按钮隐藏来保证导出权限;后台导出类能力都应有管理员专用接口。
  • 普通素材列表接口继续服务首页搜索和筛选,不承担管理员导出的权限含义。

P2-15 密码强度偏低(已完成第一轮收口)

客户影响:

商业交付下,普通用户改密码和注册只要求 6 位偏弱。特别是初始管理员首次登录会被强制改密,如果新密码仍可设置成简单 6 位,前面的初始化安全收口会被削弱。

证据:

  • lib/password-policy.ts:新增统一密码强度规则,至少 10 位,并要求大写字母、小写字母、数字、符号中的至少 3 类,同时拦截常见弱密码和全重复字符。
  • app/api/user/change-password/route.ts:改密接口改为复用统一密码强度规则;首次登录强制改密也必须使用强密码。
  • app/api/auth/register/route.ts:注册接口改为复用统一密码强度规则。
  • components/ChangePasswordModal.tsx、app/register/page.tsx:前端提示和 minLength 调整到同一规则,但最终拦截仍以后端为准。
  • lib/__tests__/password-policy.test.ts、app/api/user/change-password/__tests__/route.test.ts、app/api/auth/register/__tests__/route.test.ts:新增测试覆盖弱密码拒绝和强密码通过。

建议动作:

  • 已完成:注册、普通改密、首次登录强制改密统一使用后端密码强度规则。
  • 仍需继续:如后续需要更严格企业口径,可增加密码历史、过期周期、失败锁定、管理员重置流程。

P2-16 一次性管理员密码会出现在响应或日志里(已完成第一轮收口)

客户影响:

初始化时展示系统生成的一次性密码是必要的,否则客户无法首次登录;但环境变量中配置的固定初始密码、seed 输出、许可证服务部署日志不应再回显明文,避免客户现场日志长期保存后造成泄露风险。

证据:

  • lib/system-init.ts:环境变量 INIT_ADMIN_PASSWORD 仍会用于创建管理员哈希,但不再作为 initialPassword 回传给接口层。
  • app/api/license/activate/route.ts、app/api/license/activate-free/route.ts、app/api/system/init/route.ts:只有 passwordSource === 'generated' 时才返回一次性初始密码;环境变量来源统一返回 null。
  • app/activate/page.tsx:环境变量来源时提示“初始密码已由部署配置提供”,不再误导为“管理员账号已存在”。
  • prisma/seed.ts:未设置 INIT_ADMIN_PASSWORD 时不再生成并打印随机管理员密码;设置后只写入哈希,不在日志输出明文。
  • license-server/scripts/init-admin.ts、license-server/scripts/generate-keys.ts、license-server/deploy.ps1:不再向终端/部署日志打印完整初始管理员密码。
  • app/api/license/activate-free/route.ts:免费版激活码日志改为脱敏显示,异常日志也会脱敏 activationCode 和签名信息。
  • lib/__tests__/system-init.test.ts、app/api/license/activate/__tests__/route.test.ts、app/api/license/activate-free/__tests__/route.test.ts、app/api/system/init/__tests__/route.test.ts:新增测试覆盖环境变量密码不回显、系统生成密码仍只在首次响应展示。

建议动作:

  • 已完成:环境变量配置的固定初始密码不再通过 API、页面或脚本日志回显明文。
  • 已完成:免费版激活码和许可证签名不再在素材系统激活日志中完整输出。
  • 已完成:系统生成的一次性初始密码仍只在首次创建管理员时返回,并继续强制首次登录改密。
  • 仍需继续:如果后续新增安装脚本或初始化入口,必须复用同一规则:生成的一次性密码只展示一次;配置文件/环境变量里的密码不在日志和接口中回显。

6. 建议整改顺序

第一轮,先做安全和私有化边界:

  1. 修 GET /api/system-settings 敏感信息泄露。
  2. 给素材列表、详情、模板、筛选等私有 API 加统一登录要求。
  3. 收紧对象存储公开读取,改成登录后代理或短期签名。
  4. 收紧 CORS。
  5. 清理钉钉固定密码和硬编码密钥。
  6. 移除仓库中的真实 .env.production,并让生产密钥必填。

第二轮,做商业升级和数据一致性:

  1. 迁移失败直接阻断容器启动。
  2. 补升级前备份、迁移、健康检查、失败回滚流程。(主发布脚本第一轮已完成)
  3. 上传失败时补偿删除已上传对象。(已完成第一轮)
  4. 删除失败时保留数据库引用和失败清单。(已完成第一轮)
  5. 审计日志不要随素材硬删。(已完成第一轮)
  6. 视频转码任务持久化。(已完成第一轮)

第三轮,做模板系统真可用:

  1. 决定自定义字段搜索和筛选的底层方案。
  2. 修 isSearchable、首页筛选计数、动态筛选查询。
  3. 模板迁移小批量事务化已完成;大批量任务化迁移仍待做。
  4. 旧字段默认备份保留和迁移前字段差异预览已完成;前端确认流程仍待接入。
  5. 上传、编辑和模板迁移 metadata 超限已直接报错。
  6. 条件显示规则前后端都执行。(已完成第一轮)

第四轮,做权限和体验收口:

  1. 统一超管、全局管理员、部门管理员、普通用户权限 helper。
  2. 系统设置、用户管理、破坏性操作拆权限。
  3. 统一素材编辑权限。(已完成第一轮)
  4. 模板管理增加免费版升级提示。(已完成)
  5. 继续确认系统监控、转码统计、模板管理可见性是否需要按部门范围裁剪或仅最高权限可见。

并行推进的前端整改顺序:

  1. 先改公共样式层:lib/ui-styles.ts、app/globals.css、components/ui/Modal.tsx、components/ui/Tag.tsx。
  2. 再改后台 Tab 头部:模板、用户、架构、分类、监控、外观设置统一成紧凑工具栏。
  3. 用户管理和组织树改成更像后台表格/树形行,不再用卡片流和渐变徽章。
  4. 首页素材卡片收敛:少阴影、不上浮、弱化榜单角标、提高素材信息扫读效率。
  5. 上传、编辑、预览弹窗统一成低干扰表单和预览,不用 emoji、发光必填、渐变输入框。
  6. 模板编辑器工具化:减少卡片套卡片,窄屏改上下布局或 tabs,拖拽动画降级。
  7. 登录、激活、钉钉回调、完善资料页统一成私有化安装/访问风格。

7. 文档清理建议

默认入口保留并维护:

  • README.md
  • AGENTS.md
  • docs/README.md
  • docs/项目目标与目标环境.md
  • docs/当前工作流.md
  • docs/deployment/部署说明.md
  • docs/deployment/多品牌 NAS 与 Linux 交付差异清单.md
  • docs/初次部署与出厂设置指南.md
  • docs/许可证服务接入说明.md
  • docs/其他项目接入许可证系统说明.md
  • docs/api/模板系统API文档.md
  • docs/user-guide/模板系统用户手册.md

内部参考资料,保留但不作为默认操作入口:

  • docs/参考源码功能吸收整理_20260513.md
  • docs/商业化地基排查问题清单_20260524.md
  • docs/许可证链路安全评估_20260312.md

已移出默认入口:

仍建议单独归档的旧材料:

  • 旧服务器部署脚本说明和一次性排障报告

说明:

这些材料不一定完全无价值,但不应作为当前项目事实入口。后续开发、部署、验收优先看当前代码、当前配置和上面列出的长期维护文档。

8. 本次排查方式

  • 使用 Codex multi-agent 并行排查后端/存储、权限/许可证、模板系统、部署升级、前端流程。
  • 主线程复核了关键证据,并修正了子排查中不准确的一点:prisma/migrations 当前不是只有锁文件,实际存在多条迁移目录;真正风险是迁移失败后继续启动,以及升级/回滚 SOP 不完整。
  • 本次没有修改业务功能代码,没有连接生产服务器,没有写入数据库。
  • 本次文档不记录任何真实密码、密钥或服务器敏感值。

9. 2026-05-25 继续排查记录

本地开发环境误连旧服务器(已定位并记录)

客户影响:

本地开发系统如果继续读取旧服务器数据库地址,后台会表现为用户列表、部门列表等接口 500。这个现象容易被误判成权限或业务代码坏了,实际是环境文件和运行进程没有回到当前本地测试环境。

证据:

  • 浏览器验证时,/api/admin/users?pageSize=1000 和 /api/departments 曾返回 500,/api/system-settings 走了兜底或缓存。
  • Node/Prisma 直接查询时复现 Can't reach database server at 45.249.208.118:5432。
  • 重启 dev 服务后,启动日志显示加载 .env.development.local, .env.local, .env,浏览器内重新请求 /api/admin/users?pageSize=1000、/api/departments、/api/system-settings 均返回 200。
  • docs/当前工作流.md 已补充本地调试前检查实际环境文件和旧服务器地址的说明。

建议动作:

  • 保持本地调试优先使用 .env.development.local 的本机数据库和本机对象存储。
  • 旧测试服务器 45.249.208.118:4320 不再作为当前项目事实来源。
  • 如果后续仍需要保留 .env.local 或 .env 中的旧值,只能作为本机私有临时文件,不能写进 Git 或交付文档。

2026-05-28 继续推进:

  • 已新增 scripts/prisma-local-dev.mjs,本机数据库命令统一通过 .env.development.local 执行,并默认拒绝连接非本机数据库,避免 Prisma 命令误读根目录 .env 后连到废弃环境。
  • package.json 已新增 dev:local、prisma:dev:status、prisma:dev:apply、prisma:dev:migrate、prisma:dev:studio,本机 Codex 调试默认使用 http://localhost:3002,避免和其他项目占用的 3000 冲突。
  • docs/当前工作流.md 已补充本机 Prisma 命令入口,明确客户部署和正式服务器仍走部署脚本里的 prisma migrate deploy,不使用本机开发包装命令。
  • 本机 flnxi_local 数据库已在写入前导出备份到 tmp/flnxi-local-before-schema-sync-20260528-121058.sql;随后补齐缺失的 dingtalk_login_tokens、material_share_tokens、video_transcode_jobs、material_logs.materialSnapshot 等结构,并将 Prisma 迁移账本登记为已应用。
  • 排查中发现 homeDeleteButtonMode 已有迁移和接口使用,但 prisma/schema.prisma 未登记字段;已补齐 schema,避免后续迁移状态和代码事实不一致。
  • 验证已通过:npm run prisma:dev:status 显示本机库 up to date;node scripts/prisma-local-dev.mjs migrate diff --from-schema-datasource prisma/schema.prisma --to-schema-datamodel prisma/schema.prisma --script 输出空迁移;npx prisma validate 通过;npm run dev:local 启动后不再出现 video_transcode_jobs 缺表错误,http://localhost:3002/login 返回 200;npm test -- --runTestsByPath "app/api/system-settings/__tests__/route.test.ts" "lib/__tests__/public-share-settings.test.ts" "app/api/materials/transcode/__tests__/route.test.ts" --runInBand 通过;npm run type-check 通过。

字段池和自定义模板同步安全(2026-05-28 已完成第二轮)

客户影响:

自定义模板是当前项目的重要商业功能。字段池同步如果按当前模板“硬删除”字段或选项,客户在后台配置过的首页筛选名称、排序、启用状态和历史选项会丢失,表现为“模板改了一下,前台筛选配置突然没了”。如果模板已经保存成功、字段池同步失败,还会出现页面提示失败但模板实际已变的半成功状态。当前已把字段池同步改为非破坏式,并把模板创建、更新、删除与字段池同步放入同一个数据库事务:模板字段不再作为分类来源时只清空来源记录,旧记录普通读取接口不再展示,但数据库里的配置保留;模板下拉选项减少时旧选项也保留;字段池同步失败时模板写入也会跟随事务回滚。

证据:

  • lib/field-pool.ts:全量字段池同步不再删除 FieldDefinition 或 FieldOption;移出模板来源的字段只把 sourceTemplateIds 清空。
  • lib/field-pool.ts:syncFieldPoolFromTemplates() 支持传入事务客户端;没有传入时仍保持原来的自建事务行为,兼容后台分类管理手动同步。
  • lib/field-pool.ts:字段池普通读取和首页筛选读取只返回标准字段或仍有关联模板来源的字段,避免废弃字段继续出现在前台。
  • lib/field-pool.ts:增量同步也改为归档孤立字段,不再物理删除字段和选项。
  • app/api/admin/upload-templates/route.ts:创建模板和字段池同步现在处于同一个 prisma.$transaction。
  • app/api/admin/upload-templates/[id]/route.ts:更新模板、字段增删改、字段池同步和最终读取现在处于同一个 prisma.$transaction。
  • lib/upload-template-save-validation.ts、app/api/admin/upload-templates/route.ts、app/api/admin/upload-templates/[id]/route.ts:模板创建和更新共用服务端保存校验;模板状态只能是 draft、active、archived,必填下拉/单选字段必须有可用选项或已加入分类管理,字段条件不能引用不存在字段、不能自引用、不能循环依赖。
  • app/api/admin/upload-templates/[id]/route.ts:使用中的模板不再允许把已加入分类的字段改成不加入分类,避免客户前台分类和筛选入口被静默拿掉。
  • app/api/admin/upload-templates/[id]/route.ts:使用中的模板不再允许关闭已启用搜索的字段,也不允许把原本可填写的字段改成只读,避免历史素材仍有值但用户搜不到或后续表单无法继续维护。
  • app/api/admin/upload-templates/[id]/route.ts:删除未使用模板和字段池同步现在处于同一个 prisma.$transaction。
  • lib/__tests__/field-pool.test.ts:新增测试覆盖模板集合移除字段和模板下拉选项减少时不物理删除字段池记录或旧选项。
  • app/api/admin/upload-templates/__tests__/template-crud.test.ts:新增测试锁定创建、更新、删除模板时 syncFieldPoolFromTemplates(tx) 必须复用同一个事务,并覆盖非法模板状态、必填单选缺选项、条件引用不存在字段和循环依赖。
  • app/api/admin/upload-templates/__tests__/template-crud.test.ts:新增测试锁定使用中的模板不能取消字段加入分类、关闭字段搜索或把可填写字段改成只读。

验证:

  • npm test -- --runTestsByPath "lib/__tests__/field-pool.test.ts" --runInBand:通过,30 个用例通过。
  • npm test -- --runTestsByPath "app/api/category-fields/__tests__/route.test.ts" "app/api/template-categories/__tests__/route.test.ts" "app/api/home-filter-fields/__tests__/route-auth.test.ts" "lib/__tests__/field-pool.test.ts" --runInBand:通过,42 个用例通过。
  • npm test -- --runTestsByPath "app/api/admin/upload-templates/__tests__/template-crud.test.ts" "lib/__tests__/field-conditions.test.ts" --runInBand:通过,80 个用例通过。
  • npm test -- --runTestsByPath "app/api/admin/upload-templates/__tests__/template-crud.test.ts" "app/api/category-fields/__tests__/route.test.ts" "app/api/template-categories/__tests__/route.test.ts" "app/api/home-filter-fields/__tests__/route-auth.test.ts" "lib/__tests__/field-pool.test.ts" --runInBand:通过,74 个用例通过。
  • npm run type-check:通过。
  • npx prisma validate:通过。
  • git diff --check:通过;仅有 Windows 行尾提示,不是空白错误。

仍需继续:

  • 使用中的模板如从“加入分类”改为“不加入分类”、关闭搜索或改成只读,当前已在接口层阻断,并返回模板结构锁定提示;后续可再把前端提示做得更直观。
  • 后续可以继续补“字段池同步失败时模板写入回滚”的更贴近集成测试;当前单元测试已锁定同步函数必须接收同一个事务客户端。

部署和升级链路默认值收口(2026-05-25 已推进)

客户影响:

客户安装和升级最怕“入口不唯一”。如果发布脚本、Compose 默认镜像、部署文档分别指向不同 Registry、旧测试服务器或 latest,客户会出现拉不到镜像、推错仓库、升级后重启回旧镜像、回滚命令看似执行但实际无效等问题。

本轮已完成:

  • deploy-registry.ps1 已移除 156.239.227.105 / 156.239.227.105:5000 作为默认服务器和默认 Registry;发布和部署必须显式传入 -RegistryHost,远程部署还必须显式传入 -Server。

  • deploy-registry.ps1 的 DeployApp 默认通道已改为 release,客户现场部署命令必须显式带 -DeployChannel release -Version x.x.x;内部测试通道必须显式写 -DeployChannel test。

  • deploy-registry.ps1 对 HTTP Registry 增加前置提示:本机 Docker 和目标机 Docker 都必须配置 insecure-registries,否则登录、推送或拉取会失败。

  • deploy-registry.ps1 的 DeployApp 默认会持久写入远端 .env 的 APP_IMAGE、访问地址和 Runtime 镜像;只有临时验证时才用 -NoUpdateRemoteEnv。

  • docker-compose.prod.yml 的主应用镜像已改为必填完整 APP_IMAGE,不再默认回落到 Harbor 或 latest。

  • .env.example 已补充 APP_IMAGE、POSTGRES_IMAGE、MINIO_IMAGE、MINIO_MC_IMAGE 示例,其中 MINIO_MC_IMAGE 使用固定版本。

  • deploy-registry.ps1 的 Runtime 同步清单已移除 minio/minio:latest 和 alpine:latest,minio/mc 改为固定版本 RELEASE.2025-04-16T18-13-26Z。

  • docker-compose.source-test.yml 的 MinIO 和 MinIO Client 默认值已改成固定版本,源码测试入口不再默认使用 latest。

  • docs/deployment/部署说明.md 已把 APP_IMAGE 必填、HTTP Registry 前置配置、版本化发布命令和完整 APP_IMAGE 回滚写成主线步骤。

  • Dockerfile 已取消 npm run build 失败后继续出镜像的逻辑;构建失败会直接阻断镜像产出。

  • Dockerfile 已改为生产镜像执行 npm run build:prod,构建后会对服务端 chunks 和许可证 API 产物做混淆;混淆失败会直接阻断镜像产出,避免“脚本存在但客户镜像未启用”的防拆空档。

  • scripts/build-obfuscated.js、scripts/obfuscate-config.js、.dockerignore:混淆依赖缺失不再静默跳过;Docker 构建上下文只放行生产构建和容器启动必需脚本,确保混淆配置能进入构建阶段。

  • deploy-new-fixed.ps1、发布管理.ps1、license-server/deploy.ps1 已加默认停止闸门;不追加明确确认参数时不会继续执行旧服务器、旧 Harbor、许可证部署或 Docker 清理逻辑。

  • flnxi-agent/README.md 已把 Node 版 Agent 定位改成历史实现说明,不再把 Docker 运行命令写成新客户推荐安装方式。

  • 已完成:deploy-registry.ps1 已纳入版本管理;之前“仍是未跟踪文件状态”的记录已过期。

  • 已完成:docs/deployment/部署说明.md 已把发布方构建、客户首次安装、已有客户升级拆成三段,删除生产 Compose 的误导性 --build 命令,并明确根目录 docker-compose.yml 不是客户正式安装入口。

  • scripts/build-material-image.ps1 已加历史/内部备用说明;如要用它推送旧 Harbor,必须显式追加 -AcknowledgeLegacyHarborPush,避免误当客户发布入口。

  • scripts/README.md 已补充旧 Harbor / latest 脚本不能直接用于客户安装或升级。

  • docker-images/README.md 已改成离线/受控网络备用说明,不再说生产 Compose 默认从 Harbor 拉运行时镜像。

  • docker-images/README.md 和 docker-images/minio-mc/VERSION.txt 已把现存 minio/mc:latest 标成“历史本地缓存,不作为正式离线交付版本”;正式做离线包前仍需要重新导出固定版本或直接归档旧缓存。

  • 镜像体积偏大不是单一文件误打包,运行镜像里 canvas 运行库、字体、ffmpeg、Prisma CLI 等都是体积来源,需要单独做镜像瘦身专项。

建议动作:

  • flnxi-agent-go/install.sh 已不再默认拉取旧 Harbor latest Agent 镜像,必须通过 AGENT_IMAGE 或 --image 显式传入完整 Agent 版本镜像,并会把 FLNXI_MATERIAL_IMAGE、FLNXI_MATERIAL_IMAGE_REPOSITORY、FLNXI_POSTGRES_IMAGE、FLNXI_MINIO_IMAGE、FLNXI_VERSION_LIST_URL 等配置传入 Agent 容器。
  • flnxi-agent-go/internal/api/handlers.go 已把首装主应用镜像、PostgreSQL 镜像、MinIO 镜像改为请求字段或环境变量配置;主应用镜像缺失时直接失败,不再默认拉取旧 Harbor / latest。
  • flnxi-agent-go/internal/api/handlers.go 已把升级镜像改为完整镜像或 FLNXI_MATERIAL_IMAGE_REPOSITORY + version,并禁止 latest 作为客户升级版本。
  • flnxi-agent-go/internal/api/handlers.go 和 web/dist/index.html 已移除版本列表的旧 Harbor / latest 兜底;未配置版本来源时显示未配置,不再伪造默认版本或 1.0.0。
  • flnxi-agent-go/internal/docker/client.go 临时 Alpine 镜像已改为 ALPINE_IMAGE 可配置,默认 alpine:3.21,不再绑定旧 Harbor。
  • flnxi-agent-go/Dockerfile 运行阶段已改为 docker.m.daocloud.io/alpine:3.21,不再使用 Alpine latest。
  • flnxi-agent-go/build.ps1 已改为可传 -ImageName,默认示例 Registry,不再默认推旧 Harbor 或 latest。
  • flnxi-agent-go/README.md 已同步当前 Go Agent 的镜像配置、版本来源和内部验证定位。
  • 验证:go test ./... 已在 Docker golang:1.22-alpine 容器中通过;flnxi-agent-go/build.ps1 在正确目录下可构建 Agent 测试镜像,构建出的 Agent 镜像约 72.3MB,测试标签已删除;bash -n flnxi-agent-go/install.sh 通过。

仍需继续:

  • Go Agent 的版本来源还只是 FLNXI_VERSION_LIST_URL,后续需要决定正式接许可证服务、发布管理接口,还是 Registry tags 清单。
  • Go Agent Web 管理界面仍偏原型,需要后续和主项目 UI 风格一起收口。
  • 镜像瘦身单独立项:先量化当前镜像大小,再决定 ffmpeg、canvas、字体和 Prisma CLI 是否拆成可选能力或独立镜像。

10. 2026-05-30 商业化地基 8 项收口记录

本轮按“客户私有化交付是否可信”继续推进,并把短入口沉淀到 docs/商业化地基8项收口记录_20260530.md。

已完成收口:

  • 系统监控和视频转码统计改为最高权限账号可读;头像菜单里的“监控仪表板”也只对最高权限账号显示。
  • 监控接口遇到数据库查询失败时不再继续报告 healthy,避免安装或升级验收误判。
  • 模板管理前端操作权限与后端一致:普通管理员可查看,自定义模板的创建、复制、编辑、删除、迁移只给最高权限账号开放。
  • 内网穿透启动、停止、状态查询和 AI 改图不再返回占位成功结果;未接入真实提供商或真实任务引擎时直接返回未实现。
  • 上传失败时,生产环境不再把原始错误详情、数据库错误或存储错误返回给前端。
  • 素材编辑换预览图时,先上传新图、数据库更新成功后再删旧图;数据库更新失败会清理新上传图。
  • 软删除素材不再能通过下载接口继续下载。
  • 全库清理接口增加确认短语,避免最高权限账号误触发后直接清空素材。
  • 公开注册默认值已贯穿数据库字段、系统设置兜底、首次创建设置和注册接口;没有系统设置记录时也不会默认开放公开注册,后台开关只允许最高权限账号修改。
  • 前端先收三处明显模板感:全局底色去渐变、登录页钉钉按钮降级、素材预览弹窗去掉移动端固定最小宽度。

仍需继续:

  • 外部数据库、外部对象存储、大体量客户和离线包升级仍需要实机演练。
  • 外部 S3、历史 bucket、历史对象 ACL、公共 Logo / 头像与私有素材的拆分策略仍需要只读审计。
  • 上传分片、断点续传、PSD 大图限制和大批量模板迁移任务化仍未完成。
  • Go Agent 正式版本来源和管理界面仍未定稿。
  • 上传弹窗、模板编辑弹窗、首页素材卡动作区、邀请管理移动端表格仍需要继续 UI 收口。