flnxi-Wu/docs/商业化地基排查问题清单_20260524.md

104 KiB
Raw Blame History

商业化地基排查问题清单

更新时间:2026-05-25

本文件是当前项目面向企业/团队私有化交付前的地基排查记录。它不是对外销售文案,也不是旧参考源码功能吸收清单,而是后续整改的事实入口。它是内部整改跟踪记录,已修复、已完成和待整改条目会同时保留,阅读时必须先看每条状态,不能把整份文件理解成纯待办清单。

本次排查以当前仓库代码、配置、部署文件和现存文档为准。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/__tests__/route.test.ts:测试锁定未登录只返回公开字段、普通管理员不返回对象存储密钥、最高权限账号才返回对象存储密钥,并确认 GET 不再补写对象存储密钥。
  • 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:新增分享管理权限判断,素材负责人、上传者、个人归属人、管理员可以创建和管理该素材的分享链接。
  • 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/materials/route.ts:homeField_xxx 现在会先确认字段池中该字段已启用;标准字段走固定列,自定义字段走 metadata.templateFields.xxx = 选中值。
  • app/api/home-filter-fields/__tests__/route-auth.test.ts:新增测试,锁定自定义首页筛选字段会从 metadata 统计数量。
  • app/api/materials/__tests__/materials-list-auth.test.ts:新增测试,锁定首页自定义字段筛选会按 metadata 字段值过滤素材。

建议动作:

  • 已完成:自定义字段首页计数和筛选先用 JSON 查询打通。
  • 仍需继续:字段池同步目前主要围绕 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 或使用数据库回查结果,不只信旧会话。

证据:

  • 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 现在按最高权限/部门范围/本人范围过滤;分页总数和搜索条件使用同一权限范围,避免普通管理员看全量用户或通过总数泄露全量。
  • 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:分类字段启用、显示名和排序写入现在必须是最高权限账号。
  • app/api/template-categories/[fieldKey]/route.ts:批量修改模板分类选项现在必须是最高权限账号。
  • app/api/materials/transcode/route.ts:PATCH action=recover 转码恢复维护现在必须是最高权限账号;普通管理员仍可按既有规则触发单个素材转码任务。
  • 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:补充普通管理员不能恢复卡住的转码任务、最高权限账号可恢复。

验证:

  • 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

建议动作:

  • 已完成:组织结构、用户归属、统一用户更新、用户列表范围、模板写操作、模板迁移、素材永久删除、过期清理、系统维护、内网穿透维护、素材恢复、全局字典、分类字段、模板选项和转码恢复维护的敏感权限已阶段性收紧。
  • 仍需继续:取消部门管理员当前只清 managedDepartment,不会自动把 role: ADMIN 降回 USER;这涉及“全局管理员”和“部门管理员”模型拆分,建议后续单独设计。
  • 仍需继续:继续复核剩余 ADMIN 写接口,尤其是素材编辑、批量、导入导出类入口,避免新功能绕开最高权限边界。
  • 仍需继续:app/api/materials/[id]/route.ts 的编辑边界、app/api/admin/materials/export/route.ts 的导出范围,以及店铺/场景分类 POST 是否允许普通上传者新增,需要按前端真实入口继续定规则。

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 上传收紧到最高权限账号,并且每次写入前回查数据库确认当前用户仍是最高权限,避免旧会话残留权限。

证据:

  • app/api/system-settings/route.ts:PUT 现在使用 isCurrentSuperAdmin(),普通管理员不能修改站点名、注册开关、S3 endpoint、access key、secret key 等系统设置。
  • app/api/system-settings/upload-logo/route.ts:系统 LOGO 上传也改为最高权限账号可操作,避免通过上传接口绕过外观设置权限。
  • app/api/system-settings/__tests__/route.test.ts:新增测试覆盖普通管理员不能修改系统设置、旧会话权限已撤销时不能修改、最高权限账号可修改,以及普通管理员不能上传 LOGO。

验证:

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

建议动作:

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

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

客户影响:

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

证据:

  • lib/license/license-service.ts:新增当前有效许可证读取逻辑,isFeatureEnabled()、checkLimit()、getLicenseType() 都会从本地已验签许可证重新取当前配置,并把过期许可证视为不可用。
  • lib/license/license-service.ts:saveLicenseLocally()、updateLocalLicense() 会同步更新内存缓存;如果新许可证已过期,则不会继续保留可用缓存。
  • lib/__tests__/license-service.test.ts:新增测试覆盖许可证过期后高级功能不可用、数量限制不放行,以及本地许可证从 Pro 降级到 Free 后业务检查立即按 Free 生效。
  • lib/__tests__/check-limits.test.ts:功能检查 helper 已全部改为异步调用底层许可证检查。

验证:

  • npm test -- lib/__tests__/license-service.test.ts lib/__tests__/check-limits.test.ts --runInBand
  • npm run type-check

建议动作:

  • 已完成:受限业务 API 继续通过 canUseCustomTemplates()、canUseDepartmentIsolation()、canUseSystemSettings()、canUploadMaterial()、canCreateUser() 等 helper 检查;这些 helper 底层不再信旧缓存。
  • 后续增强:吊销、宽限期、远端不可达时的产品策略仍需单独定规则;当前本轮先保证过期和降级不会因为本地旧缓存继续放行。

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。

建议动作:

  • 保持现有补偿清理测试覆盖。
  • 后续如引入分片/直传上传,要继续保留“数据库失败自动清理已上传对象”的规则。

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:运行时兜底建表会添加 materialSnapshot,并删除旧的素材级联外键,避免老库又补回级联删除。
  • app/api/admin/material-logs/route.ts:改为 LEFT JOIN 查询日志,素材已硬删除时优先显示快照,否则显示“已删除素材”。
  • lib/__tests__/material-log.test.ts:覆盖兜底建表不再创建素材级联外键,并验证日志会保存素材快照。

建议动作:

  • 后续增强:如需更强合规追责,再评估 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:非音频原始素材改为 zipFile.stream() + uploadStreamToS3(),大视频、大压缩包、PSD 原始文件不再在上传路由里二次复制成完整 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__/local-file-stream.test.ts:新增本地文件完整响应、Range、后缀 Range、非法 Range 测试。
  • app/api/materials/download/__tests__/route.test.ts:新增 legacy 本地下载和 Range 下载测试。

建议动作:

  • 后续必须继续:上传入口从 Next.js formData() 改成流式 multipart 或浏览器直传 MinIO/S3;这是 P1-11 最大剩余项。
  • 后续继续:音频元数据解析仍依赖 Buffer,需要改成临时文件/流式解析后,音频原始文件才能彻底走流式上传。
  • 后续继续:预览图和 PSD 展示图仍会进入内存处理,但已有图片体积限制;后续可按客户文件规模决定是否做更细的切片/临时文件方案。
  • 限制单文件大小,并按客户机器等级配置。

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:事务内使用 id + sourceTemplateId 更新素材,素材已不属于源模板时抛错并整体回滚。
  • 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/__tests__/template-migrate.test.ts:新增旧自定义字段、旧标准字段备份和可清空字段清理测试。

建议动作:

  • 已完成:默认保留旧字段到备份命名空间,避免历史字段无声丢失。
  • 仍需继续:迁移前给预览报告,列出哪些字段会进入备份区,哪些字段会成为目标模板当前字段。

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/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-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 -- 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 已完成第一轮持久化恢复;后续继续增强失败原因展示和管理端重试入口。

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:模板管理页用 isFree + customTemplates 计算自定义模板能力;新建、编辑、复制和从预制模板快速创建都会先检查能力,未授权时打开 UpgradeModal。
  • app/admin/_components/template-manager/CustomTemplateSection.tsx:自定义模板区免费版显示 ProBadge 和“当前许可证未开放自定义模板功能”提示;点击受限动作只触发授权提示,不直接打开编辑器。
  • 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。

建议动作:

  • 已完成:免费版自定义模板入口有前置授权提示,受限动作不会直接进入编辑流程。
  • 已完成:后端创建、更新、删除和迁移入口继续保留许可证强制。
  • 仍需继续:后续统一把“升级后”话术改成“当前许可证未包含/联系管理员处理授权”,保持私有化交付语气一致。

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 降级为普通工作台控件。
  • 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/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。
  • npm run type-check:已通过。

建议动作:

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

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

客户影响:

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

证据:

  • app/admin/_components/template-manager/TemplateManagerHeader.tsx:22:模板页头部使用 rounded-3xl、大阴影、hover 大阴影。
  • app/admin/_components/template-manager/TemplateManagerHeader.tsx:31:显示英文小标题 Template Management。
  • app/admin/_components/template-manager/TemplateManagerHeader.tsx:35:头部带解释性长句。
  • 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:333:外观设置页使用动态径向渐变背景。
  • app/admin/_components/AppearanceSettingsPanel.tsx:357:外观设置页使用浮动光球。
  • app/admin/_components/AppearanceSettingsPanel.tsx:415:显示 Appearance Studio 和渐变文字标题。

建议动作:

  • 后台 Tab 页头统一改为紧凑工具栏:左侧中文标题,右侧搜索、筛选和主要操作。
  • 删除英文 eyebrow 小标题,例如 Template Management、User Management、Appearance Studio。
  • 删除头部动态光晕、浮动光球、hover 大阴影。
  • 页面说明只在空状态、帮助入口或 tooltip 里出现,不放在每个 Tab 首屏。
  • 已推进:app/admin/_components/UsersTab.tsx 的用户管理头部已从大 Hero 卡片收敛为普通后台工具区;最高权限 / 管理员标识已从紫粉渐变胶囊改为中性徽章。
  • 已推进:app/admin/_components/MonitorTab.tsx 的监控页头部已从展示页式大卡片收敛为普通后台工具区;指标卡改为中性竖线标识,不再使用 emoji 圆形图标和发光阴影。
  • 已推进:app/admin/_components/CategoriesTab.tsx 的分类管理页头部、左侧列表、字段启用编辑态和可添加字段列表已完成第一轮工作台化收口。

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

客户影响:

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

证据:

  • app/admin/_components/UsersTab.tsx:423:用户行使用 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:376:部门节点使用紫蓝渐变背景、左边框、圆角、阴影和 hover 放大。

建议动作:

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

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

客户影响:

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

证据:

  • app/page.tsx:400:精品、下载榜、点赞榜使用多段渐变角标。
  • app/page.tsx:1764:排序方式使用大号胶囊按钮。
  • app/page.tsx:1870:素材卡片默认大圆角、大阴影,hover 上浮。
  • app/page.tsx:2043:榜单角标叠在素材封面上方。
  • app/page.tsx:2061:SPU 型号使用黑色半透明胶囊和毛玻璃。
  • app/page.tsx:2073:点赞按钮使用悬浮圆形、阴影和毛玻璃。
  • app/page.tsx:2211:列表底部文案“继续向下滚动加载更多”偏引导式。
  • app/page.tsx:2217:文案“已经到底啦~”语气偏轻快。

建议动作:

  • 素材卡片改为轻边框、少阴影、不上浮,封面区域尽量让素材本身占主导。
  • 榜单/精品信息弱化为中性小标签或排序条件,不用多段渐变角标。
  • 排序控件改成紧凑 segmented control 或普通按钮组。
  • 工作状态文案改为“加载中”“暂无更多素材”等朴素表达。

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

客户影响:

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

证据:

  • components/UploadModal.tsx:50:已完成第一轮收口,动态模板字段统一使用中性输入框样式。
  • components/UploadModal.tsx:1196:恢复上传提示已去掉可见 emoji。
  • components/UploadModal.tsx:1233:上传弹窗内部外壳已从 rounded-apple、shadow-apple-lg 收为普通圆角和轻阴影。
  • components/UploadModal.tsx:1245:上传提示已改为普通文字,不再使用 emoji。
  • components/UploadModal.tsx:1499:PSD 展示图提示已去掉 emoji。
  • components/UploadModal.tsx:1661、components/UploadModal.tsx:1812:必填字段不再使用蓝色渐变背景和大阴影,改为边框/浅底提示。
  • components/EditMaterialModal.tsx:240:编辑弹窗必填标识同样使用紫蓝渐变胶囊。
  • components/EditMaterialModal.tsx:265:编辑输入框也有必填渐变背景。
  • components/AudioVisualizer.tsx:音频默认封面和播放频谱已先从 Apple Music 式深色渐变、光晕、毛玻璃改成中性封面和低调频谱。
  • components/MaterialPreviewModal.tsx:音频预览外壳已从深色舞台卡片改成普通中性卡片;时长和分类标签改成中性标签。
  • components/MaterialPreviewModal.tsx:448:视频预览状态使用大块深色渐变说明面板。
  • components/MaterialPreviewModal.tsx:517:详情标题使用较大的 text-2xl 加粗。
  • components/MaterialPreviewModal.tsx:599:转码状态说明句子较长。

建议动作:

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

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

客户影响:

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

证据:

  • app/admin/_components/TemplateManager.tsx:模板编辑器弹窗仍偏大,当前为 max-w-6xl,后续还要评估是否进一步拆成更轻的工具面板。
  • 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/activate/page.tsx、app/admin/_components/template-manager/editor/TemplatePreviewPanel.tsx、app/admin/_components/template-manager/editor/TemplateFieldsEditor.tsx 已无 rounded-2xl、shadow-xl、shadow-2xl、backdrop-blur、zoom-in、cubic-bezier、duration-500、tracking-wider、shadow-blue、bg-gradient 命中。
  • 仍需继续:app/admin/_components/TemplateManager.tsx 的编辑器弹窗尺寸和交互复杂度还没有彻底重构,当前只是先收掉最显眼的舞台化视觉,并解决窄屏左右挤压。

建议动作:

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

P2-11 授权/付费提示偏公网 SaaS 营销话术(升级弹窗已先收口)

客户影响:

当前项目目标是私有化交付。许可证限制应该像“当前许可证不包含此能力”的工作提示,而不是“升级后可享受”的营销弹窗。否则客户会感觉产品不是为私有部署准备的。升级弹窗已先改成中性限制提示,但 PRO 徽章、其它授权入口和部分页面样式仍需继续统一。

证据:

  • components/ui/ProBadge.tsx:6:组件说明是付费功能 PRO 徽章。
  • components/ui/ProBadge.tsx:38:PRO 徽章使用紫蓝渐变。
  • components/ui/UpgradeModal.tsx:62、components/ui/UpgradeModal.tsx:86、components/ui/UpgradeModal.tsx:111、components/ui/UpgradeModal.tsx:122:升级弹窗已改成中性图标、限制提示文案和普通按钮,不再显示“升级后可享受”。
  • components/ui/__tests__/UpgradeModal.test.tsx:测试已改为锁定限制提示和中性按钮文案。
  • components/ui/ProBadge.tsx:PRO 徽章仍使用紫色渐变,属于后续统一风格的一部分。

建议动作:

  • 改成许可证状态提示:功能不可用、当前版本、需要的版本、联系管理员/服务商。
  • 去掉营销化“升级好处”列表。
  • PRO 徽章改为中性许可证标签,不用紫蓝渐变。

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

客户影响:

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

证据:

  • app/login/page.tsx:11:默认登录 Logo 不再指向不存在的 /logo/logo2.png,没有配置时直接走文字占位。
  • app/login/page.tsx:295:登录页已改为浅灰背景和中性表单,不再使用浅蓝渐变背景。
  • 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/activate/page.tsx:激活页已完成第一轮收口,激活表单、激活成功页和免费试用弹窗统一为浅灰背景、普通边框、rounded-lg 和轻阴影;许可证输入去掉大字距和偏展示页的大号字体,蓝色主按钮改为中性深色按钮。
  • 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 验证。

建议动作:

  • 登录、激活、回调、完善资料页统一成同一套私有化安装/访问风格;钉钉回调页和激活页已完成第一轮视觉收口。
  • 背景用纯色或轻灰,不用大面积渐变。
  • 登录页可以保留品牌感,但减少泛 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:97:先创建下载日志。
  • app/api/materials/download/route.ts:120:先增加下载次数。
  • app/api/materials/download/route.ts:130:后面才判断文件路径。
  • app/api/materials/download/route.ts:217:后面才读本地文件。

建议动作:

  • 确认文件可读或签名 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-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 失败后继续出镜像的逻辑;构建失败会直接阻断镜像产出。
  • deploy-new-fixed.ps1、发布管理.ps1、license-server/deploy.ps1 已加默认停止闸门;不追加明确确认参数时不会继续执行旧服务器、旧 Harbor、许可证部署或 Docker 清理逻辑。
  • flnxi-agent/README.md 已把 Node 版 Agent 定位改成历史实现说明,不再把 Docker 运行命令写成新客户推荐安装方式。

仍需继续:

  • deploy-registry.ps1 仍是未跟踪文件状态,正式交付前要确认纳入版本管理或纳入交付包。
  • scripts/build-material-image.ps1 已加历史/内部备用说明;如要用它推送旧 Harbor,必须显式追加 -AcknowledgeLegacyHarborPush,避免误当客户发布入口。
  • scripts/README.md 已补充旧 Harbor / latest 脚本不能直接用于客户安装或升级。
  • docker-images/README.md 已改成离线/受控网络备用说明,不再说生产 Compose 默认从 Harbor 拉运行时镜像。
  • docker-images/minio-mc/VERSION.txt 和本地 tar 包仍是旧 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 是否拆成可选能力或独立镜像。