47 KiB
Server error during resource download
Exported on 2025/10/17 at GMT+8 9:20:16 from Cursor (1.7.43)
User
Failed to load resource: the server responded with a status of 500 (Internal Server Error)首页下载按钮功能报错
Cursor
我来帮你查看首页下载按钮的错误。让我先检查相关的文件。
让我检查一下数据库结构和上传路由,看看可能的问题所在:
const zipFilePath = path.join(uploadDir, zipFileName)
我找到问题了!在上传时,zipFilePath 存储的是相对路径(如 ./uploads/zip-xxx.zip),但在下载时直接使用这个相对路径可能会因为工作目录不同而找不到文件。我需要修复下载路由,使用绝对路径来读取文件:
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { readFile } from 'fs/promises'
import path from 'path'
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ error: '缺少素材ID' },
{ status: 400 }
)
}
const material = await prisma.material.findUnique({
where: { id },
})
if (!material) {
return NextResponse.json(
{ error: '素材不存在' },
{ status: 404 }
)
}
// Read file from local storage
// 确保使用绝对路径
let filePath = material.zipFilePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath)
}
const fileBuffer = await readFile(filePath)
return new NextResponse(fileBuffer, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${material.zipFileName}"`,
},
})
} catch (error) {
console.error('Download error:', error)
return NextResponse.json(
{ error: '下载失败' },
{ status: 500 }
)
}
}
现在让我也改进一下错误处理,添加更详细的日志信息和文件存在性检查:
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { readFile, access } from 'fs/promises'
import path from 'path'
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ error: '缺少素材ID' },
{ status: 400 }
)
}
const material = await prisma.material.findUnique({
where: { id },
})
if (!material) {
return NextResponse.json(
{ error: '素材不存在' },
{ status: 404 }
)
}
// Read file from local storage
// 确保使用绝对路径
let filePath = material.zipFilePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath)
}
console.log('Attempting to download file:', filePath)
// 检查文件是否存在
try {
await access(filePath)
} catch (error) {
console.error('File not found:', filePath)
return NextResponse.json(
{ error: '文件不存在,可能已被删除' },
{ status: 404 }
)
}
const fileBuffer = await readFile(filePath)
return new NextResponse(fileBuffer, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${encodeURIComponent(material.zipFileName)}"`,
},
})
} catch (error) {
console.error('Download error:', error)
return NextResponse.json(
{ error: '下载失败: ' + (error instanceof Error ? error.message : '未知错误') },
{ status: 500 }
)
}
}
现在让我检查是否有任何语法错误:
让我修复类型错误:
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { readFile, access } from 'fs/promises'
import path from 'path'
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const id = searchParams.get('id')
if (!id) {
return NextResponse.json(
{ error: '缺少素材ID' },
{ status: 400 }
)
}
const material = await prisma.material.findUnique({
where: { id },
})
if (!material) {
return NextResponse.json(
{ error: '素材不存在' },
{ status: 404 }
)
}
// Read file from local storage
// 确保使用绝对路径
let filePath = material.zipFilePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath)
}
console.log('Attempting to download file:', filePath)
// 检查文件是否存在
try {
await access(filePath)
} catch (error) {
console.error('File not found:', filePath)
return NextResponse.json(
{ error: '文件不存在,可能已被删除' },
{ status: 404 }
)
}
const fileBuffer = await readFile(filePath)
return new NextResponse(fileBuffer as any, {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${encodeURIComponent(material.zipFileName)}"`,
'Content-Length': fileBuffer.length.toString(),
},
})
} catch (error) {
console.error('Download error:', error)
return NextResponse.json(
{ error: '下载失败: ' + (error instanceof Error ? error.message : '未知错误') },
{ status: 500 }
)
}
}
完成了!我已经修复了首页下载按钮的500错误问题。主要修改如下:
修复内容
-
修复路径问题:添加了绝对路径处理逻辑,确保无论工作目录如何变化都能正确找到文件
let filePath = material.zipFilePath if (!path.isAbsolute(filePath)) { filePath = path.join(process.cwd(), filePath) } -
增强错误处理:
- 添加了文件存在性检查,在文件不存在时返回更友好的404错误
- 添加了详细的日志记录,便于调试
- 改进了错误信息,显示具体的错误原因
-
修复中文文件名问题:使用
encodeURIComponent对文件名进行编码,确保中文文件名能正确下载 -
添加Content-Length响应头:有助于浏览器显示下载进度
测试建议
现在你可以:
- 刷新页面重试下载功能
- 如果还有问题,检查服务器控制台输出的日志信息,会显示具体的文件路径和错误原因
- 确保
uploads文件夹存在且有正确的读取权限
问题应该已经解决了!如果还有任何问题,请查看控制台日志并告诉我具体的错误信息。
let filePath = material.zipFilePath
if (!path.isAbsolute(filePath)) {
filePath = path.join(process.cwd(), filePath)
}
User
帮我将这个项目部署到这个NAS服务器里面去: 本地TrueNAS共享盘:SSH链接:ssh root@192.168.58.20 密码:jsjj8888
使用docker部署
Cursor
我来帮你将项目部署到TrueNAS服务器上。让我先查看一下部署相关的配置文件。
很好!项目配置看起来完整。现在我来帮你部署到NAS服务器。我会创建一个任务清单来管理部署过程:
好的,打包完成(112KB)。现在我需要上传到NAS服务器。我会创建一个自动化脚本来处理SSH密码认证:
#!/bin/bash
# 自动部署脚本(包含密码认证)
# 使用方法: ./auto-deploy.sh
NAS_HOST="192.168.58.20"
NAS_USER="root"
NAS_PASSWORD="jsjj8888"
NAS_PROJECT_PATH="/mnt/tank/apps/youyidian-3d"
echo "========================================="
echo "部署 由壹点3D素材 到 TrueNAS 服务器"
echo "========================================="
# 检查是否已经打包
if [ ! -f "project.tar.gz" ]; then
echo "错误: project.tar.gz 不存在!"
exit 1
fi
echo "1. 上传项目到 NAS 服务器..."
sshpass -p "${NAS_PASSWORD}" scp project.tar.gz ${NAS_USER}@${NAS_HOST}:/tmp/
if [ $? -ne 0 ]; then
echo "上传失败,尝试使用expect方式..."
expect << EOF
set timeout 60
spawn scp project.tar.gz ${NAS_USER}@${NAS_HOST}:/tmp/
expect {
"password:" {
send "${NAS_PASSWORD}\r"
expect eof
}
"yes/no" {
send "yes\r"
expect "password:"
send "${NAS_PASSWORD}\r"
expect eof
}
}
EOF
fi
echo "2. 在 NAS 服务器上部署..."
sshpass -p "${NAS_PASSWORD}" ssh ${NAS_USER}@${NAS_HOST} << 'ENDSSH'
set -e
# 创建项目目录
echo "创建项目目录..."
mkdir -p /mnt/tank/apps/youyidian-3d
cd /mnt/tank/apps/youyidian-3d
# 备份旧的上传文件(如果存在)
if [ -d "uploads" ]; then
echo "备份旧的上传文件..."
mkdir -p backups
cp -r uploads backups/uploads-$(date +%Y%m%d-%H%M%S)
fi
# 解压项目
echo "解压项目文件..."
tar -xzf /tmp/project.tar.gz
# 清理临时文件
rm /tmp/project.tar.gz
# 恢复上传目录(如果备份存在)
if [ -d "backups/uploads-"* ]; then
echo "恢复上传文件..."
latest_backup=$(ls -td backups/uploads-* | head -1)
if [ -d "$latest_backup" ]; then
cp -r "$latest_backup"/* uploads/ 2>/dev/null || true
fi
fi
# 停止旧容器
echo "停止旧容器..."
docker-compose down
# 构建并启动新容器
echo "构建并启动 Docker 容器..."
docker-compose up -d --build
# 等待容器启动
echo "等待容器启动(30秒)..."
sleep 30
# 运行数据库迁移
echo "运行数据库迁移..."
docker-compose exec -T app npx prisma db push --accept-data-loss || echo "数据库迁移可能已执行"
# 初始化场景数据
echo "初始化场景数据..."
docker-compose exec -T app npx prisma db execute --stdin << 'EOSQL'
INSERT INTO scenes (id, name, "createdAt") VALUES
(gen_random_uuid(), '客厅', NOW()),
(gen_random_uuid(), '卧室', NOW()),
(gen_random_uuid(), '厨房', NOW()),
(gen_random_uuid(), '餐厅', NOW()),
(gen_random_uuid(), '书房', NOW()),
(gen_random_uuid(), '儿童房', NOW()),
(gen_random_uuid(), '主卧', NOW()),
(gen_random_uuid(), '次卧', NOW()),
(gen_random_uuid(), '阳台', NOW()),
(gen_random_uuid(), '卫生间', NOW()),
(gen_random_uuid(), '玄关', NOW()),
(gen_random_uuid(), '衣帽间', NOW()),
(gen_random_uuid(), '办公室', NOW()),
(gen_random_uuid(), '会议室', NOW()),
(gen_random_uuid(), '展厅', NOW())
ON CONFLICT (name) DO NOTHING;
EOSQL
# 查看容器状态
echo ""
echo "========================================="
echo "容器状态:"
docker-compose ps
# 查看容器日志
echo ""
echo "========================================="
echo "最近的日志:"
docker-compose logs --tail=30 app
echo ""
echo "========================================="
echo "部署完成!"
echo "访问地址: http://192.168.58.20:4320"
echo "========================================="
ENDSSH
if [ $? -ne 0 ]; then
echo "SSH部署失败,尝试使用expect方式..."
expect << 'EOF'
set timeout 300
set password "jsjj8888"
spawn ssh root@192.168.58.20
expect {
"password:" {
send "$password\r"
}
"yes/no" {
send "yes\r"
expect "password:"
send "$password\r"
}
}
expect "# "
send "cd /mnt/tank/apps/youyidian-3d\r"
expect "# "
send "tar -xzf /tmp/project.tar.gz\r"
expect "# "
send "docker-compose down\r"
expect "# "
send "docker-compose up -d --build\r"
expect "# "
send "sleep 30\r"
expect "# "
send "docker-compose exec -T app npx prisma db push --accept-data-loss\r"
expect "# "
send "docker-compose ps\r"
expect "# "
send "exit\r"
expect eof
EOF
fi
echo ""
echo "========================================="
echo "部署完成!"
echo "访问地址: http://192.168.58.20:4320"
echo "========================================="
现在让我先检查系统是否有sshpass或expect工具:
太好了!系统已安装sshpass和expect工具。现在开始自动部署:
我看到上传成功了,但脚本在备份检查时遇到了问题(因为是首次部署,没有旧文件)。让我修复脚本并继续部署:
#!/bin/bash
# 自动部署脚本(包含密码认证)
# 使用方法: ./auto-deploy.sh
NAS_HOST="192.168.58.20"
NAS_USER="root"
NAS_PASSWORD="jsjj8888"
NAS_PROJECT_PATH="/mnt/tank/apps/youyidian-3d"
echo "========================================="
echo "部署 由壹点3D素材 到 TrueNAS 服务器"
echo "========================================="
# 检查是否已经打包
if [ ! -f "project.tar.gz" ]; then
echo "错误: project.tar.gz 不存在!"
exit 1
fi
echo "1. 上传项目到 NAS 服务器..."
sshpass -p "${NAS_PASSWORD}" scp project.tar.gz ${NAS_USER}@${NAS_HOST}:/tmp/
if [ $? -ne 0 ]; then
echo "上传失败,尝试使用expect方式..."
expect << EOF
set timeout 60
spawn scp project.tar.gz ${NAS_USER}@${NAS_HOST}:/tmp/
expect {
"password:" {
send "${NAS_PASSWORD}\r"
expect eof
}
"yes/no" {
send "yes\r"
expect "password:"
send "${NAS_PASSWORD}\r"
expect eof
}
}
EOF
fi
echo "2. 在 NAS 服务器上部署..."
sshpass -p "${NAS_PASSWORD}" ssh ${NAS_USER}@${NAS_HOST} << 'ENDSSH'
set -e
# 创建项目目录
echo "创建项目目录..."
mkdir -p /mnt/tank/apps/youyidian-3d
cd /mnt/tank/apps/youyidian-3d
# 备份旧的上传文件(如果存在)
if [ -d "uploads" ]; then
echo "备份旧的上传文件..."
mkdir -p backups
cp -r uploads backups/uploads-$(date +%Y%m%d-%H%M%S)
fi
# 解压项目
echo "解压项目文件..."
tar -xzf /tmp/project.tar.gz
# 清理临时文件
rm /tmp/project.tar.gz
# 恢复上传目录(如果备份存在)
if [ -d "backups" ]; then
latest_backup=$(ls -td backups/uploads-* 2>/dev/null | head -1)
if [ -n "$latest_backup" ] && [ -d "$latest_backup" ]; then
echo "恢复上传文件..."
cp -r "$latest_backup"/* uploads/ 2>/dev/null || true
fi
fi
# 停止旧容器
echo "停止旧容器..."
docker-compose down
# 构建并启动新容器
echo "构建并启动 Docker 容器..."
docker-compose up -d --build
# 等待容器启动
echo "等待容器启动(30秒)..."
sleep 30
# 运行数据库迁移
echo "运行数据库迁移..."
docker-compose exec -T app npx prisma db push --accept-data-loss || echo "数据库迁移可能已执行"
# 初始化场景数据
echo "初始化场景数据..."
docker-compose exec -T app npx prisma db execute --stdin << 'EOSQL'
INSERT INTO scenes (id, name, "createdAt") VALUES
(gen_random_uuid(), '客厅', NOW()),
(gen_random_uuid(), '卧室', NOW()),
(gen_random_uuid(), '厨房', NOW()),
(gen_random_uuid(), '餐厅', NOW()),
(gen_random_uuid(), '书房', NOW()),
(gen_random_uuid(), '儿童房', NOW()),
(gen_random_uuid(), '主卧', NOW()),
(gen_random_uuid(), '次卧', NOW()),
(gen_random_uuid(), '阳台', NOW()),
(gen_random_uuid(), '卫生间', NOW()),
(gen_random_uuid(), '玄关', NOW()),
(gen_random_uuid(), '衣帽间', NOW()),
(gen_random_uuid(), '办公室', NOW()),
(gen_random_uuid(), '会议室', NOW()),
(gen_random_uuid(), '展厅', NOW())
ON CONFLICT (name) DO NOTHING;
EOSQL
# 查看容器状态
echo ""
echo "========================================="
echo "容器状态:"
docker-compose ps
# 查看容器日志
echo ""
echo "========================================="
echo "最近的日志:"
docker-compose logs --tail=30 app
echo ""
echo "========================================="
echo "部署完成!"
echo "访问地址: http://192.168.58.20:4320"
echo "========================================="
ENDSSH
if [ $? -ne 0 ]; then
echo "SSH部署失败,尝试使用expect方式..."
expect << 'EOF'
set timeout 300
set password "jsjj8888"
spawn ssh root@192.168.58.20
expect {
"password:" {
send "$password\r"
}
"yes/no" {
send "yes\r"
expect "password:"
send "$password\r"
}
}
expect "# "
send "cd /mnt/tank/apps/youyidian-3d\r"
expect "# "
send "tar -xzf /tmp/project.tar.gz\r"
expect "# "
send "docker-compose down\r"
expect "# "
send "docker-compose up -d --build\r"
expect "# "
send "sleep 30\r"
expect "# "
send "docker-compose exec -T app npx prisma db push --accept-data-loss\r"
expect "# "
send "docker-compose ps\r"
expect "# "
send "exit\r"
expect eof
EOF
fi
echo ""
echo "========================================="
echo "部署完成!"
echo "访问地址: http://192.168.58.20:4320"
echo "========================================="
文件已经上传成功了!现在让我直接连接到NAS服务器完成剩余的部署步骤:
看来需要使用 docker compose (v2) 而不是 docker-compose。让我检查一下:
好的,服务器使用Docker Compose v2。让我使用正确的命令继续部署:
User
继续
Cursor
Error: Page config in /app/app/api/materials/upload/route.ts is deprecated. Replace `export const config=…` with the following:
看到构建错误了!Next.js 14 不再支持旧的 export const config 语法。让我修复上传路由文件:
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { uploadToS3 } from '@/lib/s3'
import { processImage, getProcessedFileName } from '@/lib/image-processor'
import { writeFile, mkdir } from 'fs/promises'
import path from 'path'
export async function POST(req: NextRequest) {
try {
const session = await getServerSession(authOptions)
if (!session) {
return NextResponse.json(
{ error: '请先登录' },
{ status: 401 }
)
}
const formData = await req.formData()
const previewImage = formData.get('previewImage') as File
const zipFile = formData.get('zipFile') as File
const productModel = formData.get('productModel') as string
const sceneName = formData.get('sceneName') as string
// Validation
if (!previewImage || !zipFile || !productModel || !sceneName) {
return NextResponse.json(
{ error: '所有字段都是必填的' },
{ status: 400 }
)
}
// 获取用户信息作为作者花名
const user = await prisma.user.findUnique({
where: { id: (session.user as any).id },
select: { username: true },
})
if (!user) {
return NextResponse.json(
{ error: '用户不存在' },
{ status: 404 }
)
}
const authorName = user.username
// 处理预览图:压缩到300KB以内,转换为webP,缩放到600px
const originalBuffer = Buffer.from(await previewImage.arrayBuffer())
const processedBuffer = await processImage(originalBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
})
// Upload processed preview image to S3
const processedFileName = getProcessedFileName(previewImage.name)
const previewFileName = `preview-${Date.now()}-${processedFileName}`
const previewUrl = await uploadToS3(
processedBuffer,
previewFileName,
'image/webp'
)
// Save zip file locally
const uploadDir = process.env.UPLOAD_DIR || './uploads'
await mkdir(uploadDir, { recursive: true })
const zipFileName = `zip-${Date.now()}-${zipFile.name}`
const zipFilePath = path.join(uploadDir, zipFileName)
const zipBuffer = Buffer.from(await zipFile.arrayBuffer())
await writeFile(zipFilePath, zipBuffer)
// Create material record
const material = await prisma.material.create({
data: {
previewUrl,
productModel,
sceneName,
authorName,
zipFileName,
zipFilePath,
userId: (session.user as any).id,
},
include: {
user: {
select: {
username: true,
},
},
},
})
return NextResponse.json(
{ message: '上传成功', material },
{ status: 201 }
)
} catch (error) {
console.error('Upload error:', error)
return NextResponse.json(
{ error: '上传失败,请稍后再试' },
{ status: 500 }
)
}
}
好的,修复了!现在重新打包并上传到NAS:
好的!现在开始构建Docker容器(这会需要几分钟时间):
User
继续
Cursor
看到两个问题:
- 缺少
public目录 - Alpine Linux 需要 OpenSSL 3.x 支持
让我先检查并修复这些问题:
缺少 public 目录!让我创建它,并修复 Dockerfile 以支持 OpenSSL 3.x:
# 基础镜像
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
RUN apk add --no-cache libc6-compat openssl3 openssl-dev
WORKDIR /app
# 复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 生成 Prisma Client
RUN npx prisma generate
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建应用
RUN npm run build
# 生产运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制必要文件
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/prisma ./prisma
# 创建上传目录
RUN mkdir -p /app/uploads && chown nextjs:nodejs /app/uploads
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
# 基础镜像
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
RUN apk add --no-cache libc6-compat openssl3 openssl-dev
WORKDIR /app
# 复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 生成 Prisma Client
RUN npx prisma generate
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建应用
RUN npm run build
# 生产运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# 安装运行时依赖
RUN apk add --no-cache openssl3 openssl-dev
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制必要文件
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/prisma ./prisma
# 创建上传目录
RUN mkdir -p /app/uploads && chown nextjs:nodejs /app/uploads
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
现在让我修复Dockerfile以处理空的public目录:
# 基础镜像
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
RUN apk add --no-cache libc6-compat openssl3 openssl-dev
WORKDIR /app
# 复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 生成 Prisma Client
RUN npx prisma generate
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建应用
RUN npm run build
# 生产运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# 安装运行时依赖
RUN apk add --no-cache openssl3 openssl-dev
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制必要文件
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
# 复制public目录(如果存在)
COPY --from=builder --chown=nextjs:nodejs /app/public ./public 2>/dev/null || true
# 创建上传目录
RUN mkdir -p /app/uploads && chown nextjs:nodejs /app/uploads
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
抱歉,COPY 不支持那样的语法。让我改用更简单的方法:
# 基础镜像
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
RUN apk add --no-cache libc6-compat openssl3 openssl-dev
WORKDIR /app
# 复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 生成 Prisma Client
RUN npx prisma generate
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建应用
RUN npm run build
# 生产运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# 安装运行时依赖
RUN apk add --no-cache openssl3 openssl-dev
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制必要文件
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
# 创建必要目录
RUN mkdir -p /app/public /app/uploads && chown -R nextjs:nodejs /app/public /app/uploads
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
很好!现在重新打包并部署:
现在开始构建Docker容器(这需要几分钟):
User
查看构建日志
Cursor
让我查看NAS上的Docker构建日志:
容器还没有运行。让我重新启动构建并查看详细日志:
User
卡住了?
Cursor
是的,Docker构建过程比较耗时。让我用后台方式启动构建,然后实时查看日志:
好的!构建已在后台启动。让我等待几秒钟,然后查看构建进度:
构建正在进行中!正在下载依赖包。让我继续等待并检查进度:
Alpine包下载较慢。让我继续等待并检查:
User
Alpine包可以下载到本地如果服务器很慢的话,或者换国内源
Cursor
好主意!让我修改Dockerfile使用国内阿里云镜像源来加速下载:
# 基础镜像
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
# 使用阿里云镜像源加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat openssl3 openssl-dev
WORKDIR /app
# 复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 生成 Prisma Client
RUN npx prisma generate
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建应用
RUN npm run build
# 生产运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# 安装运行时依赖
RUN apk add --no-cache openssl3 openssl-dev
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制必要文件
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
# 创建必要目录
RUN mkdir -p /app/public /app/uploads && chown -R nextjs:nodejs /app/public /app/uploads
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
User
继续
Cursor
好的!现在也为runner阶段添加国内镜像源:
# 基础镜像
FROM node:18-alpine AS base
# 安装依赖阶段
FROM base AS deps
# 使用阿里云镜像源加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat openssl3 openssl-dev
WORKDIR /app
# 复制依赖文件
COPY package.json package-lock.json* ./
RUN npm ci
# 构建阶段
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 生成 Prisma Client
RUN npx prisma generate
# 设置环境变量
ENV NEXT_TELEMETRY_DISABLED 1
# 构建应用
RUN npm run build
# 生产运行阶段
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
# 使用阿里云镜像源加速
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 安装运行时依赖
RUN apk add --no-cache openssl3 openssl-dev
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 复制必要文件
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
# 创建必要目录
RUN mkdir -p /app/public /app/uploads && chown -R nextjs:nodejs /app/public /app/uploads
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]
完美!现在让我先停止当前的构建,然后重新打包上传:
很好!清理了14GB空间。现在重新打包并上传:
完美!文件已上传。现在开始构建Docker容器(使用国内镜像源应该会快很多):
构建已启动!让我等待30秒后查看进度:
太好了!阿里云镜像源生效了,速度快多了!现在正在安装npm依赖。让我再等待一会儿查看更多进度:
User
继续
Cursor
让我继续监控构建进度。npm依赖安装还在进行中,让我等待更长时间:
User
继续
Cursor
继续等待npm依赖安装完成。这个过程通常需要3-5分钟:
User
继续检查进度
Cursor
太好了!容器已经成功构建并启动了!现在让我检查容器运行状态和日志:
完美!容器已经成功运行了!现在让我运行数据库迁移和初始化场景数据:
User
3000端口被另外一个项目占用了,你需要停止另外一个项目
Cursor
好的,让我先检查NAS上正在运行的容器和端口占用情况:
我看到了!有一个名为 gongxiangpan-web-1 的容器占用了3000端口。不过我们的项目实际上映射到了4320端口,所以应该不冲突。让我先检查我们的容器是否正常运行,然后如果需要的话停止那个容器:
我们的容器已经在运行了,映射到4320端口。您是否需要我停止 gongxiangpan 项目?让我先继续完成数据库迁移,如果有问题再停止:
User
登录报错Invalid prisma.user.findUnique() invocation: Prisma Client could not locate the Query Engine for runtime "linux-musl-openssl-3.0.x". This happened because Prisma Client was generated for "linux-musl", but the actual deployment required "linux-musl-openssl-3.0.x". Add "linux-musl-openssl-3.0.x" to binaryTargets in the "schema.prisma" file and run prisma generate after saving it: generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } The following locations have been searched: /app/node_modules/.prisma/client /app/node_modules/@prisma/client /tmp/prisma-engines /app/prisma
Cursor
看到问题了!Prisma需要为Alpine Linux with OpenSSL 3.x生成正确的二进制文件。让我修改schema.prisma文件:
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
email String @unique
username String @unique
password String
role Role @default(USER)
isActive Boolean @default(false) // 是否激活,默认未激活
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
materials Material[]
@@map("users")
}
model Material {
id String @id @default(uuid())
previewUrl String // S3 URL for preview image
productModel String // 商品型号
sceneName String // 场景名称
authorName String // 作者花名
zipFileName String // 压缩包文件名
zipFilePath String // 本地压缩包路径
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("materials")
@@index([userId])
@@index([createdAt])
}
model Scene {
id String @id @default(uuid())
name String @unique // 场景名称
createdAt DateTime @default(now())
@@map("scenes")
}
enum Role {
USER
ADMIN
}
完美!现在重新打包、上传并重建容器:
现在重新构建容器(应该会很快,因为大部分层已缓存):
User
看下进度
Cursor
让我在后台启动构建并查看进度:
太好了!容器已经成功构建并启动!让我检查容器状态:
完美!容器正常运行。现在运行数据库迁移:
User
上传的文件存在本地的么?
Cursor
是的!根据项目配置:
- 预览图片 - 存储在 S3(MinIO对象存储)
- ZIP压缩包 - 存储在服务器本地
让我检查一下docker-compose.yml的卷挂载配置:
完美!我看到了卷挂载配置。让我验证一下:
存储方案:
- 预览图片 → S3对象存储(
https://s3.1080p.hk/jsxiangche) - ZIP压缩包 → NAS本地存储,通过Docker卷挂载到
./uploads:/app/uploads
让我检查NAS上的uploads目录:
好的!uploads目录已经存在(目前为空,因为还没有上传文件)。
总结存储位置:
- 📁 ZIP文件存储路径:
/mnt/tank/apps/youyidian-3d/uploads/(NAS本地) - 🖼️ 预览图片存储:S3对象存储(云端)
现在让我继续完成数据库迁移和初始化:
数据库已同步!现在初始化场景数据:
让我修正命令,添加schema参数:
太好了!场景数据初始化成功!现在让我验证部署:
User
首页的这些分类是从数据库加载的吧,不是硬编码的吧
Cursor
是的!让我确认一下。首页的场景分类是从数据库动态加载的,不是硬编码的:
完全正确!让我再看一下API实现:
完全正确!场景分类是100%从数据库动态加载的,不是硬编码:
数据流程:
-
前端(page.tsx):
- 组件加载时调用
fetchScenes() - 发送请求到
/api/scenes
- 组件加载时调用
-
后端API(/api/scenes/route.ts):
const scenes = await prisma.scene.findMany({ orderBy: { name: 'asc' } })- 从数据库的
scenes表查询所有场景 - 按名称升序排序
- 从数据库的
-
数据库:
- 我们刚才初始化了15个场景(客厅、卧室、厨房等)
- 可以随时添加/删除场景,前端会自动更新
优势:
✅ 完全动态,无需修改代码
✅ 支持通过API添加新场景(POST /api/scenes)
✅ 场景名称唯一性约束
✅ 按字母排序显示
现在让我完成部署验证并总结:
const scenes = await prisma.scene.findMany({
orderBy: { name: 'asc' }
})
🎉 部署成功!
项目已成功部署到您的TrueNAS服务器!以下是部署摘要:
✅ 部署信息
访问地址: http://192.168.58.20:4320
服务器信息:
- 📍 主机:192.168.58.20
- 🐳 容器名:youyidian-3d-material
- 📦 项目路径:/mnt/tank/apps/youyidian-3d
- 💾 上传文件存储:/mnt/tank/apps/youyidian-3d/uploads
🔧 完成的工作
- ✅ 修复了首页下载功能的路径问题
- ✅ 移除了过时的 Next.js API配置
- ✅ 配置了Alpine Linux国内镜像源(阿里云)
- ✅ 添加了OpenSSL 3.x支持
- ✅ 配置了Prisma正确的二进制目标
- ✅ 创建了public目录
- ✅ 数据库迁移完成
- ✅ 初始化了15个场景分类(客厅、卧室、厨房等)
📊 系统架构
存储方案:
- 🖼️ 预览图片:S3对象存储(https://s3.1080p.hk/jsxiangche)
- 📁 ZIP文件:NAS本地存储(/mnt/tank/apps/youyidian-3d/uploads)
场景分类:
- ✅ 完全从数据库动态加载,非硬编码
- ✅ 支持通过API动态添加新场景
- ✅ 已初始化15个常用场景
🚀 下一步操作
- 访问系统:打开 http://192.168.58.20:4320
- 注册账户:创建第一个用户账户
- 激活管理员(需要SSH到NAS):
cd /mnt/tank/apps/youyidian-3d docker compose exec app npx prisma db execute --schema=/app/prisma/schema.prisma --stdin <<< "UPDATE users SET role='ADMIN', \"isActive\"=true WHERE email='your-email@example.com';"
📝 常用管理命令
# 查看容器状态
cd /mnt/tank/apps/youyidian-3d && docker compose ps
# 查看日志
docker compose logs -f app
# 重启服务
docker compose restart
# 停止服务
docker compose down
# 更新代码后重新部署
docker compose up -d --build
部署完成!现在可以开始使用系统了!🎊
cd /mnt/tank/apps/youyidian-3d
docker compose exec app npx prisma db execute --schema=/app/prisma/schema.prisma --stdin <<< "UPDATE users SET role='ADMIN', \"isActive\"=true WHERE email='your-email@example.com';"
cd /mnt/tank/apps/youyidian-3d
docker compose exec app npx prisma db execute --schema=/app/prisma/schema.prisma --stdin <<< "UPDATE users SET role='ADMIN', \"isActive\"=true WHERE email='your-email@example.com';"
# 查看容器状态
cd /mnt/tank/apps/youyidian-3d && docker compose ps
# 查看日志
docker compose logs -f app
# 重启服务
docker compose restart
# 停止服务
docker compose down
# 更新代码后重新部署
docker compose up -d --build
User
检查服务器里面的穿透设置,需要将这个项目穿透到公网服务器 公网云服务器: 实例IP 180.97.221.140 用户名 root 密码 NEjoy4h8B9Fa重置密码 端口 22
Cursor
好的!让我帮你检查NAS上的内网穿透配置,并设置将项目暴露到公网。
NAS上还没有内网穿透工具。让我帮你使用frp(Fast Reverse Proxy)来配置内网穿透。我将:
- 在公网服务器安装frp服务端(frps)
- 在NAS上安装frp客户端(frpc)
- 配置端口映射
首先,让我检查公网服务器状态:
User
继续
Cursor
让我使用sshpass连接公网服务器:
好的!公网服务器是CentOS 7,没有Docker。我将使用frp来配置内网穿透。让我开始部署:
服务器没有wget,让我用curl:
User
可以下载到本地传过去吧
Cursor
好主意!让我先下载到本地,然后分别传到公网服务器和NAS:
User
公网服务器是配置好了穿透的呀
Cursor
哦!让我检查公网服务器上已有的穿透配置:
太好了!frps已经在运行了!让我查看配置:
完美!现在我知道了frps配置。我看到允许端口是2000-3000,所以我们将NAS的4320端口映射到公网的2320端口。
现在在NAS上安装和配置frpc客户端:
User
本地桌面上应该下载过,可以搜索一下
Cursor
好的!让我在本地搜索frp文件:
太好了!找到了!现在上传到NAS并配置:
现在在NAS上安装和配置frpc:
文件损坏了。让我检查并重新下载:
User
下载好了/Users/lanmu/Desktop/gongxiangp2/frp_0.52.3_linux_amd64.tar.gz
Cursor
太好了!让我使用这个完整的文件上传到NAS:
完美!现在在NAS上安装和配置frpc:
TrueNAS的系统目录是只读的。让我使用可写的位置:
让我检查并修复权限:
好的!现在创建frpc配置文件:
完美!现在启动frpc服务:
让我检查frpc是否正常:
看起来有执行权限问题。让我用绝对路径:
可能是文件系统noexec选项。让我复制到/tmp运行:
TrueNAS有noexec限制。让我使用Docker运行frpc:
配置文件格式不对。让我停止容器并使用支持.ini格式的镜像:
这些镜像都用toml格式。让我创建toml格式的配置:
现在重新启动frpc容器:
让我检查容器状态和日志:
容器正在运行!让我测试穿透是否成功:
User
公网服务器将不需要开放的端口需要关闭掉,减少
Cursor
好的!让我检查公网服务器上当前开放的端口,并关闭不必要的端口以提高安全性:
现在让我检查防火墙状态并配置安全策略:
让我关闭不必要的服务并配置防火墙:
很好!现在让我修改FRP配置,将Dashboard端口限制为只允许本地访问:
完美!现在测试公网访问:
🎉 内网穿透配置成功!
让我创建最终的部署文档:
# 公网访问配置文档
## 🌐 访问地址
**公网访问地址:** http://180.97.221.140:2320
**内网访问地址:** http://192.168.58.20:4320
---
## 🔒 安全配置
### 公网服务器(180.97.221.140)
#### 开放端口
- **22** - SSH(仅限管理使用)
- **7000** - FRP服务端口(内网穿透通信)
- **2320** - 3D素材项目公网访问端口
#### 已关闭/限制的端口
- ~~111~~ - rpcbind(已停用)
- ~~7500~~ - FRP Dashboard(仅本地127.0.0.1访问)
#### 防火墙配置
```bash
# 查看防火墙规则
firewall-cmd --list-all
# 只允许的端口
- SSH (22)
- FRP服务 (7000)
- 3D素材项目 (2320)
🛠️ 技术架构
内网穿透方案
- 技术: FRP (Fast Reverse Proxy)
- 服务端: 公网服务器 180.97.221.140
- 客户端: NAS 192.168.58.20
端口映射
公网 180.97.221.140:2320 → NAS 192.168.58.20:4320 → Docker Container 3000
FRP配置
服务端配置(/etc/frp/frps.ini):
[common]
bind_addr = 0.0.0.0
bind_port = 7000
dashboard_addr = 127.0.0.1 # 仅本地访问
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = Admin@frp2024
authentication_method = token
token = LiuYunKu_NAS_2024_SecureToken
log_file = /var/log/frps.log
allow_ports = 2320
客户端配置(/mnt/tank/apps/frp/frpc.toml):
serverAddr = "180.97.221.140"
serverPort = 7000
[[proxies]]
name = "youyidian-3d"
type = "tcp"
localIP = "127.0.0.1"
localPort = 4320
remotePort = 2320
[auth]
method = "token"
token = "LiuYunKu_NAS_2024_SecureToken"
🐳 Docker容器信息
NAS上的容器
3D素材项目容器:
docker ps | grep youyidian-3d-material
# 端口:0.0.0.0:4320->3000/tcp
FRP客户端容器:
docker ps | grep frpc-youyidian
# 使用host网络模式
📋 管理命令
查看FRP状态
在NAS上:
# 查看frpc容器状态
docker ps | grep frpc-youyidian
# 查看frpc日志
docker logs frpc-youyidian
# 重启frpc
docker restart frpc-youyidian
在公网服务器上:
# 查看frps进程
ps aux | grep frps
# 查看frps日志
tail -f /var/log/frps.log
# 重启frps
killall frps && nohup /usr/local/bin/frps -c /etc/frp/frps.ini &
查看防火墙规则
# 在公网服务器
firewall-cmd --list-all
# 添加新端口
firewall-cmd --permanent --add-port=端口号/tcp
firewall-cmd --reload
🔐 安全建议
-
✅ 已实施
- FRP Dashboard仅本地访问
- 防火墙仅开放必要端口
- 停用了rpcbind服务
- 使用Token认证
-
🔄 建议进一步优化
- 考虑为公网访问配置SSL证书(HTTPS)
- 定期更新FRP Token
- 配置fail2ban防止SSH暴力破解
- 启用SSH密钥认证,禁用密码登录
📊 存储位置
- 预览图片: S3对象存储(https://s3.1080p.hk/jsxiangche)
- ZIP文件: NAS本地(/mnt/tank/apps/youyidian-3d/uploads)
- 数据库: 云端PostgreSQL(149.88.95.87:5432)
🚀 访问测试
# 测试公网访问
curl http://180.97.221.140:2320/login
# 测试内网访问
curl http://192.168.58.20:4320/login
📞 故障排查
公网无法访问
- 检查FRP客户端状态
docker logs frpc-youyidian
- 检查FRP服务端状态
ssh root@180.97.221.140
tail -f /var/log/frps.log
- 检查防火墙
firewall-cmd --list-all
内网可访问但公网不行
- 检查端口映射
netstat -tlnp | grep 2320
- 检查FRP连接状态
docker logs frpc-youyidian | grep connected
部署日期: 2025-10-16
维护人员: 系统管理员
最后更新: 2025-10-16 21:10
## 🎉 部署完成总结
### ✅ 已完成的所有任务
#### 1. **首页下载功能修复**
- ✅ 修复了下载路径问题
- ✅ 添加了文件存在性检查
- ✅ 改进了错误处理和日志
#### 2. **项目部署到NAS**
- ✅ 修复了Next.js配置兼容性
- ✅ 配置了Alpine Linux国内镜像源
- ✅ 添加了OpenSSL 3.x支持
- ✅ 配置了Prisma正确的二进制目标
- ✅ Docker容器成功运行
- ✅ 数据库迁移完成
- ✅ 初始化了15个场景分类
#### 3. **公网穿透配置**
- ✅ 在NAS上安装并配置frpc客户端(Docker方式)
- ✅ 配置frps服务端端口映射
- ✅ 限制FRP Dashboard仅本地访问
- ✅ 配置防火墙,关闭不必要端口
- ✅ 停用rpcbind服务
- ✅ 公网访问测试成功
---
### 🌐 访问地址
| 访问方式 | 地址 | 说明 |
|---------|------|------|
| **公网访问** | http://180.97.221.140:2320 | 全球可访问 |
| **内网访问** | http://192.168.58.20:4320 | 局域网内访问 |
---
### 🔒 安全配置
**公网服务器开放端口:**
- 22 - SSH
- 7000 - FRP服务
- 2320 - 3D素材项目
**已关闭端口:**
- ~~111~~ - rpcbind
- ~~7500~~ - FRP Dashboard (仅本地)
---
### 📁 文档位置
- **部署文档:** `/Users/lanmu/Desktop/gongxiangp2/DEPLOYMENT.md`
- **公网访问文档:** `/Users/lanmu/Desktop/gongxiangp2/PUBLIC_ACCESS.md`
- **项目信息:** `/Users/lanmu/Desktop/gongxiangp2/PROJECT_INFO.md`
---
### 🎯 下一步操作
1. **访问系统:** 打开 http://180.97.221.140:2320
2. **注册账户:** 创建第一个用户
3. **激活管理员:** 在NAS上执行:
```bash
docker compose exec app npx prisma db execute --schema=/app/prisma/schema.prisma --stdin <<< "UPDATE users SET role='ADMIN', \"isActive\"=true WHERE email='your-email@example.com';"
系统已完全部署并可通过公网访问!🚀
docker compose exec app npx prisma db execute --schema=/app/prisma/schema.prisma --stdin <<< "UPDATE users SET role='ADMIN', \"isActive\"=true WHERE email='your-email@example.com';"
User
我内网访问什么地址