部署更新 - 2026-01-12 23:25:35
This commit is contained in:
parent
e465f7d766
commit
0368cf8f62
@ -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)
|
||||
|
||||
@ -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))
|
||||
}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user