929 lines
45 KiB
Markdown
929 lines
45 KiB
Markdown
# 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 样式
|
||
|
||
## 开发命令
|
||
|
||
```bash
|
||
# 开发环境
|
||
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`)。
|
||
|
||
### 运维脚本
|
||
|
||
```bash
|
||
# 人员架构盘点(纯只读,可重复执行)
|
||
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` 参数:**
|
||
```bash
|
||
docker run -d --name youyidian-3d-material \
|
||
--network youyidian-3d_app-network \ # ⚠️ 必须指定网络!
|
||
-p 4320:3000 \
|
||
...
|
||
```
|
||
|
||
**诊断 Redis 连接问题:**
|
||
```bash
|
||
# 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格式)
|
||
|
||
**连接方式**:
|
||
```bash
|
||
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 查看环境变量**
|
||
```bash
|
||
# 查看生产环境所有环境变量
|
||
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(服务器端)**
|
||
```bash
|
||
# 生产服务器上的配置文件路径
|
||
ssh root@192.168.58.20 "cat /root/youyidian-3d/docker-compose.yml | grep -A 20 'environment:'"
|
||
```
|
||
|
||
**方式3:查看 .env 文件(如果存在)**
|
||
```bash
|
||
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` 执行:
|
||
|
||
```powershell
|
||
# 预检
|
||
.\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 握手失败——穿透代理对任何连接都返回成功,端口连通性测试是假阳性):
|
||
|
||
```powershell
|
||
$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,脚本不接受"分支最新提交":
|
||
|
||
```powershell
|
||
.\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 路由认证:**
|
||
```typescript
|
||
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 事务:**
|
||
```typescript
|
||
await prisma.$transaction(async (tx) => {
|
||
const material = await tx.material.create({ ... })
|
||
await tx.materialAsset.createMany({ ... })
|
||
return material
|
||
})
|
||
```
|
||
|
||
**错误处理:**
|
||
```typescript
|
||
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` 表
|