部署更新 - 2026-01-12 23:25:35

This commit is contained in:
chenpei 2026-01-12 23:25:35 +08:00
parent e465f7d766
commit 0368cf8f62
5 changed files with 109 additions and 43 deletions

View File

@ -9,6 +9,7 @@ import fs from 'fs/promises'
import { createWriteStream, createReadStream } from 'fs'
import { pipeline } from 'stream/promises'
import { getUploadSession, deleteUploadSession } from '@/lib/upload-sessions'
import { sanitizeFileName } from '@/lib/image-processor'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
@ -62,7 +63,10 @@ export async function POST(req: NextRequest) {
// 合并文件
await fs.mkdir(tempDir, { recursive: true })
const mergedFileName = originalFileName || uploadSession.fileName || `upload-${uploadId}`
// 处理文件名:提取纯文件名(去除路径),并清理特殊字符,避免中文路径或特殊字符导致的问题
const rawFileName = originalFileName || uploadSession.fileName || `upload-${uploadId}`
const baseFileName = path.basename(rawFileName) // 提取文件名部分,去除路径
const mergedFileName = sanitizeFileName(baseFileName) || `upload-${uploadId}` // 清理特殊字符
const mergedPath = path.join(tempDir, `merged-${mergedFileName}`)
const mergedStream = createWriteStream(mergedPath)

View File

@ -75,7 +75,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
// 音乐:最多100个
const [batchMusicFiles, setBatchMusicFiles] = useState<Array<{ file: File; productModel: string }>>([])
// 种草视频:最多8个
const [batchVideoFiles, setBatchVideoFiles] = useState<Array<{ file: File; productModel: string }>>([])
const [batchVideoFiles, setBatchVideoFiles] = useState<Array<{ file: File; productModel: string; productCategoryName?: string }>>([])
// 3D场景:最多5个,每个需要预览图
const [batch3DFiles, setBatch3DFiles] = useState<Array<{ file: File; productModel: string; previewImage?: File }>>([])
// 品牌VI:最多50个PDF
@ -1036,7 +1036,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
setError('')
try {
await uploadBatchVideo(batchVideoFiles, platformName, renderer, (progress) => {
await uploadBatchVideo(batchVideoFiles, platformName, renderer, productCategoryName, (progress) => {
setUploadProgress(progress)
})
@ -1758,11 +1758,17 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{(materialType === '种草视频' && batchVideoFiles.length > 0) && (
<BatchVideoList
files={batchVideoFiles}
productCategories={productCategories}
onUpdateProductModel={(index, value) => {
const newFiles = [...batchVideoFiles]
newFiles[index].productModel = value
setBatchVideoFiles(newFiles)
}}
onUpdateProductCategory={(index, value) => {
const newFiles = [...batchVideoFiles]
newFiles[index].productCategoryName = value
setBatchVideoFiles(newFiles)
}}
onDelete={(index) => {
setBatchVideoFiles(batchVideoFiles.filter((_, i) => i !== index))
}}

View File

@ -78,41 +78,75 @@ export function BatchMusicList({ files, onUpdateProductModel, onDelete }: BatchM
// ========== 视频批量上传列表 ==========
interface BatchVideoFile extends BatchFileWithSpu {
productCategoryName?: string
}
interface BatchVideoListProps {
files: BatchFileWithSpu[]
files: BatchVideoFile[]
productCategories: Array<{ id: string; name: string }>
onUpdateProductModel: (index: number, value: string) => void
onUpdateProductCategory?: (index: number, value: string) => void
onDelete: (index: number) => void
}
export function BatchVideoList({ files, onUpdateProductModel, onDelete }: BatchVideoListProps) {
export function BatchVideoList({
files,
productCategories,
onUpdateProductModel,
onUpdateProductCategory,
onDelete
}: BatchVideoListProps) {
if (files.length === 0) return null
return (
<div className="mb-4 space-y-3 max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-apple p-4">
{files.map((item, index) => (
<div key={index} className="flex items-center space-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
{item.file.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatBytes(item.file.size)}
</p>
<div key={index} className="flex flex-col space-y-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
<div className="flex items-center space-x-3">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
{item.file.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{formatBytes(item.file.size)}
</p>
</div>
<input
type="text"
value={item.productModel}
onChange={(e) => onUpdateProductModel(index, e.target.value)}
className="flex-1 px-3 py-1.5 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="SPU型号"
/>
<button
type="button"
onClick={() => onDelete(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
>
删除
</button>
</div>
<input
type="text"
value={item.productModel}
onChange={(e) => onUpdateProductModel(index, e.target.value)}
className="flex-1 px-3 py-1.5 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="SPU型号"
/>
<button
type="button"
onClick={() => onDelete(index)}
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
>
删除
</button>
{/* 品类字段 */}
{productCategories.length > 0 && onUpdateProductCategory && (
<div>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
品类(可选)
</label>
<select
value={item.productCategoryName || ''}
onChange={(e) => onUpdateProductCategory(index, e.target.value)}
className="w-full px-3 py-1.5 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none"
>
<option value="">不选择品类</option>
{productCategories.map((category) => (
<option key={category.id} value={category.name}>
{category.name}
</option>
))}
</select>
</div>
)}
</div>
))}
</div>

View File

@ -184,9 +184,10 @@ export async function uploadBatchMusic(
* 批量上传种草视频
*/
export async function uploadBatchVideo(
files: Array<{ file: File; productModel: string }>,
files: Array<{ file: File; productModel: string; productCategoryName?: string }>,
platformName: string,
renderer: string,
productCategoryName: string,
onProgress: (progress: number) => void
) {
const baseMetadata = {
@ -195,6 +196,7 @@ export async function uploadBatchVideo(
sceneName: '通用',
platformName: platformName || '',
renderer: renderer,
productCategoryName: productCategoryName || '',
}
const results = await Promise.all(
@ -203,6 +205,7 @@ export async function uploadBatchVideo(
const metadata = {
...baseMetadata,
productModel: item.productModel,
productCategoryName: item.productCategoryName || productCategoryName || '',
fileName: item.file.name,
}

View File

@ -2,35 +2,54 @@
## 核心部署脚本
### `deploy-local-build.ps1` ⭐
**主部署脚本** - 本地构建镜像并部署到服务器
- 支持影子系统和正式系统
- 本地构建 Docker 镜像
- 支持清理旧镜像
### `deploy-git-build.ps1` ⭐
**主部署脚本** - Git 工作流部署(推荐使用)
- 本地提交代码到 Git
- 服务器从 Git 拉取代码
- 服务器端构建 Docker 镜像
- 自动部署新容器
- 支持清理旧镜像(保留最新4个)
**重要提示:**
- ⚠️ **文件位置**:此脚本位于**项目根目录**(`d:\gongxiangp2\deploy-local-build.ps1`),**不在** `scripts` 目录
- ⚠️ **错误记录**:之前误判文件不存在,实际文件存在于项目根目录,请勿重复此错误
- ⚠️ **文件位置**:此脚本位于**项目根目录**(`d:\gongxiangp2\deploy-git-build.ps1`),**不在** `scripts` 目录
- ⚠️ **部署流程**:本地提交 → Git推送 → 服务器拉取 → 服务器构建 → 部署
**使用方式:**
```powershell
# 在项目根目录执行(d:\gongxiangp2\)
# 部署到正式系统(端口 4320,自动提交并推送代码)
.\deploy-git-build.ps1 -Target production
# 部署到影子系统(端口 5320)
.\deploy-local-build.ps1 -Target shadow
.\deploy-git-build.ps1 -Target shadow
# 部署到正式系统(端口 4320)
.\deploy-local-build.ps1 -Target production
# 指定提交信息
.\deploy-git-build.ps1 -Target production -CommitMessage "修复BUG"
# 部署并清理旧镜像
.\deploy-local-build.ps1 -Target production -CleanImages
# 仅提交不推送(用于本地测试)
.\deploy-git-build.ps1 -Target shadow -SkipPush
# 跳过构建,直接部署
.\deploy-local-build.ps1 -Target shadow -SkipBuild
# 跳过提交,直接推送已有提交
.\deploy-git-build.ps1 -Target production -SkipCommit
# 构建镜像时禁用缓存
.\deploy-git-build.ps1 -Target production -NoCache
# 跳过构建产物清理
.\deploy-git-build.ps1 -Target production -SkipCleanup
# 显示帮助
.\deploy-local-build.ps1 -Help
.\deploy-git-build.ps1 -Help
```
**参数说明:**
- `-Target`: 部署目标,`shadow`(影子系统,端口5320)或 `production`(正式系统,端口4320)
- `-CommitMessage`: 提交信息(如果为空,将自动生成)
- `-SkipCommit`: 跳过Git提交(直接推送到远程)
- `-SkipPush`: 跳过Git推送(仅提交,不推送)
- `-NoCache`: 构建镜像时禁用缓存
- `-SkipCleanup`: 跳过构建产物清理
## 基础镜像管理
### `save-base-images.ps1`