17 KiB
生产系统问题分析报告
日期: 2025-12-20
分析人: AI Assistant
系统: 由壹点3D素材平台
📋 问题描述
用户报告生产系统(http://192.168.58.20:4320)存在数据显示问题:
- 筛选"3D场景"类型时,瀑布流显示 1026个素材
- "使用店铺"筛选的"全部"按钮显示 1530个素材
- 差异:504个素材 (1530 - 1026 = 504)
初步分析: 用户认为缺失了504个素材
调查结果:
- ✅ 1026个是正确的(370个"3D场景" + 656个"3DMAX")
- ❌ 1530个包含了6个软删除的素材(应该显示1524)
- ✅ 504个差异是其他素材类型(PSD、视频、图片等)
真正的问题: 统计API包含了软删除数据,导致"全部"按钮显示错误数字
🔍 调查过程
1. 数据库检查
生产数据库 (lobechat)
-- 总记录数(包含软删除)
SELECT COUNT(*) FROM materials;
-- 结果: 1530
-- 未删除的素材
SELECT COUNT(*) FROM materials WHERE "deletedAt" IS NULL;
-- 结果: 1524
-- 已软删除的素材
SELECT COUNT(*) FROM materials WHERE "deletedAt" IS NOT NULL;
-- 结果: 6
结论: 数据库数据完整,没有被覆盖
影子数据库 (lobechat_stage)
SELECT COUNT(*) FROM materials WHERE "deletedAt" IS NULL;
-- 结果: 1049
结论: 影子系统数据独立,没有混乱
2. API 检查
API 1: /api/materials/stats
curl http://192.168.58.20:4320/api/materials/stats
返回结果:
{
"total": 1530,
"months": [
{"value": "2025-12", "label": "2025年12月", "count": 638},
{"value": "2025-11", "label": "2025年11月", "count": 818},
{"value": "2025-10", "label": "2025年10月", "count": 74}
]
}
问题: total: 1530 包含了6条软删除数据!
API 2: /api/materials?page=1&pageSize=20
curl http://192.168.58.20:4320/api/materials?page=1&pageSize=20
返回结果:
{
"totalCount": 1524,
"totalPages": 77,
"currentPage": 1,
"materials": [... 1524条数据 ...]
}
严重问题:
totalCount正确 (1524)- 但
materials数组包含 全部1524条数据,而不是分页的20条! - 无论请求第几页,都返回全部数据
🐛 发现的BUG
BUG 1: 统计API包含软删除数据 ⚠️
文件: app/api/materials/stats/route.ts
问题代码:
// 错误:没有过滤软删除数据
const total = await prisma.material.count()
正确代码:
// 正确:过滤软删除数据
const total = await prisma.material.count({
where: { deletedAt: null }
})
影响:
- "使用店铺"的"全部"按钮显示 1530(错误)
- 应该显示 1524(正确)
BUG 2: 分页API返回全部数据 🔥 严重
文件: app/api/materials/route.ts
问题: 分页逻辑失效,每次请求都返回全部1524条数据
影响:
-
前端性能严重下降
- 一次性加载1524条数据
- 每条数据包含图片URL、用户信息等
- 数据量约 1-2MB
-
用户体验问题
- 页面加载缓慢
- 滚动卡顿
- 用户报告只能看到1026个素材(可能是前端渲染限制或浏览器内存限制)
-
服务器资源浪费
- 每次请求都查询全部数据
- 数据库负载增加
需要检查的代码:
// 检查 skip 和 take 是否正确使用
const materials = await prisma.material.findMany({
where,
skip: (page - 1) * pageSize, // 是否正确计算?
take: pageSize, // 是否正确应用?
})
BUG 3: 分类统计未过滤软删除 ⚠️
文件: app/api/categories/route.ts
问题代码:
// 错误:统计时没有过滤软删除数据
const count = await prisma.material.count({
where: {
categoryName: category.name,
},
})
正确代码:
// 正确:过滤软删除数据
const count = await prisma.material.count({
where: {
categoryName: category.name,
deletedAt: null,
},
})
影响:
- 分类按钮显示的数字可能包含已删除素材
- 用户点击后看到的实际素材数量与按钮显示不符
📁 素材存储路径问题
背景
系统经历过存储位置迁移,存在新老两套存储路径:
老素材存储(2025-10-27之前)
-
预览图: MinIO S3(老bucket配置)
- Endpoint:
https://s3.1080p.hk - Bucket:
jsxiangche(老bucket,无路径前缀) - URL格式:
https://s3.1080p.hk/jsxiangche/preview-{timestamp}-{hash}.webp - 示例:
https://s3.1080p.hk/jsxiangche/preview-1761537819211-edb4a190180c24f9.webp
- Endpoint:
-
源文件(压缩包): 本地文件系统
- 路径:
uploads/material-{timestamp}-{hash}.zip - 相对路径格式(数据库存储)
- 示例:
uploads/material-1761537819728-8fb5090670b5b986.zip
- 路径:
-
特征识别:
- previewUrl 不包含
由壹点AI/路径前缀 - previewUrl 使用 bucket
jsxiangche(不是gongxiangpan) - zipFilePath 格式:
uploads/material-*.zip - 创建时间: 2025-10-27 及之前
- previewUrl 不包含
新素材存储(2025-10-27之后)
-
预览图/视频缩略图: MinIO S3(新路径配置)
- Endpoint:
https://s3.1080p.hk - Bucket:
jsxiangche(同一bucket,但有路径前缀) - 路径前缀:
由壹点AI/ - URL格式:
https://s3.1080p.hk/jsxiangche/由壹点AI/video-thumb-{timestamp}-{hash}.jpg - 示例:
https://s3.1080p.hk/jsxiangche/由壹点AI/video-thumb-1766220802404-d6c20cf8f82c0c80.jpg
- Endpoint:
-
源文件(压缩包/视频): 本地文件系统
- 路径:
uploads/video-{timestamp}-{hash}.mp4或uploads/material-{timestamp}-{hash}.zip - 相对路径格式(数据库存储)
- 示例:
uploads/video-1766220805356-02770716caf02d9c.mp4
- 路径:
-
特征识别:
- previewUrl 包含
由壹点AI/路径前缀 - zipFilePath 格式:
uploads/video-*.mp4或uploads/material-*.zip - 创建时间: 2025-10-27 之后
- previewUrl 包含
存储路径对比
| 项目 | 老素材 | 新素材 |
|---|---|---|
| S3 Endpoint | https://s3.1080p.hk |
https://s3.1080p.hk |
| S3 Bucket | jsxiangche |
jsxiangche |
| S3 路径前缀 | 无 | 由壹点AI/ |
| 预览图URL | https://s3.1080p.hk/jsxiangche/preview-*.webp |
https://s3.1080p.hk/jsxiangche/由壹点AI/video-thumb-*.jpg |
| 本地文件路径 | uploads/material-*.zip |
uploads/video-*.mp4 或 uploads/material-*.zip |
| 本地存储目录 | ./uploads (相对路径) |
./uploads (相对路径) |
| Docker挂载 | 容器内 /app/uploads |
容器内 /app/uploads |
| 时间分界点 | ≤ 2025-10-27 | > 2025-10-27 |
环境变量配置
当前配置 (.env.local):
# MinIO S3 Storage
S3_ENDPOINT=https://s3.1080p.hk
S3_BUCKET=jsxiangche
S3_ACCESS_KEY_ID=AEvT1p0wSNjow3Asu9NV
S3_SECRET_ACCESS_KEY=u8nzFl1S6dNoT5PQaQhkHz8Du6iX0hVnPswtvHhs
S3_REGION=us-east-1
S3_PUBLIC_DOMAIN=https://s3.1080p.hk/jsxiangche
S3_ENABLE_PATH_STYLE=1
S3_FILE_PATH=由壹点AI/
# Local storage for zip files
UPLOAD_DIR=./uploads
注意:
- 老素材和新素材使用同一个 S3 bucket (
jsxiangche) - 区别在于新素材有路径前缀
由壹点AI/ - 本地文件都存储在
./uploads目录
⚠️ 重要原则
-
不要移动或修改老素材
- 老素材保持在原位置
- 不要尝试迁移老素材到新位置
- 确保老素材路径可访问
-
新素材统一使用新位置
- 所有新上传的素材使用 MinIO S3 + 本地uploads
- 遵循新的命名和组织规则
-
下载功能必须兼容新老路径
- 根据数据库中的路径信息判断使用哪个存储
- 自动适配不同的路径格式
- 确保新老素材都能正常下载
数据库字段
// materials 表相关字段
interface Material {
id: string;
previewUrl: string; // 预览图URL(可能是S3或老存储)
zipFileName: string; // 压缩包文件名
zipFilePath: string; // 本地压缩包路径(可能是老路径或新路径)
zipFileSize: bigint; // 文件大小
// ... 其他字段
}
路径判断逻辑(已从本地代码确认)
下载功能 (app/api/materials/download/route.ts):
- 当前代码使用相对路径
zipFilePath - 自动转换为绝对路径:
path.join(process.cwd(), zipFilePath) - 老素材和新素材都存储在
./uploads目录 - 无需特殊判断逻辑,因为都使用相同的本地存储目录
代码片段:
// 普通素材:下载压缩包文件
let filePath = material.zipFilePath
if (!filePath) {
return NextResponse.json(
{ error: '素材没有可下载的文件' },
{ status: 404 }
)
}
// 确保使用绝对路径
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath)
}
// 检查文件是否存在
await access(filePath)
const fileBuffer = await readFile(filePath)
上传功能 (app/api/materials/upload/route.ts):
- 新素材使用环境变量
UPLOAD_DIR(默认./uploads) - 文件命名:
material-{timestamp}-{hash}.zip或video-{timestamp}-{hash}.mp4 - 预览图上传到 S3,路径前缀:
由壹点AI/
代码片段:
const uploadDir = process.env.UPLOAD_DIR || './uploads'
await mkdir(uploadDir, { recursive: true })
const zipFileExt = path.extname(zipFile.name)
zipFileName = generateUniqueFileName('material', zipFileExt)
zipFilePath = path.join(uploadDir, zipFileName)
// 保存到数据库时使用相对路径
// zipFilePath = "uploads/material-1766220805356-02770716caf02d9c.zip"
✅ 生产系统验证结果(2025-12-20)
1. S3 预览图验证
老素材预览图:
- URL:
https://s3.1080p.hk/jsxiangche/preview-1761537819211-edb4a190180c24f9.webp - 状态: ✅ 可访问
- 大小: 63,076 bytes
- 类型: image/webp
新素材预览图:
- URL:
https://s3.1080p.hk/jsxiangche/由壹点AI/video-thumb-1766220802404-d6c20cf8f82c0c80.jpg - 状态: ✅ 可访问
- 类型: image/jpeg
2. 本地文件验证
老素材文件 (2025-10-27):
# 数据库记录
zipFilePath: uploads/material-1761537819728-8fb5090670b5b986.zip
# 生产容器实际文件
/app/uploads/material-1761537819728-8fb5090670b5b986.zip
大小: 807.0M
状态: ✅ 存在
新素材文件 (2025-12-19):
# 数据库记录
zipFilePath: uploads/video-1766119244101-6a457afe724a412d.mp4
# 生产容器实际文件
/app/uploads/video-1766119244101-6a457afe724a412d.mp4
大小: 50.2M
状态: ✅ 存在
3. 存储目录统计
# 生产容器 /app/uploads 目录
总大小: 716GB
文件类型:
- material-*.zip (老素材压缩包)
- video-*.mp4 (新素材视频文件)
兼容性说明
✅ 当前代码已兼容新老素材:
- 本地文件都存储在
./uploads目录 - 数据库存储相对路径
uploads/xxx.zip - 下载时自动转换为绝对路径
- 老素材和新素材使用相同的路径处理逻辑
✅ S3 预览图兼容:
- 老素材:
https://s3.1080p.hk/jsxiangche/preview-*.webp - 新素材:
https://s3.1080p.hk/jsxiangche/由壹点AI/video-thumb-*.jpg - 前端直接使用
previewUrl字段,无需特殊处理
✅ 生产系统验证通过:
- 老素材文件存在且可访问(807MB zip文件)
- 新素材文件存在且可访问(50MB mp4文件)
- S3 预览图都可以正常访问
- 数据库路径与实际文件路径完全匹配
⚠️ 注意事项:
- 不要修改
uploads目录的文件(总大小716GB) - 不要修改数据库中的
zipFilePath字段 - 确保 Docker 挂载正确: 容器内
/app/uploads→ 宿主机uploads目录
🎯 修复优先级
🔥 P0 - 紧急(影响用户体验)
- 修复分页BUG -
app/api/materials/route.ts- 问题:返回全部数据导致性能问题
- 影响:所有用户的浏览体验
- 预计修复时间:30分钟
⚠️ P1 - 高优先级(数据显示错误)
-
修复统计API -
app/api/materials/stats/route.ts- 问题:包含软删除数据
- 影响:"全部"按钮显示错误数字
- 预计修复时间:15分钟
-
修复分类统计 -
app/api/categories/route.ts- 问题:分类统计包含软删除数据
- 影响:分类按钮数字不准确
- 预计修复时间:15分钟
📋 P2 - 中优先级(功能验证)
-
验证下载功能 -
app/api/materials/download/route.ts- 确认:支持新老路径
- 测试:老素材和新素材都能正常下载
- 预计时间:30分钟
-
验证上传功能 - 素材上传相关代码
- 确认:新素材使用新路径
- 测试:上传后能正常预览和下载
- 预计时间:30分钟
🔧 修复方案
❌ 方案 A: 从生产容器恢复源码(不可行)
调查结果:
- 生产容器使用 Next.js standalone 模式部署
- 容器内只包含编译后的代码:
.next/- 编译后的JavaScript代码node_modules/- 依赖包server.js- 服务器入口package.json- 依赖配置public/- 静态资源uploads/- 用户上传文件(716GB)
- 没有TypeScript源码文件(.ts/.tsx)
结论: 无法从生产容器恢复源码
✅ 方案 B: 修复本地代码的BUG(推荐)
前提条件:
- 本地代码虽然版本较旧(1.0.0 vs 2.2.0),但核心功能应该是完整的
- 已经识别出3个明确的BUG需要修复
- 可以通过修复这3个BUG来解决当前的数据显示问题
优点:
- 快速解决当前问题
- 不依赖生产容器的源码
- 可以在影子系统充分测试
- 修复后可以逐步完善其他功能
步骤:
- 修复本地代码的3个BUG:
- BUG 1:
app/api/materials/stats/route.ts- 统计API过滤软删除 - BUG 2:
app/api/materials/route.ts- 修复分页逻辑 - BUG 3:
app/api/categories/route.ts- 分类统计过滤软删除
- BUG 1:
- 在本地构建并测试
- 部署到影子系统(端口5320)
- 执行完整功能测试
- 验证通过后部署到生产系统(端口4320)
预计时间: 1-2小时
方案 C: 从编译代码反推(不推荐)❌
缺点:
- 编译后的代码已经被混淆和优化
- 反推工作量巨大且容易出错
- 无法保证准确性
不推荐原因: 成本高、风险大、不可靠
📊 数据统计
素材类型分布(生产数据库)
| 素材类型 | 数量 | 说明 |
|---|---|---|
| 3DMAX | 656 | 3D场景文件 |
| 3D场景 | 370 | 3D场景文件 |
| 其他 | 101 | 其他类型 |
| 音乐 | 93 | 音乐文件 |
| PSD | 71 | PSD设计文件 |
| 种草视频 | 57 | 营销视频 |
| 买家秀图片 | 50 | 用户图片 |
| 主图视频 | 41 | 产品视频 |
| 安装视频 | 29 | 教程视频 |
| 实拍图片 | 19 | 实拍照片 |
| 详情页 | 17 | 详情页设计 |
| 材质 | 11 | 材质文件 |
| 模特 | 7 | 模特图片 |
| 标准模型 | 1 | 标准模型 |
| 小红书种草 | 1 | 小红书内容 |
| 总计 | 1524 | 未删除素材 |
说明:
- 用户筛选"3D场景"时看到1026个 = 370(3D场景)+ 656(3DMAX)
- 这是正常的筛选结果,不是数据缺失
- 其他504个素材是不同类型(PSD、视频、图片等)
素材店铺分布(生产数据库)
| 店铺 | 素材数量 |
|---|---|
| 顾家 | 388 |
| 亿家达 | 302 |
| 朴美 | 224 |
| 卓禾 | 179 |
| 欧意朗 | 169 |
| 通用 | 159 |
| 索菲亚 | 99 |
| (空) | 4 |
| 总计 | 1524 |
最近上传(2025-12-20)
- 08:53 - 通用店铺种草视频 (2条)
- 08:22 - 通用店铺种草视频 (3条)
- 08:03 - 欧意朗 XG0232
- 07:50 - 顾家 GBGJ-AD0312书房
- 07:46 - 顾家 GBGJ-AD0312客厅
- ... (更多)
观察: 系统正常运行,用户持续上传素材
⚠️ 注意事项
1. 不要直接修改生产系统
- ❌ 不要在生产容器中直接修改代码
- ❌ 不要在生产数据库中直接修改数据
- ✅ 只从生产系统读取和复制文件
- ✅ 所有修改先在本地测试
- ✅ 测试通过后部署到影子系统
- ✅ 影子系统验证通过后才部署到生产
2. 保护用户数据
- ❌ 不要删除或覆盖
uploads目录 - ❌ 不要修改老素材的存储路径
- ✅ 数据库操作前必须备份
- ✅ 保持老素材在原位置可访问
3. 环境隔离
-
生产系统 (端口4320):
- 数据库:
lobechat - Redis: 6379
- 容器:
youyidian-3d-material
- 数据库:
-
影子系统 (端口5320):
- 数据库:
lobechat_stage - Redis: 6380
- 容器:
youyidian-3d-material-shadow
- 数据库:
-
✅ 确保环境变量正确配置
-
✅ 部署脚本默认部署到生产,使用
-Target shadow部署到影子
4. 测试验证
- ✅ 在影子系统完整测试所有功能
- ✅ 特别测试新老素材的下载功能
- ✅ 验证分页功能正常
- ✅ 验证统计数字正确
- ✅ 记录测试结果
📝 后续行动
立即执行
- 从生产容器提取源码
- 对比本地差异,生成差异报告
- 恢复缺失和不一致的文件
测试验证
- 在本地构建并测试
- 部署到影子系统
- 执行完整功能测试
- 特别测试新老素材下载
生产部署
- 备份生产数据库
- 部署到生产系统
- 验证生产系统功能
- 监控系统运行状态
文档更新
- 更新功能恢复进度文档
- 记录修复的BUG
- 更新部署文档
- 建立Git版本控制
📞 联系信息
如有问题,请联系:
- 系统管理员
- 开发团队
报告结束