gongxiangp2/CLAUDE.md

45 KiB
Raw Blame History

CLAUDE.md

本文件为 Claude Code (claude.ai/code) 提供项目代码工作的指导说明。

项目概述

由壹点3D素材 - 一个具有 Apple 风格 UI 设计的现代化 3D 素材共享平台。

技术栈:

  • Next.js 14 (App Router) + React 18 + TypeScript
  • NextAuth.js 身份认证(凭证登录 + 钉钉 OAuth)
  • PostgreSQL + Prisma ORM
  • MinIO S3(预览图)+ 本地存储(压缩包)
  • Redis(上传会话管理)
  • TailwindCSS 样式

开发命令

# 开发环境
npm run dev              # 在 :3000 端口启动开发服务器

# 构建与部署
npm run build           # 生成 Prisma 客户端 + Next.js 构建
npm start               # 启动生产服务器

# 数据库
npm run prisma:generate  # 生成 Prisma 客户端
npm run prisma:push      # 推送数据库结构
npm run prisma:studio    # 打开 Prisma Studio

# 测试
npm test                # 运行 Jest 测试
npm run test:watch      # 监视模式
npm run test:coverage   # 覆盖率报告

# 代码检查
npm run lint            # ESLint

网络架构

生产基础设施

物理服务器:

  • 生产服务器:192.168.58.20
  • 构建服务器:192.168.40.206(部署脚本内的固定值;旧文档中的 192.168.42.134 已不是部署入口)
  • 公网入口服务器:218.93.9.38(FRP 入口 + Nginx 反向代理;2026-10-06 核实,原香港服务器 103.118.245.6 已下线)
  • 上游报表库:218.93.9.21(SQL Server 2019,人事与组织架构数据源)

公网访问架构:

  • 域名:sc.jiansun.vip(素材平台)、s3.jiansun.vip(MinIO)
  • 公网入口:218.93.9.38(生产机 frpc 配置自 2026-08-02 起指向此地址)
  • FRP 服务:frps-sucai(端口 7003,只放行远端端口 2321-2326)。同机 7002 是 furniture 项目的 frps,别混用
  • 生产服务器通过 FRPC 连接到入口服务器:配置 /etc/frp/frpc-*-entry.toml,日志 /root/frp/frpc-*-entry.log
  • Nginx 反向代理:sc.jiansun.vip → 127.0.0.1:2321,s3.jiansun.vip → 127.0.0.1:2323

Docker 网络:

  • 网络名称:youyidian-3d_app-network
  • 驱动类型:bridge
  • 连接的容器:
    • youyidian-3d-material(生产应用)
    • gongxiangpan-redis(生产 Redis)
    • youyidian-3d-shadow(影子环境应用)

⚠️ 生产实际部署形态(2026-08-14 现场核实,与旧文档不一致,以此为准)

CLAUDE.md 旧版记的容器名/端口映射与线上对不上,实际是蓝绿部署 + 独立 nginx 反代:

  • 应用容器采用蓝绿双实例:youyidian-3d-material-blue / youyidian-3d-material-green,同一时刻一个接流量、另一个待命回退。容器不直接映射宿主机端口(3000/tcp 不外露)。
  • 流量入口是一个独立的 local-proxy nginx(不是 TrueNAS 系统自带的那个),监听宿主机 4320:
    • 配置路径:/mnt/PoolOne/apps/youyidian-3d/local-proxy/youyidian-3d-material/nginx.conf
    • 通过 upstream 里的容器内网 IP(如 172.19.0.3:3000)指向当前生效实例
    • 切换蓝绿 = 改这个 upstream IP + nginx -s reload,current-container 文件记录当前指向
  • 应用挂载:/mnt/PoolOne/apps/youyidian-3d/uploads:/app/uploads(uploads 实际路径,注意不是部署脚本默认的 /mnt/PoolOne/youyidian-uploads)
  • 应用连的数据库是 192.168.58.20:5432/lobechat(宿主机 5432 由 gongxiangpan-postgres-1 映射)

⚠️ 大文件分片上传 500 的一个基础设施坑(2026-08-14 已修)

现象:视频等大文件分片上传,chunk 0 就返回 500,前端报"上传块 0 失败,已重试多次"或"分片合并文件必须同时提供 uploadId",但应用容器日志里查不到该 chunk 请求——因为请求在 nginx 层就被拦下了。

根因:local-proxy nginx(以 nobody 运行)和 TrueNAS 系统 nginx(以 www-data 运行)共用同一个临时缓冲目录 /var/lib/nginx/body。当系统 nginx 重建该目录并锁成 www-data drwx------ 后,nobody 无法写入,10MB 的分片缓冲失败,nginx 直接回 500。nginx -s reload 或系统 nginx 重启都可能触发。

修复:给 local-proxy 配独立临时目录,不再和系统 nginx 抢。在 nginx.conf 的 http {} 块内加:

client_body_temp_path <proxy_dir>/tmp/client_body;
proxy_temp_path       <proxy_dir>/tmp/proxy;
fastcgi_temp_path     <proxy_dir>/tmp/fastcgi;
uwsgi_temp_path       <proxy_dir>/tmp/uwsgi;
scgi_temp_path        <proxy_dir>/tmp/scgi;

并 mkdir -p 这些目录、chown -R nobody:nogroup、chmod 700,再 reload。诊断时先看 local-proxy/.../error.log 有没有 open() "..." failed (13: Permission denied)。

⚠️ 公网 502 排查顺序(2026-10-06 事故)

现象:sc.jiansun.vip 整站返回 nginx 502。

本次是两层原因叠加:

  1. 应用 / Redis / Postgres 三个容器从 9-30 起处于停止状态(退出码 0,不是崩溃,停止原因未查明),10-03 服务器重启后也没有被拉起
  2. 10-03 下午起公司防火墙拦截了生产机出网,FRP 隧道连不上入口机。第 1 层修好后公网仍是 502,防火墙放开后才恢复

排查顺序(登录生产机执行):

  1. docker ps -a 看应用容器(以 current-container 文件为准)、gongxiangpan-redis、gongxiangpan-postgres-1 是否在跑。停了就按 Postgres → Redis → 应用的顺序 docker start
  2. curl http://127.0.0.1:4320/api/health:返回 200 说明应用和本地反代都正常,问题在公网链路
  3. 看 /root/frp/frpc-sc-entry.log:如果满屏 dial tcp 218.93.9.38:7003: i/o timeout,再从生产机访问任意外网地址,全部超时就是出网被拦。这时找网管查公司防火墙,不用动应用

上游报表库(人事与组织架构数据源)

⚠️ 重要认知:花名册不是从钉钉通讯录来的。钉钉在本项目中只负责登录鉴权与消息推送,从未调用过通讯录接口。人事权威源是公司报表库。

连接信息(只读账号,配置在 .env,勿写入仓库):

  • Server:218.93.9.21
  • Database:JSReportReplica
  • Schema:rpt(只读)
  • 环境变量:REPORT_DB_SERVER / REPORT_DB_DATABASE / REPORT_DB_USER / REPORT_DB_PASSWORD

⚠️ 密码含 # 等特殊字符时,在 .env 中必须加引号,否则 # 之后会被当作注释截断,表现为登录失败。

关键表:

表 行数量级 用途
rpt.hr_groups 1075 组织架构树。仅 Id / parent_id / name 三列可用,无状态位、无排序、无编码。单一根节点(Id=1),零孤儿节点
rpt.hr_employee 1296 钉钉员工表,含 manager_userid、dept_id_list、active
rpt.hr_employee_register 1296 入职登记,含 sys01-employeeStatus 状态码、sys00-flowerName 花名
rpt.hr_flowerName_book — 完整员工档案(含社保、银行卡等敏感信息,谨慎查询)
rpt.hr_employee_dimission 4711 离职表,含 last_work_day、handover_userid 交接人

**员工状态码:2 试用、3 正式、5 已离职。**注意上游还存在中文值(正式/离职/试用)与空值,口径混杂,不可用关键词匹配判断在职。


人员架构与组织同步(2026-08-13 重构)

核心原则

钉钉唯一值是全项目唯一的人员身份判定依据。 姓名、花名、邮箱只能用于人工排错,不得自动建立绑定。

数据模型

模型 作用
UserEmployeeBinding 素材库账号 ↔ 权威人员档案的正式绑定。活跃绑定双向唯一(部分唯一索引,仅约束 status='ACTIVE')
OrganizationUnit 共享组织架构,直接镜像 hr_groups。上游删除的部门置 isActive=false 而非物理删除
PerformanceSurveyEmployeeProfile 事实上的平台级人员主档(表名仍带历史前缀)。新增 employmentState、resignedAt、handoverDingtalkUserId

users.dingtalkUserId 已加唯一约束,数据库层面杜绝一人多号。

关键文件

文件 用途
lib/employee-directory.ts 通用人员目录服务。上层统一通过它访问人员数据,不直接读带 PerformanceSurvey 前缀的模型
lib/organization-sync.ts 组织架构同步、在职状态校正、离职联动、每日定时任务
lib/material-transfer.ts 素材正式转移与部门托管

在职状态判定

不要用关键词匹配 employmentStatus(上游原样值,口径混杂)。使用规范化的 employmentState:

  • ACTIVE / RESIGNED / UNKNOWN
  • UNKNOWN 视为在职——上游查不到明确状态的人宁可多留,误判导致"人凭空消失"比"多出几个人"难发现
  • 判定条件统一走 getActivePerformanceSurveyEmployeeProfileWhere() 或 getActiveEmployeeWhere()

离职联动

上游标记离职后自动执行:归档素材库账号 + 把素材托管给所在部门。

  • 人事已确认花名册标记离职即为事实离职:离职即托管+归档,无天数缓冲(RESIGNATION_MIN_DAYS 已废弃)
  • 离职日期缺失者同样处理(日期仅作展示,不作门槛)
  • 超级管理员账号不由自动流程处理

素材归属与署名分离

这是刻意的设计,不是数据不一致:

  • Material.userId(归属)+ authorName(署名):素材是谁做的,永远记在谁名下
  • Material.custodyDepartmentId(托管部门):人离职后由部门接手维护,不改 userId 与 authorName
  • 首页卡片显示的作者读的是 user.realName || username,托管不影响显示

权限组绑定花名册身份与团队职能(2026-10-08)

  • 权限组的部门关联已从"路径字符串"改为绑定 OrganizationUnit.id 外键(迁移 20261008_permission_group_department_identity,生产 17 条已回填)。departmentPath 降级为失效关联的展示快照;鉴权一律用绑定部门的当前有效路径;一个部门只属一个组(DB 唯一索引),一人多部门取各任职权限组的并集;删除"按名称自动挂默认组"的旧逻辑。
  • 品牌中心"团队职能"已数据库化:TeamFunctionConfig(迁移 20261008_team_function_configs)每组织单元一条,负责人 = 花名册员工 profile ID,branches JSON,revision + 审计表。API app/api/organization/team-functions(GET 品牌中心可读、PUT 仅超管),UI TeamFunctionsDashboard.tsx,初始内容由 scripts/seed-team-function-config.ts 从 data/team-function-initial-content.json 导入。
  • 权限组与用户权限管理界面已合并简化(PermissionGroupsDashboard.tsx、UserPermissionsDashboard.tsx)。

定时同步

ensureDailyOrganizationSyncStarted() 在考核调研上下文中懒启动,每 24 小时执行:组织架构 → 在职状态 → 离职联动。

⚠️ 生产容器必须配置 REPORT_DB_* 环境变量,否则同步连不上上游,人员档案会退化成用素材库信息拼的兜底假档案(无工号、无职级、isLeadership=false)。

运维脚本

# 人员架构盘点(纯只读,可重复执行)
npx tsx scripts/diagnose-people-architecture.ts

# 组织架构与在职状态同步(默认预演,--apply 才写入)
npx tsx --env-file=.env scripts/sync-organization.ts
npx tsx --env-file=.env scripts/sync-organization.ts --apply

# 离职人员素材部门托管
npx tsx scripts/custody-archived-user-materials.ts

数据库配置

PostgreSQL (192.168.58.20:5432):

  • 数据库用户:gongxiangpan
  • 生产数据库:lobechat(5918 条素材记录)✅
  • 影子环境数据库:lobechat_stage(4051 条素材记录)
  • 默认数据库:gongxiangpan(旧系统,仅 4 个表,未使用)

⚠️ 重要提示:生产数据库名称是 lobechat,不是 gongxiangpan(那只是 PostgreSQL 用户名)

Redis 配置

⚠️ 【重要】Redis 连接依赖 Docker 网络配置

应用容器必须连接到 youyidian-3d_app-network 网络才能访问 Redis。如果 Redis 连接失败,会导致:

  • 上传分片无法保存,上传到 100% 后循环重传
  • 会话无法持久化,用户频繁掉线

正确的部署命令必须包含 --network 参数:

docker run -d --name youyidian-3d-material \
  --network youyidian-3d_app-network \  # ⚠️ 必须指定网络!
  -p 4320:3000 \
  ...

诊断 Redis 连接问题:

# 1. 检查容器是否在正确网络
docker network inspect youyidian-3d_app-network | grep youyidian-3d-material

# 2. 检查 Redis 连接
docker exec youyidian-3d-material sh -c "ping -c 2 gongxiangpan-redis"

# 3. 检查容器日志中的 Redis 状态
docker logs youyidian-3d-material 2>&1 | grep -i redis | tail -5

# 4. 手动连接到网络(如果遗漏)
docker network connect youyidian-3d_app-network youyidian-3d-material
docker restart youyidian-3d-material

生产环境 Redis:

  • 容器名称:gongxiangpan-redis
  • 连接地址:redis://gongxiangpan-redis:6379(Docker 网络内)
  • 环境变量:USE_REDIS=true、REDIS_URL=redis://gongxiangpan-redis:6379
  • 会话 TTL:4 小时(14400 秒)
  • 内存限制:128MB(maxmemory-policy: allkeys-lru)
  • 无需密码

✅ 已验证:生产环境正在使用 Redis 存储上传会话(不是内存 Map 兜底方案)

  • 验证方式:在 Redis 中找到 3 个活跃的上传会话
  • 数据格式:{"fileName": "...", "fileSize": N, "totalChunks": N, "chunks": [...], "createdAt": N, "userId": "..."}

开发环境 Redis:

  • 连接地址:redis://localhost:6379
  • 兜底方案:内存 Map(Redis 不可用时)
  • 启用方式:通过 USE_REDIS=true 环境变量

MinIO/S3 存储配置

MinIO 服务器 (192.168.58.20:9000):

  • 访问密钥:由服务器环境变量 S3_ACCESS_KEY_ID 提供
  • 秘密密钥:由服务器环境变量 S3_SECRET_ACCESS_KEY 提供
  • 存储桶:gongxiangpan
  • 文件路径:由壹点AI/

公网访问域名:

  • URL:https://s3.jiansun.vip/gongxiangpan
  • 展示缓存前缀:/gongxiangpan/public-display/
  • 预览链接过期时间:7200 秒(2 小时)
  • 路径样式:已启用

上传配置

当前设置(截至 2026-01-27):

  • 分片大小:50MB(内网/外网统一)
  • 并发数:2-3 个并发上传
  • 会话 TTL:4 小时(Redis)
  • 超时时间:每个分片 5-10 分钟

容器端口映射

容器 内部端口 外部端口 用途
youyidian-3d-material 3000 4320 生产应用
youyidian-3d-shadow 3000 5320 影子环境应用
gongxiangpan-redis 6379 - 生产 Redis

密码和密钥管理

⚠️ 安全原则:敏感信息存储在服务器上,不提交到 Git。

SSH 密钥配置

本地密钥位置:C:\Users\54972\.ssh

服务器 IP地址 SSH别名 密钥文件 用途
生产服务器 192.168.58.20 直接用 IP(prod 别名已删除) youyidian_prod_ed25519 应用容器、数据库、Redis
构建服务器 192.168.42.134 build id_rsa_build_192.168.42.134 Docker镜像构建
公网入口服务器 218.93.9.38 - root 密码登录(密码不入库) FRP 入口、Nginx 反向代理
License服务器 122.51.149.232 license-server id_ed25519_license_122.51.149.232 License服务
腾讯云服务器 45.249.208.118 tencent tencent_server.pem 腾讯云服务器

已废弃服务器:

  • 旧镇江服务器 180.97.221.140(已封禁,不再使用)
  • 原香港服务器 103.118.245.6(已下线;~/.ssh/config 里的 frp 别名仍指向它,勿用)

未识别的密钥:

  • id_rsa / id_rsa.pub - 默认密钥(保留不动)
  • flnxilicenseserver.pem - 可能是 license 服务器的备用密钥(PEM格式)

连接方式:

ssh 192.168.58.20   # 生产服务器(~/.ssh/config 已配好密钥)
ssh build           # 构建服务器
ssh tencent         # 腾讯云服务器

公网入口服务器 218.93.9.38:root 密码登录,密码向维护人员索取。Windows 下用 plink -batch -hostkey <指纹> -pw <密码> root@218.93.9.38;2026-10-06 首次连接记录的指纹是 SHA256:UwFngDHnyUFNAszlwbtVzBc4NdZMwCCfOz+8yoJvgKs(未经第二渠道核对)。

配置文件:C:\Users\54972\.ssh\config(已配置好 IdentityFile)

查看密码的方式

方式1:通过 SSH 查看环境变量

# 查看生产环境所有环境变量
ssh root@192.168.58.20 "docker exec youyidian-3d-material printenv | grep -E 'DATABASE|S3|REDIS|NEXTAUTH'"

# 查看特定密码
ssh root@192.168.58.20 "docker exec youyidian-3d-material printenv | grep DATABASE_URL"
ssh root@192.168.58.20 "docker exec youyidian-3d-material printenv | grep S3_"

方式2:查看 docker-compose.yml(服务器端)

# 生产服务器上的配置文件路径
ssh root@192.168.58.20 "cat /root/youyidian-3d/docker-compose.yml | grep -A 20 'environment:'"

方式3:查看 .env 文件(如果存在)

ssh root@192.168.58.20 "cat /root/youyidian-3d/.env 2>/dev/null || echo 'No .env file found'"

需要记录的敏感信息位置

信息类型 存储位置 查看命令
数据库密码 服务器环境变量 docker exec youyidian-3d-material printenv | grep DATABASE_URL
MinIO/S3 密钥 服务器环境变量 docker exec youyidian-3d-material printenv | grep S3_
NextAuth 密钥 服务器环境变量 docker exec youyidian-3d-material printenv | grep NEXTAUTH
钉钉 App 密钥 代码中(已加密) cat app/api/dingtalk/login/route.ts | grep DINGTALK
管理员 API Key 服务器环境变量 docker exec youyidian-3d-material printenv | grep ADMIN_API_KEY

密码变更记录

当密码变更时,在此处记录:

  • 2026-01-27:初始记录,建立密码管理机制
  • 2026-05-05:公网接入迁移到香港服务器 103.118.245.6
  • 2026-10-06:核实公网入口已换为 218.93.9.38,原香港服务器 103.118.245.6 已下线

部署

当前默认使用 Git 增量部署,统一通过 scripts/git-incremental-deploy.ps1 执行:

# 预检
.\scripts\git-incremental-deploy.ps1 -DryRun

# 部署到生产环境
.\scripts\git-incremental-deploy.ps1

# 部署到影子环境
.\scripts\git-incremental-deploy.ps1 -Target shadow

当前部署流程:

  1. 源码先提交并推送到 Git 仓库
  2. 构建服务器 (192.168.40.206) 增量拉取指定提交
  3. 构建服务器构建 Docker 镜像并推送到本地镜像仓库 (192.168.40.206:5000)
  4. 生产服务器 (192.168.58.20) 拉取指定提交镜像
  5. 替换正式容器或影子容器
  6. 检查 /api/health,失败时默认尝试回滚旧镜像

⚠️ 部署前置条件(缺一不可):

脚本由维护人员执行,需要以下环境变量,且必须在内网环境(构建机 192.168.40.206 走公网穿透连不上,表现为 TCP 端口"可达"但 SSH 握手失败——穿透代理对任何连接都返回成功,端口连通性测试是假阳性):

$env:BUILD_SERVER_PASSWORD = '构建机密码'      # 脚本用 plink -pw,不走 SSH 密钥
$env:BUILD_SERVER_HOST_KEY = 'SHA256:...'      # 已人工核对的构建机指纹
$env:PROD_SERVER_HOST_KEY  = 'SHA256:...'      # 已人工核对的生产机指纹

已记录的主机指纹(来自 ~/.ssh/known_hosts,生产机已现场验证一致):

  • 生产机 192.168.58.20:SHA256:b0qukGVPuI8j+lykfCIplkcKWh7d4Pq9APzvr4FDnss
  • 构建机 192.168.40.206:SHA256:MXW5w48SFSTUaqS+dS1G9hY228hI6SLepjhCmItHL04

正式部署必须显式指定 40 位提交 SHA,脚本不接受"分支最新提交":

.\scripts\git-incremental-deploy.ps1 -Commit '<40位小写提交SHA>'

deploy-remote-build.ps1 属于本地历史脚本名,仅作为识别应急资料使用,不是仓库交付入口,也不再作为默认部署入口。旧的“本机打包源码 -> 上传到构建机”链路不再作为当前标准口径。

详细部署规范见:

  • docs/Git增量部署方案.md
  • docs/部署规范.md
  • docs/部署脚本安全说明.md

架构

目录结构

app/
├── api/                          # API 路由
│   ├── auth/[...nextauth]/       # NextAuth 配置
│   ├── materials/                # 素材增删改查、上传、下载
│   │   ├── upload/               # 上传端点(分片、完成、特定类型)
│   │   └── [id]/                 # 单个素材操作
│   ├── admin/                    # 管理员端点(用户、权限、统计)
│   ├── external/                 # 外部 API 集成
│   └── user/                     # 用户资料操作
├── [id]/share                    # 公开分享页面
├── admin/                        # 管理员后台页面(素材库管理,旧壳待统一)
├── edit/[id]/                    # 素材编辑页面
├── login/                        # 登录页面
├── materials/                    # 创意资产库(原素材库首页,2026-08-15 迁入)
├── organization/                 # 组织管理(人员架构顶层页,2026-08-15 上移)
└── page.tsx                      # 品牌中心看板首页(登录落点,Zen×Organic 白皮书风)

components/
├── upload/                       # 上传组件
│   ├── UploadModal.tsx           # 主上传弹窗(通用上传)
│   ├── ProductPackageUploader.tsx    # 产品全案
│   ├── TaskPageUploader.tsx          # 任务-页面
│   ├── VideoScreenshotUploader.tsx   # 任务-视频截图
│   ├── ThreeDOriginalUploader.tsx    # 3D原图
│   ├── AIOriginalUploader.tsx        # AI原图
│   ├── RealPhotoUploader.tsx        # 实拍图
│   └── SpecialBatchUploadUI.tsx  # 批量上传界面(音乐、视频、3D、品牌VI等)
├── admin/                       # 管理后台组件
├── board/                       # 看板壳组件(BoardShell、MaterialsHeader——新视觉语言)
├── home/                        # 创意资产库(原首页)组件
└── detail/                      # 详情页组件

lib/
├── chunk-upload.ts              # 分片上传(IndexedDB 状态 + 重试)
├── chunk-upload-config.ts       # 分片上传配置(50MB统一分片)
├── upload-sessions.ts           # 内存上传会话 Map(开发环境)
├── upload-sessions-redis.ts     # Redis 上传会话(生产环境)
├── task-material-processor.ts   # 任务类型素材处理逻辑
├── material-validation.ts       # 素材类型验证辅助函数
├── material-type-mapper.ts      # 素材类型常量和映射器
├── pdf-processor.ts             # PDF 处理(预览图提取)
├── image-processor.ts           # 图片压缩和切片
├── video-processor.ts           # 视频转码
├── audio-processor.ts           # 音频元数据提取
├── psd-processor.ts             # PSD 文件处理
├── s3.ts                        # MinIO S3 客户端
├── auth.ts                      # NextAuth 配置
├── prisma.ts                    # Prisma 客户端
└── network.ts                   # 网络检测和带宽监控

素材类型

平台支持多种素材类型,使用不同的上传流程:

类型 中文名称 上传流程 特殊要求
3D场景 3D Scene 标准流程 渲染器 (CR/V5/FB)
详情模板 Detail Template 展示图 + zip PSD 切片(200KB 分块)
模特 Model 仅模特图片 无需 zip
标准模型 Standard Model 主图 + SKU SKU 管理 + 白底图元数据
品牌VI灵感 Brand VI 仅 PDF 自动从 PDF 第1页提取预览
证书-检测报告 Certificate Report 仅 PDF 支持中文文件名
任务-详情 Task-Detail ProductPackageUploader 主图/SKU/详情图/PSD
任务-页面 Task-Page TaskPageUploader 预览图 → zip
任务-视频截图 Task-Video VideoScreenshotUploader 截图 → zip
淘系-主图视频 Video (Main) 批量上传 原名:主图视频
淘系-逛逛视频 Video (Browse) 批量上传 原名:种草视频
抖音-千川素材 Douyin Qianchuan 批量上传 新增类型
抖音-账号维护 Douyin Account 批量上传 新增类型
音乐/买家秀/小红书 特殊类型 批量上传 特定类型限制
直播切片 Live Clip 批量上传(仅视频,复用视频转码链路) 标题必填;直播日期(默认当天)/主播(可关联用户)/平台(抖音、快手、视频号、淘宝直播、B站);豁免 15 天视频下载保护期(站内与外部接口同时生效,lib/video-download-policy.ts)

证书-检测报告元数据(2026-09-23 起): 报告标题(兼作素材名)、报告类型(成品 / 原材料)、检测日期、到期日期(默认 = 检测日期 + 1 年,lib/certificate-report.ts)、证书编号、使用说明与内置使用规则。迁移 20260923_add_certificate_report_metadata、20260924_add_certificate_tested_on。

视频类型改名说明(2026-04-30):

  • 主图视频 → 淘系-主图视频
  • 种草视频 → 淘系-逛逛视频
  • 权限系统支持新旧名称兼容(lib/video-material-types.ts、lib/department-permission.ts)
  • 数据库迁移:prisma/migrations/20260430_200000_rename_video_material_types/migration.sql

身份认证与授权

用户角色:

  • USER - 普通用户
  • ADMIN - 管理员(旧版,保留兼容性)
  • DEPT_ADMIN - 部门管理员
  • SYSTEM_ADMIN - 系统管理员
  • isSuperAdmin - 超级管理员标记(最多4人)

钉钉集成:

  • OAuth 流程:/api/dingtalk/login
  • 回调地址:/dingtalk-callback
  • 通过 dingtalkUserId 关联用户
  • 首次登录时创建/更新用户资料

会话管理:

  • JWT 策略,30天过期
  • Token 包含:id、role、department、isSuperAdmin、avatar

部门权限:

  • DepartmentMaterialPermission 表控制部门访问权限
  • 字段:canView、canDownload、canShare、canDownloadSource
  • 通过 linkedToDepartment 实现权限继承
  • 视频类型权限支持新旧名称兼容(lib/department-permission.ts)

标准模型 SKU 权限(2026-05-05):

  • SKU 操作权限改为登录即可(不再限制作者/管理员)
  • 白底图上传记录:出图方式(实拍/渲染/AI)、上传人、上传时间
  • 白底图元数据字段:whiteBackgroundSourceType、whiteBackgroundUploadedBy、whiteBackgroundUploadedAt
  • 历史数据回填:已有白底图默认标记为"渲染"

上传系统架构

分片上传流程(lib/chunk-upload.ts):

  1. 初始化 - 生成 uploadId,在 Redis/Map 中创建会话
  2. 分片上传 - 顺序上传分片,支持重试
    • 统一分片大小:50MB(内网/外网统一)
    • IndexedDB 持久化状态,支持断点续传
  3. 完成 - 服务端合并分片并处理

上传会话管理:

  • 开发环境:lib/upload-sessions.ts - 内存 Map
  • 生产环境:lib/upload-sessions-redis.ts - Redis,4小时 TTL
  • 已完成会话:1小时 TTL
  • 关键函数:saveUploadSession、getUploadSession、updateChunk、setCompletedSessionTTL

特殊上传流程:

  1. 标准上传 (/api/materials/upload)

    • 预览图 + 可选 zip
    • 直接上传或分片上传(如果超过阈值)
  2. 任务-详情 (ProductPackageUploader)

    • /api/materials/upload/product-package/init
    • 上传内容:主图、SKU图、详情图、PSD 文件
    • lib/task-material-processor.ts 处理(6个步骤)
  3. 任务-页面 (TaskPageUploader)

    • /api/materials/upload/task-page/init
    • 预览图 → zip 生成
  4. 品牌VI(PDF 上传)

    • lib/pdf-processor.ts 提取第1页作为预览
    • lib/pdf-upload-handler.ts 处理上传
  5. 证书-检测报告(PDF 上传)

    • /api/materials/upload/certificate-report/init
    • /api/materials/upload/certificate-report/complete
    • 支持中文文件名(使用 execFile 调用 pdftoppm)
    • 核心提交成功后,收尾步骤失败不影响整体成功

AI 模型接入与详情页生成器(2026-10 新增,灰度中)

AI 模型管理(仅超管)

  • 管理界面:/admin 超级管理员 → AI 模型接入(components/admin/AiModelManagementPanel.tsx);API 在 app/api/admin/ai-models/;领域逻辑 lib/ai-model-protocol.ts 等。
  • 管四类对象:上游服务商(API Key AES-256-GCM 加密存储)、模型目录(含能力元数据)、按用途的真实测试记录、三阶段配置(资料分析 / 卖点文案 / 生图)+ 操作审计。
  • 核心纪律:只有"真实协议测试"通过的用途才允许开放——资料分析要真读测试图并返回指定位置颜色 JSON,文案要返回合规大纲 JSON,生图要可解码、尺寸有效、非空白;连接 / 密钥 / 能力 / 路由 / 适配版本任一变化即要求重新测试。
  • 测试与详情页生成共享配额:每人 1 并发、全局 2。上游已下架(present=false)的模型允许直接"停用"保存,不再被"需验证"卡住。
  • 表:AiModelProvider / AiModelCatalog / AiModelVerification / AiModelConfiguration / AiModelAudit(迁移 20261012_ai_model_registry、20261013_ai_model_capabilities、20261013_ai_model_verifications)。文档:docs/ai-model-management.md。

详情页生成器(正式验收前仅超管可见)

  • 页面 /assets/detail-generator;API app/api/detail-generator/*;业务逻辑 lib/detail-generator/;表 DetailGeneratorProject / BrandProfile / Asset / Job(迁移 20261012_detail_generator,job 带幂等键、租约、checkpoint)。
  • 两级 worker:进程内队列(instrumentation 每 2 秒 tick,DETAIL_GENERATOR_ENABLED==='true' 才启动,全局并发 2,与模型测试共享);渲染走独立子进程 workers/detail-renderer/cli.mjs——自带依赖锁(Fabric 7 / Canvas 3 / Sharp 0.34,与主应用 Canvas 2 隔离)、768MB 独立堆、非守护服务不监听端口,生产由 DETAIL_RENDERER_NODE / DETAIL_RENDERER_ENTRY / DETAIL_FONT_DIR 指定。
  • 存储必须用独立私有卷 DETAIL_GENERATOR_STORAGE_DIR,不得复用 uploads/公开展示目录;生产 Docker 有独立 stage 与中文渲染 smoke 测试。
  • 显式未验收项:PSD 在 Photoshop 应用内改字保存、品牌风格一致性、多屏完整生成。带 unknown 状态的模型调用需人工到上游核对计费。

回收站永久删除(2026-10 重做为 outbox 任务)

  • 入口 POST /api/admin/materials/batch-delete(permanent:true,单次上限 100):请求路径只落 MaterialPurgeJob(文件清单快照 JSON)+ MaterialPurgeAudit 并删素材记录,不做物理删除;物理删除由 instrumentation 启动的 worker 执行(租约 10 分钟、最多 3 次尝试、每批 8 个文件、per-file outcome 写回快照,重启可续跑,可经 POST /api/admin/materials/purge-jobs 重试)。核心逻辑 lib/material-purge.ts,进度 UI components/admin/MaterialPurgeStatus.tsx。
  • 引用检查依赖 9 张表上的 GIN 表达式索引(规范化路径 md5,迁移 20261009_material_purge_reference_indexes;lib/material-purge-reference-query.ts 用 MATERIALIZED / OFFSET 0 钉住计划);S3 派生文件用 derived: 键参与判断,共享派生文件不会被误删。
  • ⚠️ material_logs / material_download_logs 的 materialId 外键已为 purge 移除,不要加回去。备份恢复演练脚本:scripts/verify-purge-backup-restore.mjs。

文件处理流程

图片处理(lib/image-processor.ts):

  • 预览压缩:最大 200KB,质量 85
  • 详情切片:200KB 分块,带重叠
  • 支持水印
  • 使用 Sharp 库处理

视频处理(lib/video-processor.ts):

  • canonical 源视频(生产机 UPLOAD_DIR / uploads/NAS)→ 默认 720P H.265(HEVC)主预览
  • 生成 10 秒 H.265 预览片段
  • 提取封面
  • 基于 FFmpeg 转码;同一应用实例最多 1 条任务、FFmpeg 默认 1 线程
  • 任务由 PostgreSQL 租约持久化,启动时恢复并每 60 秒重扫;Redis 只用于上传会话等业务,不承担视频队列
  • 上传建档后异步经历 pending → processing → done/failed

音频处理(lib/audio-processor.ts):

  • 元数据提取(艺术家、专辑、时长)
  • 支持格式:MP3、FLAC、M4A、AAC、OGG、WAV

PSD 处理(lib/psd-processor.ts):

  • 图层提取
  • 使用 ag-psd 生成预览

API 集成

外部 API(lib/external-api.ts):

  • /api/external/download - 通过 Token 验证下载
  • /api/external/product-assets - 任务系统批量资产下载
  • /api/external/dingtalk-material-match - 钉钉素材匹配接口
  • /api/external/dingtalk-material-options - 钉钉下拉选项接口
  • /api/external/standard-model-sku-white-background - 标准模型 SKU 白底图元数据查询

钉钉接口集成(2026-05-05):

  • 支持两种鉴权方式:URL 参数 apiKey 或请求头 X-API-Key
  • 钉钉低代码流程推荐使用 URL 参数方式
  • 程序系统对接推荐使用请求头方式
  • 字段映射:需求类型→素材类型、品牌→店铺、场景类别、产品类别、SPU
  • 文档:docs/钉钉素材库接口对接文档.md

API Key 系统:

  • ApiKey 表用于外部系统身份认证
  • ApiCallLog 跟踪所有 API 调用
  • 通过 x-api-key 请求头或 ADMIN_API_KEY 环境变量验证
  • 钉钉接口额外支持 URL 参数 apiKey(lib/external-api.ts 中 allowQuery: true)

性能优化

分片配置(lib/chunk-upload-config.ts):

  • 统一使用 50MB 分片(内网/外网一致)
  • 动态超时时间:根据分片大小自动计算(300KB/s 最低速度)
  • 并发控制:2-3 个并发(内网3、外网2)
  • 请求数减少:1GB 文件从 100 个请求减少到 20 个(80% 减少)

Redis 优化:

  • 会话 TTL:4小时(活跃中)、1小时(已完成)
  • 连接池,带重试策略
  • 离线队列,增强连接弹性

内存监控(lib/memory-monitor.ts):

  • 跟踪上传期间的堆使用
  • 流式合并,避免 Buffer.concat() 溢出

重要文件位置

文件 用途
prisma/schema.prisma 数据库结构(所有模型)
lib/material-validation.ts 类型特定验证规则
lib/material-type-mapper.ts 素材类型常量和映射器
lib/video-material-types.ts 视频类型定义和新旧兼容
lib/department-permission.ts 部门权限判断(支持视频类型兼容)
lib/task-material-processor.ts 任务类型处理流程(6个步骤)
lib/chunk-upload.ts 核心分片上传逻辑
lib/pdf-processor.ts PDF 处理(支持中文文件名)
lib/external-api.ts 外部 API 鉴权(支持 URL 参数)
lib/standard-model-white-background-metadata.ts 标准模型白底图元数据
middleware.ts 请求拦截和认证检查
app/api/materials/upload/route.ts 旧版上传端点
app/api/materials/upload/chunk/route.ts 分片上传处理
app/api/materials/upload/certificate-report/complete/route.ts 证书报告上传完成
components/HeaderTaobaoSearch.tsx 标题栏淘宝 ID 搜索框
components/SkuEditModal.tsx SKU 编辑弹窗(白底图上传)
components/SkuList.tsx SKU 列表(白底图元数据显示)
docs/钉钉素材库接口对接文档.md 钉钉接口对接文档

环境变量

.env 中必需的配置:

DATABASE_URL=postgresql://...
NEXTAUTH_SECRET=...
NEXTAUTH_URL=http://localhost:3000

# S3/MinIO
S3_ENDPOINT=...
S3_BUCKET=...
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...

# Redis(生产环境)
REDIS_URL=redis://localhost:6379

# 管理员
ADMIN_API_KEY=...

# 钉钉(可选)
DINGTALK_APP_KEY=...
DINGTALK_APP_SECRET=...

# AI 详情页生成器(默认关;开启需独立私有持久卷,不得用 uploads)
DETAIL_GENERATOR_ENABLED=false
DETAIL_GENERATOR_BASE_URL=...
DETAIL_GENERATOR_API_KEY=...
DETAIL_GENERATOR_STORAGE_DIR=...

# 详情渲染子进程(生产镜像已固定 /opt/detail-renderer/...,一般无需覆盖)
DETAIL_RENDERER_NODE=
DETAIL_RENDERER_ENTRY=
DETAIL_FONT_DIR=

# AI 上游密钥加密(≥32 字符;与 API Key 加密密钥独立生成、稳定保存)
API_KEY_ENCRYPTION_KEY=...

常用模式

API 路由认证:

import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'

const session = await getServerSession(authOptions)
if (!session?.user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

Prisma 事务:

await prisma.$transaction(async (tx) => {
  const material = await tx.material.create({ ... })
  await tx.materialAsset.createMany({ ... })
  return material
})

错误处理:

import { parseApiError } from '@/lib/error-handler'
try { ... }
catch (error) {
  const { message } = parseApiError(error)
  return NextResponse.json({ error: message }, { status: 500 })
}

已知问题和注意事项

  1. 内存溢出 - 在 upload-sessions.ts 中使用流式合并,不要用 Buffer.concat()
  2. 竞态条件 - 处理过程中避免重复更新 previewUrl
  3. 软删除 - 素材有 deletedAt 字段 - 始终过滤掉这些记录
  4. 任务号格式 - 格式:TASK + 12-18位数字 + 可选的 -XX 后缀
  5. 预览生成 - 上传先完成建档并进入 pending,预览由后台异步生成;三类产物完整且租约仍有效才是 done,失败写入 failed
  6. Docker 网络 - 容器必须在 youyidian-3d_app-network 上才能访问 Redis
  7. 视频类型权限 - 视频类型改名后需同步更新部门权限表,或依赖代码兼容逻辑
  8. PDF 中文文件名 - 必须使用 execFile 而非 exec 调用 pdftoppm,避免路径注入
  9. 部署版本管理 - 不同对话中的改动需合并到同一工作区再部署,避免"回退"现象
  10. 构建缓存 - 默认保留构建缓存加快构建,只在必要时使用 -AggressiveBuildCacheCleanup
  11. Alpine 镜像源 - 使用清华源,避免官方源在国内网络环境下的连接问题
  12. 音频/文件访问路由 - .dockerignore 和部署脚本中排除 uploads 时要精确,避免误伤 app/api/uploads 路由
  13. 人员身份判定 - 只认钉钉唯一值。钉钉登录遇到同名账号会拒绝并转人工,不再自动合并——这是有意的摩擦,防止重名变成越权
  14. 在职状态 - 不要用关键词匹配 employmentStatus,用 employmentState。上游有数字码、中文、空值三种口径并存
  15. 素材署名与归属分离 - 署名和归属对不上通常是正确的(素材转过手,署名保留原作者),不要"修正"成一致
  16. Material.user 是级联删除 - 一旦有代码真的删除 User,会连带硬删其全部素材。目前全项目无硬删除调用,靠"没人碰"撑着而非数据库拦着;引入离职下线流程前必须先改掉
  17. 上游连接密码含特殊字符 - .env 中含 # 的值必须加引号,否则被当注释截断

最近修复的问题(2026-04-30 至 2026-05-05)

  1. 证书-检测报告中文 PDF 上传失败 - 修复 lib/pdf-processor.ts 路径校验,支持中文文件名
  2. DNZ032912 视频素材"看不见" - 视频类型改名后权限表未同步,已补齐权限并增加代码兼容
  3. 标准模型 SKU 白底图缺少时间显示 - 新增元数据字段并回填历史数据(2730 条)
  4. 钉钉接口需要手动输入请求头 - 增加 URL 参数 apiKey 支持,适配钉钉低代码流程
  5. 音频素材播放 404 - 修复 .dockerignore 和部署脚本,避免误排除 app/api/uploads 路由
  6. 部署后小改动重装基础包 - 优化构建缓存策略,默认保留缓存
  7. Alpine 镜像源连接失败 - 从官方源改为清华源

测试状态

  • Jest 已配置,测试覆盖关键业务逻辑
  • 重要测试文件:
    • lib/__tests__/video-material-types.test.ts - 视频类型兼容测试
    • lib/__tests__/external-api-key.test.ts - 外部 API 鉴权测试
    • lib/__tests__/dingtalk-material-match-route.test.ts - 钉钉匹配接口测试
    • lib/__tests__/dingtalk-material-options-route.test.ts - 钉钉选项接口测试
  • 推荐通过影子环境部署进行手动测试
  • 使用 /api/admin/system-status 进行健康检查

数据库维护记录

2026-04-30:视频类型改名

  • 执行迁移:prisma/migrations/20260430_200000_rename_video_material_types/migration.sql
  • 手动补齐部门权限:复制 6 条"主图视频"权限到"淘系-主图视频",复制 6 条"种草视频"权限到"淘系-逛逛视频"

2026-05-05:标准模型白底图元数据

  • 执行迁移:prisma/migrations/20260430_add_standard_model_white_background_metadata/migration.sql
  • 历史数据回填:2730 条 SKU 的 white_background_uploaded_at 字段(使用资产最后创建时间)
  • 历史数据默认值:white_background_source_type 统一回填为"渲染"

2026-08-13:人员架构统一到钉钉唯一值

执行了三个迁移(均已在生产库 lobechat 应用):

  • 20260813_add_user_employee_binding:绑定表 + users.dingtalkUserId 唯一约束
  • 20260813_add_material_custody_department:素材托管部门
  • 20260813_add_organization_units_and_employment_state:组织架构表 + 规范化在职状态

数据操作结果:

  • 建立 503 条账号↔人员绑定,覆盖活跃账号的 96.3%(余下 18 个无钉钉 ID,需人工确认)
  • 同步 1075 个部门,单根树 5 层,零孤儿节点
  • 校正在职状态:1285 在职 / 219 离职 / 214 未知(此前 216 条已离职档案中有 215 条被误判为在职)
  • 离职联动:归档 139 个账号(离职满 30 天者),39 人暂缓
  • 素材部门托管:3775 条,孤儿素材清零
  • 回填 1807 条被误存为邮箱的作者名(全部来自任务-详情入口的历史缺陷)
  • 账号治理:处置 1 个"组织上查无此人"的 ADMIN 账号(素材移交后撤权归档)

👤 开发者偏好

⚠️ 沟通偏好

语言:

  • 全中文环境:回复、注释、文档、变量命名、思考过程、Git message
  • 表达方式:多用自然语言,少用代码堆砌
  • 用户背景:非程序员,侧重日常语言解释

回复风格:

  • 少代码多文字
  • 必要时用少量代码示例
  • 优先自然语言说明

🧠 思维方式

方案分析:

  • 对比不同方案的优劣
  • 用数字和星级量化评价
  • 结构化输出,用 emoji 标记章节

问题解决:

  • 根因分析:不满足表面修复,深入排查底层原因
  • 验证导向:修改后必须验证效果

架构思维:

  • 优先考虑稳定性和可扩展性
  • 模块化设计,构建稳定基础
  • 统一 UI 组件和页面风格
  • 抽取通用工具函数和样式
  • 减少重复代码,降低维护成本

💼 工作风格

代码原则:

  • 不写不重要的文档
  • 保持代码简洁,避免过度工程化
  • 优先使用现有组件和方案

垃圾文件清理(开发时即时清理,不留尾巴):

  • 更新脚本/文档时删除旧版本
  • 测试完成后删除临时文件
  • 一次性脚本用完即删
  • 用 Git 管理版本,不创建备份文件
  • 清理无用的会话数据、缓存、临时文件

执行偏好:

  • 能自动执行的不让用户手动操作
  • 能批量处理的不让用户逐个处理

⚡ 性能意识

内存:

  • 避免内存泄漏(事件监听器、定时器、闭包)
  • 优化缓存策略(TTL、上限控制)
  • 流式处理大文件,避免全量加载

🔒 数据安全

  • 绝不删除重要数据目录
  • 修改前备份数据

📋 工作习惯

Git:

  • 只有明确要求时才创建 commit
  • Commit message 使用中文,简洁明了

文件操作:

  • 修改前先读取文件内容
  • 创建新文件前检查是否已存在

🚨 重要提醒

工作区状态:

  • 工作区长期存在大量未提交改动,这是正常状态
  • 不同对话中的改动需要合并到同一工作区再部署
  • 部署脚本只打包当前磁盘工作区,不会自动合并其它对话的改动
  • 高敏感文件(如 components/HeaderTaobaoSearch.tsx)曾多次被"回退",根因是未合并工作区

部署前检查清单:

  1. 确认所有相关改动都在当前工作区
  2. 检查 .dockerignore 和部署脚本的排除规则
  3. 验证 Alpine 镜像源配置(清华源)
  4. 确认构建缓存策略(默认保留)
  5. 检查 Docker 网络配置(youyidian-3d_app-network)

数据库操作原则:

  • 生产数据库名称是 lobechat,不是 gongxiangpan
  • 涉及权限、素材类型、用户数据的变更需谨慎
  • 大批量数据回填前先在影子环境验证
  • 保持迁移文件和手动操作的记录

外部接口注意事项:

  • 钉钉接口使用 URL 参数 apiKey,不要做成用户填写字段
  • API Key 不要在日志中明文输出
  • 外部接口调用需记录到 ApiCallLog 表