feat: 为任务素材增加钉钉审批编号
This commit is contained in:
parent
b19a5fff58
commit
362a468d67
@ -184,6 +184,7 @@ export async function GET(
|
||||
videoDuration: true,
|
||||
videoProcessingStatus: true,
|
||||
taskNo: true,
|
||||
dingtalkApprovalNo: true,
|
||||
taskStatus: true,
|
||||
taskFetchedAt: true,
|
||||
isFullSet: true,
|
||||
@ -255,6 +256,7 @@ export async function GET(
|
||||
videoDuration: true,
|
||||
videoProcessingStatus: true,
|
||||
taskNo: true,
|
||||
dingtalkApprovalNo: true,
|
||||
taskStatus: true,
|
||||
taskFetchedAt: true,
|
||||
isFullSet: true,
|
||||
@ -415,6 +417,7 @@ export async function GET(
|
||||
videoDuration: materialWithUser.videoDuration || null,
|
||||
videoProcessingStatus: materialWithUser.videoProcessingStatus || null,
|
||||
taskNo: materialWithUser.taskNo || null,
|
||||
dingtalkApprovalNo: materialWithUser.dingtalkApprovalNo || null,
|
||||
taskStatus: materialWithUser.taskStatus || null,
|
||||
taskFetchedAt: materialWithUser.taskFetchedAt || null,
|
||||
isFullSet: deriveTaskDetailIsFullSet(
|
||||
@ -507,6 +510,9 @@ export async function PUT(
|
||||
authorName: String(formData.get('authorName') || ''),
|
||||
userId: formData.get('userId') as string | null,
|
||||
taskNo: formData.get('taskNo') as string | null,
|
||||
...(formData.has('dingtalkApprovalNo')
|
||||
? { dingtalkApprovalNo: formData.get('dingtalkApprovalNo') as string | null }
|
||||
: {}),
|
||||
taskDetailSourceType: formData.get('taskDetailSourceType') as string | null,
|
||||
taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null,
|
||||
isFullSet: formData.get('isFullSet') as string | null,
|
||||
@ -545,6 +551,7 @@ export async function PUT(
|
||||
renderer: result.material.renderer,
|
||||
authorName: result.material.authorName,
|
||||
taskNo: result.material.taskNo,
|
||||
dingtalkApprovalNo: (result.material as any).dingtalkApprovalNo || null,
|
||||
taskDetailSourceType: (result.material as any).taskDetailSourceType || null,
|
||||
taskDetailRequesterUserId: (result.material as any).taskDetailRequesterUserId || null,
|
||||
taskDetailRequesterName: (result.material as any).taskDetailRequesterName || null,
|
||||
|
||||
@ -32,6 +32,9 @@ export async function PUT(request: NextRequest) {
|
||||
authorName: String(formData.get('authorName') || ''),
|
||||
userId: formData.get('userId') as string | null,
|
||||
taskNo: formData.get('taskNo') as string | null,
|
||||
...(formData.has('dingtalkApprovalNo')
|
||||
? { dingtalkApprovalNo: formData.get('dingtalkApprovalNo') as string | null }
|
||||
: {}),
|
||||
taskDetailSourceType: formData.get('taskDetailSourceType') as string | null,
|
||||
taskDetailRequesterUserId: formData.get('taskDetailRequesterUserId') as string | null,
|
||||
isFullSet: formData.get('isFullSet') as string | null,
|
||||
|
||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { materialId: matId, assets, taskNo } = body;
|
||||
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||
|
||||
materialId = matId;
|
||||
|
||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||
|
||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
||||
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||
|
||||
// 步骤5: 清理临时文件
|
||||
await cleanupTempFiles(tempFiles, uploadIds);
|
||||
|
||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
// POST - 初始化3D原图素材记录
|
||||
export async function POST(request: NextRequest) {
|
||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!productModel || !categoryName || !sceneName) {
|
||||
@ -54,6 +55,11 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: (session.user as any).id },
|
||||
|
||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { materialId: matId, assets, taskNo } = body;
|
||||
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||
|
||||
materialId = matId;
|
||||
|
||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||
|
||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
||||
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||
|
||||
// 步骤5: 清理临时文件
|
||||
await cleanupTempFiles(tempFiles, uploadIds);
|
||||
|
||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
// POST - 初始化AI原图素材记录
|
||||
export async function POST(request: NextRequest) {
|
||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!productModel || !categoryName || !sceneName) {
|
||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: (session.user as any).id },
|
||||
|
||||
@ -19,6 +19,7 @@ export const maxDuration = 10800
|
||||
interface CompleteRequestBody {
|
||||
materialId: string
|
||||
taskNo?: string
|
||||
dingtalkApprovalNo?: string
|
||||
noBarcodes?: boolean
|
||||
assets: unknown
|
||||
}
|
||||
@ -88,6 +89,7 @@ export async function POST(request: NextRequest) {
|
||||
const result = await completeProductPackageUpload({
|
||||
materialId: body.materialId,
|
||||
taskNo: body.taskNo,
|
||||
dingtalkApprovalNo: body.dingtalkApprovalNo,
|
||||
assets,
|
||||
operatorUserId: (session.user as any).id,
|
||||
options: {
|
||||
@ -100,6 +102,7 @@ export async function POST(request: NextRequest) {
|
||||
data: {
|
||||
materialId: result.materialId,
|
||||
taskNo: result.taskNo,
|
||||
dingtalkApprovalNo: result.dingtalkApprovalNo,
|
||||
isFullSet: result.isFullSet,
|
||||
message: '上传完成',
|
||||
},
|
||||
|
||||
@ -9,6 +9,7 @@ import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { validateTaskNo, isTaskNoExists, purgeDeletedMaterialsByTaskNo } from '@/lib/product-package';
|
||||
import { normalizeTaskDetailSourceType } from '@/lib/task-detail-source-type';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@ -24,6 +25,7 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const {
|
||||
taskNo,
|
||||
dingtalkApprovalNo,
|
||||
productModel,
|
||||
categoryName,
|
||||
sceneName,
|
||||
@ -33,6 +35,14 @@ export async function POST(request: NextRequest) {
|
||||
const rawTaskDetailSourceType = body?.taskDetailSourceType;
|
||||
const taskDetailSourceType = normalizeTaskDetailSourceType(rawTaskDetailSourceType);
|
||||
const taskDetailRequesterUserId = String(body?.taskDetailRequesterUserId || '').trim();
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
|
||||
if (dingtalkApprovalNoError) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: dingtalkApprovalNoError },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (rawTaskDetailSourceType != null && String(rawTaskDetailSourceType).trim() && !taskDetailSourceType) {
|
||||
return NextResponse.json(
|
||||
|
||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { materialId: matId, assets, taskNo } = body;
|
||||
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||
|
||||
materialId = matId;
|
||||
|
||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||
|
||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
||||
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||
|
||||
// 步骤5: 清理临时文件
|
||||
await cleanupTempFiles(tempFiles, uploadIds);
|
||||
|
||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
// POST - 初始化实拍原图素材记录
|
||||
export async function POST(request: NextRequest) {
|
||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!productModel || !categoryName || !sceneName) {
|
||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: (session.user as any).id },
|
||||
|
||||
@ -28,6 +28,10 @@ import {
|
||||
resolveOwnedMergedUpload,
|
||||
type OwnedMergedUpload,
|
||||
} from '@/lib/upload-sessions';
|
||||
import {
|
||||
getDingtalkApprovalNoError,
|
||||
normalizeDingtalkApprovalNo,
|
||||
} from '@/lib/dingtalk-approval-no';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
@ -234,6 +238,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const isTaskOtherMaterial = materialType === '任务-其他';
|
||||
const isTaskMaterial = materialType.startsWith('任务-');
|
||||
const rendererParam = formData.get('renderer') as string | null;
|
||||
// 统一处理renderer:3D场景必须有renderer,其他类型可选
|
||||
const renderer = requiresRenderer(materialType)
|
||||
@ -241,6 +246,10 @@ export async function POST(request: NextRequest) {
|
||||
: (rendererParam || '');
|
||||
const rawTaskNo = (formData.get('taskNo') as string) || '';
|
||||
const taskNo = isTaskOtherMaterial ? rawTaskNo.trim().toUpperCase() : rawTaskNo;
|
||||
const rawDingtalkApprovalNo = (formData.get('dingtalkApprovalNo') as string) || '';
|
||||
const dingtalkApprovalNo = isTaskMaterial
|
||||
? normalizeDingtalkApprovalNo(rawDingtalkApprovalNo)
|
||||
: null;
|
||||
const productModel = productModelInput || (isTaskOtherMaterial ? taskNo : '');
|
||||
const shouldAutoFillGeneralCategoryScene =
|
||||
AUTO_GENERAL_TYPES.includes(materialType) || materialType === '标准模型';
|
||||
@ -298,6 +307,21 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isTaskMaterial && rawDingtalkApprovalNo.trim()) {
|
||||
return NextResponse.json(
|
||||
{ error: '钉钉审批编号仅适用于“任务-”素材' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
return NextResponse.json(
|
||||
{ error: dingtalkApprovalNoError },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 任务号验证(可选,如果提供则验证格式和唯一性)
|
||||
if (taskNo && taskNo.trim()) {
|
||||
const { validateTaskNo, isTaskNoExists, purgeDeletedMaterialsByTaskNo } = await import('@/lib/product-package');
|
||||
@ -831,31 +855,10 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提供了taskNo,上传完成后写入taskNo(所有素材类型统一处理)
|
||||
if (taskNo && taskNo.trim()) {
|
||||
try {
|
||||
// 双重验证:再次检查taskNo唯一性(防止并发问题)
|
||||
const { validateTaskNo, isTaskNoExists } = await import('@/lib/product-package');
|
||||
if (validateTaskNo(taskNo)) {
|
||||
const exists = await isTaskNoExists(taskNo);
|
||||
if (!exists) {
|
||||
// 写入taskNo和taskStatus
|
||||
await prisma.material.update({
|
||||
where: { id: material.id },
|
||||
data: {
|
||||
taskNo: taskNo,
|
||||
taskStatus: 'completed',
|
||||
} as any,
|
||||
});
|
||||
console.log('[Upload] TaskNo written to database:', taskNo);
|
||||
} else {
|
||||
console.warn('[Upload] TaskNo already exists, skipping write:', taskNo);
|
||||
}
|
||||
}
|
||||
} catch (taskNoError) {
|
||||
console.error('[Upload] Failed to write taskNo:', taskNoError);
|
||||
// taskNo写入失败不影响上传流程,只记录错误
|
||||
}
|
||||
// 文件成功落盘后再写入任务关联字段,避免半成品占用任务号。
|
||||
if (isTaskMaterial || taskNo.trim()) {
|
||||
const { assignTaskNoOnComplete } = await import('@/lib/product-package');
|
||||
await assignTaskNoOnComplete(material.id, taskNo, dingtalkApprovalNo);
|
||||
}
|
||||
|
||||
// 记录上传日志
|
||||
@ -884,6 +887,10 @@ export async function POST(request: NextRequest) {
|
||||
oldValue: null,
|
||||
newValue: taskNo || null,
|
||||
},
|
||||
dingtalkApprovalNo: {
|
||||
oldValue: null,
|
||||
newValue: dingtalkApprovalNo,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -1010,16 +1017,15 @@ export async function POST(request: NextRequest) {
|
||||
console.log(`[Upload] Cleaned up ${cleanupResult.deletedCount} files, ${cleanupResult.failedCount} failed`);
|
||||
}
|
||||
|
||||
// 如果素材记录已创建但没有taskNo,删除记录(避免残留)
|
||||
// 只回滚尚未完成的素材;任务号本身是可选字段,不能据此判断上传是否完成。
|
||||
if (materialId) {
|
||||
try {
|
||||
const material = await prisma.material.findUnique({
|
||||
where: { id: materialId },
|
||||
select: { taskNo: true },
|
||||
select: { taskStatus: true },
|
||||
});
|
||||
|
||||
// 如果没有taskNo,说明上传未完成,删除记录
|
||||
if (material && !material.taskNo) {
|
||||
if (material && material.taskStatus !== 'completed') {
|
||||
// 先删除关联记录
|
||||
const materialType = (error as any)?.materialType || '未知';
|
||||
await deleteMaterialRelatedRecords(materialId, materialType);
|
||||
|
||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { materialId: matId, assets, taskNo } = body;
|
||||
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||
|
||||
materialId = matId;
|
||||
|
||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||
|
||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
||||
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||
|
||||
// 步骤5: 清理临时文件
|
||||
await cleanupTempFiles(tempFiles, uploadIds);
|
||||
|
||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
// POST - 初始化任务-页面素材记录
|
||||
export async function POST(request: NextRequest) {
|
||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!productModel || !categoryName || !sceneName) {
|
||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: (session.user as any).id },
|
||||
|
||||
@ -31,7 +31,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { materialId: matId, assets, taskNo } = body;
|
||||
const { materialId: matId, assets, taskNo, dingtalkApprovalNo } = body;
|
||||
|
||||
materialId = matId;
|
||||
|
||||
@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
||||
await saveAssets(materialId!, uploadedPreviewUrls, zipInfo);
|
||||
|
||||
// 步骤4.5: 仅在素材成功完成后写入任务号
|
||||
await assignTaskNoOnComplete(materialId!, taskNo);
|
||||
await assignTaskNoOnComplete(materialId!, taskNo, dingtalkApprovalNo);
|
||||
|
||||
// 步骤5: 清理临时文件
|
||||
await cleanupTempFiles(tempFiles, uploadIds);
|
||||
|
||||
@ -3,6 +3,7 @@ import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { purgeDeletedMaterialsByTaskNo, validateTaskNo } from '@/lib/product-package';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
// POST - 初始化视频截图素材记录
|
||||
export async function POST(request: NextRequest) {
|
||||
@ -13,7 +14,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { taskNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
const { taskNo, dingtalkApprovalNo, productModel, categoryName, sceneName, platformName, productCategoryName } = body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!productModel || !categoryName || !sceneName) {
|
||||
@ -58,6 +59,11 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
return NextResponse.json({ error: dingtalkApprovalNoError }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: (session.user as any).id },
|
||||
|
||||
@ -5,6 +5,7 @@ import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Header from '@/components/Header'
|
||||
import SearchableUserSelect from '@/components/SearchableUserSelect'
|
||||
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||
import {
|
||||
canManuallyReplacePreview,
|
||||
getSelectableMaterialTypeOptions,
|
||||
@ -16,6 +17,7 @@ import {
|
||||
requiresProductCategoryName,
|
||||
requiresRenderer,
|
||||
} from '@/lib/material-validation'
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'
|
||||
|
||||
interface Material {
|
||||
id: string
|
||||
@ -30,6 +32,7 @@ interface Material {
|
||||
authorName: string
|
||||
previewUrl?: string | null
|
||||
taskNo?: string | null
|
||||
dingtalkApprovalNo?: string | null
|
||||
taskStatus?: string | null
|
||||
taskFetchedAt?: string | null
|
||||
isFullSet?: boolean
|
||||
@ -58,6 +61,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
const [authorName, setAuthorName] = useState('')
|
||||
const [userId, setUserId] = useState('')
|
||||
const [taskNo, setTaskNo] = useState('')
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('')
|
||||
const [taskStatus, setTaskStatus] = useState('')
|
||||
const [taskFetchedAt, setTaskFetchedAt] = useState('')
|
||||
const [isFullSet, setIsFullSet] = useState(false)
|
||||
@ -136,6 +140,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
setAuthorName(data.authorName || '')
|
||||
setUserId(data.userId || '')
|
||||
setTaskNo(data.taskNo || '')
|
||||
setDingtalkApprovalNo(data.dingtalkApprovalNo || '')
|
||||
setTaskStatus(data.taskStatus || '')
|
||||
setTaskFetchedAt(data.taskFetchedAt || '')
|
||||
setIsFullSet(Boolean(data.isFullSet))
|
||||
@ -236,6 +241,14 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
return
|
||||
}
|
||||
|
||||
if (showTaskNo) {
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isFetchedTask) {
|
||||
const confirmed = window.confirm('这条任务素材已经被外部系统获取过,继续保存可能影响对方使用,确认继续吗?')
|
||||
if (!confirmed) {
|
||||
@ -257,6 +270,7 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
formData.append('authorName', authorName)
|
||||
formData.append('userId', userId)
|
||||
formData.append('taskNo', showTaskNo ? taskNo : '')
|
||||
formData.append('dingtalkApprovalNo', showTaskNo ? dingtalkApprovalNo : '')
|
||||
formData.append(
|
||||
'taskDetailRequesterUserId',
|
||||
materialType === '任务-详情' ? taskDetailRequesterUserId : '',
|
||||
@ -478,18 +492,14 @@ export default function EditMaterialPage({ params }: { params: { id: string } })
|
||||
)}
|
||||
|
||||
{showTaskNo && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
任务号
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(event) => setTaskNo(event.target.value)}
|
||||
className="input-apple"
|
||||
placeholder="TASK + 12-18位数字,可带多级 -01"
|
||||
<TaskReferenceFields
|
||||
idPrefix={`edit-material-page-${params.id}`}
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={submitting}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{materialType === '任务-详情' && (
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
} from '@/lib/material-type-options'
|
||||
import { isCertificateReportMaterialType } from '@/lib/certificate-report'
|
||||
import { TASK_DETAIL_SOURCE_TYPE_OPTIONS } from '@/lib/task-detail-source-type'
|
||||
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'
|
||||
import {
|
||||
requiresCategoryAndScene,
|
||||
requiresProductCategoryName,
|
||||
@ -31,6 +33,7 @@ interface EditMaterialModalProps {
|
||||
authorName: string
|
||||
userId: string
|
||||
taskNo?: string | null
|
||||
dingtalkApprovalNo?: string | null
|
||||
taskDetailSourceType?: string | null
|
||||
taskStatus?: string | null
|
||||
taskFetchedAt?: string | null
|
||||
@ -56,6 +59,7 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
||||
const [authorName, setAuthorName] = useState(material.authorName)
|
||||
const [userId, setUserId] = useState(material.userId)
|
||||
const [taskNo, setTaskNo] = useState(material.taskNo || '')
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState(material.dingtalkApprovalNo || '')
|
||||
const [taskDetailSourceType, setTaskDetailSourceType] = useState(material.taskDetailSourceType || '')
|
||||
const [taskDetailRequesterUserId, setTaskDetailRequesterUserId] = useState(material.taskDetailRequesterUserId || '')
|
||||
const [taskDetailRequesterName, setTaskDetailRequesterName] = useState(material.taskDetailRequesterName || '')
|
||||
@ -182,6 +186,14 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
||||
return
|
||||
}
|
||||
|
||||
if (showTaskNo) {
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isFetchedTask) {
|
||||
const confirmed = window.confirm('这条任务素材已经被外部系统获取过,继续保存可能影响对方使用,确认继续吗?')
|
||||
if (!confirmed) {
|
||||
@ -205,6 +217,7 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
||||
formData.append('authorName', authorName)
|
||||
formData.append('userId', userId)
|
||||
formData.append('taskNo', showTaskNo ? taskNo : '')
|
||||
formData.append('dingtalkApprovalNo', showTaskNo ? dingtalkApprovalNo : '')
|
||||
formData.append('taskDetailSourceType', materialType === '任务-详情' ? taskDetailSourceType : '')
|
||||
formData.append(
|
||||
'taskDetailRequesterUserId',
|
||||
@ -389,16 +402,14 @@ export default function EditMaterialModal({ material, onClose, onSuccess }: Edit
|
||||
)}
|
||||
|
||||
{showTaskNo && (
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">任务号</label>
|
||||
<input
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(event) => setTaskNo(event.target.value)}
|
||||
className="input-apple"
|
||||
placeholder="TASK + 12-18位数字,可带多级 -01"
|
||||
<TaskReferenceFields
|
||||
idPrefix={`edit-material-${material.id}`}
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={updating}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{materialType === '任务-详情' && (
|
||||
|
||||
@ -51,7 +51,7 @@ describe('UploadModal - 任务-其他', () => {
|
||||
}) as any
|
||||
})
|
||||
|
||||
test('任务-其他应使用公共上传面板形式,只保留任务号 + 预览图片 + 素材文件', async () => {
|
||||
test('任务-其他应使用公共上传面板形式,任务号与钉钉审批编号并列显示', async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<UploadModal onClose={jest.fn()} onSuccess={jest.fn()} />)
|
||||
@ -63,6 +63,7 @@ describe('UploadModal - 任务-其他', () => {
|
||||
expect(screen.getByLabelText('任务号 *')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.getByLabelText('钉钉审批编号')).toBeInTheDocument()
|
||||
expect(screen.getByText('预览图片 *')).toBeInTheDocument()
|
||||
expect(screen.getByText('素材文件 *')).toBeInTheDocument()
|
||||
expect(screen.getByText('支持格式:压缩包(.zip / .rar / .7z)')).toBeInTheDocument()
|
||||
|
||||
@ -60,11 +60,13 @@ import ProductPackageUploader from '@/components/upload/ProductPackageUploader'
|
||||
import VideoScreenshotUploader from '@/components/upload/VideoScreenshotUploader'
|
||||
import TaskPageUploader from '@/components/upload/TaskPageUploader'
|
||||
import CertificateReportUploader from '@/components/upload/CertificateReportUploader'
|
||||
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||
import {
|
||||
hasDragLeftDropZone,
|
||||
prepareFileDrag,
|
||||
validateImageFiles,
|
||||
} from '@/components/upload/drag-drop-utils'
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'
|
||||
|
||||
interface UploadModalProps {
|
||||
onClose: () => void
|
||||
@ -155,6 +157,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
|
||||
// 任务号(所有素材类型支持)
|
||||
const [taskNo, setTaskNo] = useState('')
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('')
|
||||
|
||||
// SKU号(视频类素材支持)
|
||||
const [sku, setSku] = useState('')
|
||||
@ -178,6 +181,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
|
||||
const isTaskOtherMaterial = materialType === '任务-其他'
|
||||
const isTaskMaterial = materialType.startsWith('任务-')
|
||||
const usesDedicatedUploader = SPECIAL_UPLOAD_TYPES.includes(materialType as typeof SPECIAL_UPLOAD_TYPES[number])
|
||||
|
||||
const previewInputRef = useRef<HTMLInputElement>(null)
|
||||
@ -239,6 +243,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
setProductModel('')
|
||||
setIsBatchMode(false)
|
||||
setTaskNo('') // 切换素材类型时清空任务号
|
||||
setDingtalkApprovalNo('')
|
||||
setSku('') // 切换素材类型时清空SKU号
|
||||
}, [materialType])
|
||||
|
||||
@ -1505,6 +1510,14 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isTaskMaterial) {
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 任务号为空时仅提醒,不阻止上传(已在UI中显示提醒)
|
||||
|
||||
// 使用统一的验证逻辑
|
||||
@ -1613,6 +1626,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
productCategoryName,
|
||||
renderer: resolvedRenderer,
|
||||
taskNo: resolvedTaskNo,
|
||||
dingtalkApprovalNo: isTaskMaterial ? dingtalkApprovalNo.trim() : '',
|
||||
})
|
||||
|
||||
// 分片后主文件已在服务器,添加路径信息
|
||||
@ -1793,23 +1807,36 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 任务号输入框(有专用上传器的任务类型不显示,因为它们有自己的任务号输入) */}
|
||||
{!usesDedicatedUploader && (
|
||||
{/* 专用上传器会在自己的表单内渲染任务关联字段。 */}
|
||||
{!usesDedicatedUploader && isTaskMaterial && (
|
||||
<TaskReferenceFields
|
||||
idPrefix="generic-task-upload"
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
taskNoRequired={isTaskOtherMaterial}
|
||||
disabled={uploading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!usesDedicatedUploader && !isTaskMaterial && (
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
{isTaskOtherMaterial ? '任务号 *' : <>任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span></>}
|
||||
<label htmlFor="taskNo" className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
任务号
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(e) => setTaskNo(e.target.value)}
|
||||
placeholder="TASK + 12-18位数字,如 TASK202601030184 或 TASK202601071437194808-01"
|
||||
className="w-full px-4 py-2 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"
|
||||
onChange={(event) => setTaskNo(event.target.value)}
|
||||
placeholder="例如:TASK202601030184"
|
||||
className="w-full rounded-apple border border-gray-300 px-4 py-2 outline-none focus:border-gray-900 focus:ring-2 focus:ring-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-gray-400 dark:focus:ring-gray-600"
|
||||
disabled={uploading}
|
||||
aria-invalid={Boolean(taskNo && !validateTaskNo(taskNo.trim().toUpperCase()))}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo.trim().toUpperCase()) && (
|
||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
prepareFileDrag,
|
||||
validateImageFiles,
|
||||
} from './drag-drop-utils';
|
||||
import TaskReferenceFields from './TaskReferenceFields';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
interface AssetFile {
|
||||
file: File;
|
||||
@ -43,6 +45,7 @@ export default function AIOriginalUploader({
|
||||
}: AIOriginalUploaderProps) {
|
||||
|
||||
const [taskNo, setTaskNo] = useState('');
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||
const [productModel, setProductModel] = useState('');
|
||||
const [categoryName, setCategoryName] = useState('');
|
||||
const [sceneName, setSceneName] = useState('');
|
||||
@ -203,6 +206,11 @@ export default function AIOriginalUploader({
|
||||
setError('任务号格式错误');
|
||||
return;
|
||||
}
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError);
|
||||
return;
|
||||
}
|
||||
if (!productModel.trim()) {
|
||||
setError('请输入产品型号(SPU)');
|
||||
return;
|
||||
@ -230,6 +238,7 @@ export default function AIOriginalUploader({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
productModel,
|
||||
categoryName,
|
||||
sceneName,
|
||||
@ -287,6 +296,7 @@ export default function AIOriginalUploader({
|
||||
body: JSON.stringify({
|
||||
materialId,
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
assets: {
|
||||
preview: [], // 不传预览图,后端自动生成
|
||||
main: mainAssets.map((m, index) => ({
|
||||
@ -330,24 +340,14 @@ export default function AIOriginalUploader({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 任务号字段 */}
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(e) => setTaskNo(e.target.value)}
|
||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
||||
className="w-full px-4 py-2 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"
|
||||
<TaskReferenceFields
|
||||
idPrefix="ai-original"
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo) && (
|
||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SPU型号 */}
|
||||
<div>
|
||||
|
||||
@ -18,6 +18,8 @@ import {
|
||||
prepareFileDrag,
|
||||
validateImageFiles,
|
||||
} from './drag-drop-utils';
|
||||
import TaskReferenceFields from './TaskReferenceFields';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
const TASK_DETAIL_SOURCE_FILE_EXTENSIONS = ['psd', 'psb', 'tif', 'tiff', 'zip', 'rar', '7z'] as const;
|
||||
const TASK_DETAIL_SOURCE_FILE_ACCEPT = '.psd,.psb,.tif,.tiff,.zip,.rar,.7z';
|
||||
@ -100,6 +102,7 @@ export default function ProductPackageUploader({
|
||||
}: ProductPackageUploaderProps) {
|
||||
// 基本信息
|
||||
const [taskNo, setTaskNo] = useState('');
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||
const [productModel, setProductModel] = useState('');
|
||||
const [categoryName, setCategoryName] = useState('');
|
||||
const [sceneName, setSceneName] = useState('');
|
||||
@ -442,6 +445,11 @@ export default function ProductPackageUploader({
|
||||
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)');
|
||||
return;
|
||||
}
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError);
|
||||
return;
|
||||
}
|
||||
|
||||
// 主图改为非必选,只要至少有一张图片即可
|
||||
if (mainImages.length === 0 && skuImages.length === 0 && detailImages.length === 0) {
|
||||
@ -504,6 +512,7 @@ export default function ProductPackageUploader({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskNo,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
productModel,
|
||||
categoryName,
|
||||
sceneName,
|
||||
@ -613,6 +622,7 @@ export default function ProductPackageUploader({
|
||||
body: JSON.stringify({
|
||||
materialId,
|
||||
taskNo, // 传递taskNo,上传完成后写入数据库
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
noBarcodes,
|
||||
assets: {
|
||||
main: mainAssets,
|
||||
@ -689,24 +699,14 @@ export default function ProductPackageUploader({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 任务号字段(任务-详情独有) */}
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(e) => setTaskNo(e.target.value)}
|
||||
placeholder="TASK + 12-18位数字,如 TASK202601030184 或 TASK202601071437194808-01"
|
||||
className="w-full px-4 py-2 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"
|
||||
<TaskReferenceFields
|
||||
idPrefix="product-package"
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo) && (
|
||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
prepareFileDrag,
|
||||
validateImageFiles,
|
||||
} from './drag-drop-utils';
|
||||
import TaskReferenceFields from './TaskReferenceFields';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
interface AssetFile {
|
||||
file: File;
|
||||
@ -43,6 +45,7 @@ export default function RealPhotoUploader({
|
||||
}: RealPhotoUploaderProps) {
|
||||
|
||||
const [taskNo, setTaskNo] = useState('');
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||
const [productModel, setProductModel] = useState('');
|
||||
const [categoryName, setCategoryName] = useState('');
|
||||
const [sceneName, setSceneName] = useState('');
|
||||
@ -203,6 +206,11 @@ export default function RealPhotoUploader({
|
||||
setError('任务号格式错误');
|
||||
return;
|
||||
}
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError);
|
||||
return;
|
||||
}
|
||||
if (!productModel.trim()) {
|
||||
setError('请输入产品型号(SPU)');
|
||||
return;
|
||||
@ -230,6 +238,7 @@ export default function RealPhotoUploader({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
productModel,
|
||||
categoryName,
|
||||
sceneName,
|
||||
@ -286,6 +295,7 @@ export default function RealPhotoUploader({
|
||||
body: JSON.stringify({
|
||||
materialId,
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
assets: {
|
||||
preview: [],
|
||||
main: mainAssets.map((m, index) => ({
|
||||
@ -327,24 +337,14 @@ export default function RealPhotoUploader({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 任务号字段 */}
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(e) => setTaskNo(e.target.value)}
|
||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
||||
className="w-full px-4 py-2 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"
|
||||
<TaskReferenceFields
|
||||
idPrefix="real-photo"
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo) && (
|
||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SPU型号 */}
|
||||
<div>
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
prepareFileDrag,
|
||||
validateImageFiles,
|
||||
} from './drag-drop-utils';
|
||||
import TaskReferenceFields from './TaskReferenceFields';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
interface AssetFile {
|
||||
file: File;
|
||||
@ -43,6 +45,7 @@ export default function TaskPageUploader({
|
||||
}: TaskPageUploaderProps) {
|
||||
|
||||
const [taskNo, setTaskNo] = useState('');
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||
const [productModel, setProductModel] = useState('');
|
||||
const [categoryName, setCategoryName] = useState('');
|
||||
const [sceneName, setSceneName] = useState('');
|
||||
@ -181,6 +184,11 @@ export default function TaskPageUploader({
|
||||
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)');
|
||||
return;
|
||||
}
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证至少有一张预览图
|
||||
if (previewImages.length === 0) {
|
||||
@ -201,6 +209,7 @@ export default function TaskPageUploader({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskNo,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
productModel,
|
||||
categoryName,
|
||||
sceneName,
|
||||
@ -262,6 +271,7 @@ export default function TaskPageUploader({
|
||||
body: JSON.stringify({
|
||||
materialId,
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
assets: {
|
||||
main: previewAssets.map((m, index) => ({
|
||||
uploadId: m.uploadId,
|
||||
@ -313,24 +323,14 @@ export default function TaskPageUploader({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 任务号字段 */}
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(e) => setTaskNo(e.target.value)}
|
||||
placeholder="TASK + 12-18位数字,如 TASK202601030184 或 TASK202601071437194808-01"
|
||||
className="w-full px-4 py-2 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"
|
||||
<TaskReferenceFields
|
||||
idPrefix="task-page"
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo) && (
|
||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SPU型号 */}
|
||||
<div>
|
||||
|
||||
58
components/upload/TaskReferenceFields.test.tsx
Normal file
58
components/upload/TaskReferenceFields.test.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
|
||||
import TaskReferenceFields from '@/components/upload/TaskReferenceFields'
|
||||
|
||||
describe('TaskReferenceFields', () => {
|
||||
test('任务号与钉钉审批编号使用响应式双列布局', () => {
|
||||
const { container } = render(
|
||||
<TaskReferenceFields
|
||||
idPrefix="task-reference"
|
||||
taskNo=""
|
||||
onTaskNoChange={jest.fn()}
|
||||
dingtalkApprovalNo=""
|
||||
onDingtalkApprovalNoChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(container.firstChild).toHaveClass('grid-cols-1', 'md:grid-cols-2')
|
||||
expect(screen.getByLabelText('任务号')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('钉钉审批编号')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('任务号输入会统一转为大写', () => {
|
||||
const onTaskNoChange = jest.fn()
|
||||
|
||||
render(
|
||||
<TaskReferenceFields
|
||||
idPrefix="task-reference"
|
||||
taskNo=""
|
||||
onTaskNoChange={onTaskNoChange}
|
||||
dingtalkApprovalNo=""
|
||||
onDingtalkApprovalNoChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByLabelText('任务号'), {
|
||||
target: { value: 'task202601030184' },
|
||||
})
|
||||
|
||||
expect(onTaskNoChange).toHaveBeenCalledWith('TASK202601030184')
|
||||
})
|
||||
|
||||
test('非法审批编号应显示紧邻输入框的错误状态', () => {
|
||||
render(
|
||||
<TaskReferenceFields
|
||||
idPrefix="task-reference"
|
||||
taskNo=""
|
||||
onTaskNoChange={jest.fn()}
|
||||
dingtalkApprovalNo="20250230123"
|
||||
onDingtalkApprovalNoChange={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
const input = screen.getByLabelText('钉钉审批编号')
|
||||
fireEvent.blur(input)
|
||||
expect(input).toHaveAttribute('aria-invalid', 'true')
|
||||
expect(screen.getByText('钉钉审批编号前8位不是有效日期')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
112
components/upload/TaskReferenceFields.tsx
Normal file
112
components/upload/TaskReferenceFields.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
import {
|
||||
DINGTALK_APPROVAL_NO_MAX_LENGTH,
|
||||
getDingtalkApprovalNoError,
|
||||
} from '@/lib/dingtalk-approval-no'
|
||||
|
||||
interface TaskReferenceFieldsProps {
|
||||
idPrefix: string
|
||||
taskNo: string
|
||||
onTaskNoChange: (value: string) => void
|
||||
dingtalkApprovalNo: string
|
||||
onDingtalkApprovalNoChange: (value: string) => void
|
||||
taskNoRequired?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function getTaskNoError(value: string): string | null {
|
||||
const normalized = value.trim().toUpperCase()
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
return /^TASK\d{12,18}(-\d{2})*$/.test(normalized)
|
||||
? null
|
||||
: '任务号格式错误'
|
||||
}
|
||||
|
||||
export default function TaskReferenceFields({
|
||||
idPrefix,
|
||||
taskNo,
|
||||
onTaskNoChange,
|
||||
dingtalkApprovalNo,
|
||||
onDingtalkApprovalNoChange,
|
||||
taskNoRequired = false,
|
||||
disabled = false,
|
||||
}: TaskReferenceFieldsProps) {
|
||||
const [taskNoTouched, setTaskNoTouched] = useState(false)
|
||||
const [approvalNoTouched, setApprovalNoTouched] = useState(false)
|
||||
const taskNoId = `${idPrefix}-task-no`
|
||||
const taskNoErrorId = `${taskNoId}-error`
|
||||
const approvalNoId = `${idPrefix}-dingtalk-approval-no`
|
||||
const approvalNoErrorId = `${approvalNoId}-error`
|
||||
const taskNoError = getTaskNoError(taskNo)
|
||||
const approvalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
const visibleTaskNoError = taskNoTouched ? taskNoError : null
|
||||
const visibleApprovalNoError = approvalNoTouched ? approvalNoError : null
|
||||
const inputClassName =
|
||||
'w-full rounded-apple border border-gray-300 px-4 py-2 outline-none focus:border-gray-900 focus:ring-2 focus:ring-gray-200 disabled:cursor-not-allowed disabled:opacity-60 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-gray-400 dark:focus:ring-gray-600'
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="min-w-0">
|
||||
<label
|
||||
htmlFor={taskNoId}
|
||||
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
任务号{taskNoRequired ? ' *' : ''}
|
||||
</label>
|
||||
<input
|
||||
id={taskNoId}
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(event) => onTaskNoChange(event.target.value.toUpperCase())}
|
||||
onBlur={() => setTaskNoTouched(true)}
|
||||
placeholder="例如:TASK202601030184"
|
||||
className={inputClassName}
|
||||
disabled={disabled}
|
||||
required={taskNoRequired}
|
||||
aria-invalid={Boolean(visibleTaskNoError)}
|
||||
aria-describedby={visibleTaskNoError ? taskNoErrorId : undefined}
|
||||
/>
|
||||
{visibleTaskNoError && (
|
||||
<p id={taskNoErrorId} className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{visibleTaskNoError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<label
|
||||
htmlFor={approvalNoId}
|
||||
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
钉钉审批编号
|
||||
</label>
|
||||
<input
|
||||
id={approvalNoId}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
autoComplete="off"
|
||||
maxLength={DINGTALK_APPROVAL_NO_MAX_LENGTH}
|
||||
value={dingtalkApprovalNo}
|
||||
onChange={(event) => onDingtalkApprovalNoChange(event.target.value)}
|
||||
onBlur={() => setApprovalNoTouched(true)}
|
||||
placeholder="例如:202508211445000157693"
|
||||
className={inputClassName}
|
||||
disabled={disabled}
|
||||
aria-invalid={Boolean(visibleApprovalNoError)}
|
||||
aria-describedby={visibleApprovalNoError ? approvalNoErrorId : undefined}
|
||||
/>
|
||||
{visibleApprovalNoError && (
|
||||
<p id={approvalNoErrorId} className="mt-1 text-sm text-red-600 dark:text-red-400">
|
||||
{visibleApprovalNoError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -8,6 +8,8 @@ import {
|
||||
prepareFileDrag,
|
||||
validateImageFiles,
|
||||
} from './drag-drop-utils';
|
||||
import TaskReferenceFields from './TaskReferenceFields';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
interface AssetFile {
|
||||
file: File;
|
||||
@ -43,6 +45,7 @@ export default function ThreeDOriginalUploader({
|
||||
}: ThreeDOriginalUploaderProps) {
|
||||
|
||||
const [taskNo, setTaskNo] = useState('');
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||
const [productModel, setProductModel] = useState('');
|
||||
const [categoryName, setCategoryName] = useState('');
|
||||
const [sceneName, setSceneName] = useState('');
|
||||
@ -203,6 +206,11 @@ export default function ThreeDOriginalUploader({
|
||||
setError('任务号格式错误');
|
||||
return;
|
||||
}
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError);
|
||||
return;
|
||||
}
|
||||
if (!productModel.trim()) {
|
||||
setError('请输入产品型号(SPU)');
|
||||
return;
|
||||
@ -230,6 +238,7 @@ export default function ThreeDOriginalUploader({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
productModel,
|
||||
categoryName,
|
||||
sceneName,
|
||||
@ -287,6 +296,7 @@ export default function ThreeDOriginalUploader({
|
||||
body: JSON.stringify({
|
||||
materialId,
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
assets: {
|
||||
preview: [], // 不传预览图,后端自动生成
|
||||
main: mainAssets.map((m, index) => ({
|
||||
@ -330,24 +340,14 @@ export default function ThreeDOriginalUploader({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 任务号字段 */}
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(e) => setTaskNo(e.target.value)}
|
||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
||||
className="w-full px-4 py-2 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"
|
||||
<TaskReferenceFields
|
||||
idPrefix="three-d-original"
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo) && (
|
||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SPU型号 */}
|
||||
<div>
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
prepareFileDrag,
|
||||
validateImageFiles,
|
||||
} from './drag-drop-utils';
|
||||
import TaskReferenceFields from './TaskReferenceFields';
|
||||
import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no';
|
||||
|
||||
interface AssetFile {
|
||||
file: File;
|
||||
@ -43,6 +45,7 @@ export default function VideoScreenshotUploader({
|
||||
}: VideoScreenshotUploaderProps) {
|
||||
|
||||
const [taskNo, setTaskNo] = useState('');
|
||||
const [dingtalkApprovalNo, setDingtalkApprovalNo] = useState('');
|
||||
const [productModel, setProductModel] = useState('');
|
||||
const [categoryName, setCategoryName] = useState('');
|
||||
const [sceneName, setSceneName] = useState('');
|
||||
@ -204,6 +207,11 @@ export default function VideoScreenshotUploader({
|
||||
setError('任务号格式错误');
|
||||
return;
|
||||
}
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
setError(dingtalkApprovalNoError);
|
||||
return;
|
||||
}
|
||||
if (!productModel.trim()) {
|
||||
setError('请输入产品型号(SPU)');
|
||||
return;
|
||||
@ -234,6 +242,7 @@ export default function VideoScreenshotUploader({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
productModel,
|
||||
categoryName,
|
||||
sceneName,
|
||||
@ -293,6 +302,7 @@ export default function VideoScreenshotUploader({
|
||||
body: JSON.stringify({
|
||||
materialId,
|
||||
taskNo: taskNo || undefined,
|
||||
dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined,
|
||||
assets: {
|
||||
preview: [], // 不传预览图,后端自动生成
|
||||
main: screenshotAssets.map((m, index) => ({
|
||||
@ -345,24 +355,14 @@ export default function VideoScreenshotUploader({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 任务号字段 */}
|
||||
<div>
|
||||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
id="taskNo"
|
||||
type="text"
|
||||
value={taskNo}
|
||||
onChange={(e) => setTaskNo(e.target.value)}
|
||||
placeholder="TASK + 12-18位数字,如 TASK202601030184"
|
||||
className="w-full px-4 py-2 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"
|
||||
<TaskReferenceFields
|
||||
idPrefix="video-screenshot"
|
||||
taskNo={taskNo}
|
||||
onTaskNoChange={setTaskNo}
|
||||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||||
disabled={uploading}
|
||||
/>
|
||||
{taskNo && !validateTaskNo(taskNo) && (
|
||||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SPU型号 */}
|
||||
<div>
|
||||
|
||||
@ -23,6 +23,7 @@ export function buildFormData(params: {
|
||||
productCategoryName: string
|
||||
renderer: string
|
||||
taskNo?: string
|
||||
dingtalkApprovalNo?: string
|
||||
}): FormData {
|
||||
const {
|
||||
previewImages,
|
||||
@ -37,6 +38,7 @@ export function buildFormData(params: {
|
||||
productCategoryName,
|
||||
renderer,
|
||||
taskNo,
|
||||
dingtalkApprovalNo,
|
||||
} = params
|
||||
|
||||
const formData = new FormData()
|
||||
@ -72,6 +74,9 @@ export function buildFormData(params: {
|
||||
if (taskNo) {
|
||||
formData.append('taskNo', taskNo)
|
||||
}
|
||||
if (dingtalkApprovalNo) {
|
||||
formData.append('dingtalkApprovalNo', dingtalkApprovalNo)
|
||||
}
|
||||
|
||||
// 统一处理renderer字段:根据素材类型判断是否需要传递
|
||||
const rendererInfo = shouldPassRenderer(materialType, renderer)
|
||||
|
||||
39
lib/__tests__/dingtalk-approval-no.test.ts
Normal file
39
lib/__tests__/dingtalk-approval-no.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {
|
||||
DINGTALK_APPROVAL_NO_MAX_LENGTH,
|
||||
getDingtalkApprovalNoError,
|
||||
normalizeDingtalkApprovalNo,
|
||||
validateDingtalkApprovalNo,
|
||||
} from '@/lib/dingtalk-approval-no'
|
||||
|
||||
describe('dingtalk approval number', () => {
|
||||
test('允许不填写', () => {
|
||||
expect(normalizeDingtalkApprovalNo(' ')).toBeNull()
|
||||
expect(validateDingtalkApprovalNo('')).toBe(true)
|
||||
expect(validateDingtalkApprovalNo(null)).toBe(true)
|
||||
})
|
||||
|
||||
test('接受日期前缀加任意长度数字后缀', () => {
|
||||
expect(validateDingtalkApprovalNo('202508211445000157693')).toBe(true)
|
||||
expect(validateDingtalkApprovalNo('202402291')).toBe(true)
|
||||
expect(validateDingtalkApprovalNo(' 20250821123 ')).toBe(true)
|
||||
})
|
||||
|
||||
test('拒绝非数字内容和缺少后续编号', () => {
|
||||
expect(getDingtalkApprovalNoError('20250821A1')).toBe('钉钉审批编号只能输入数字')
|
||||
expect(getDingtalkApprovalNoError('20250821')).toBe('钉钉审批编号需包含8位日期和后续编号')
|
||||
})
|
||||
|
||||
test('拒绝无效日期前缀', () => {
|
||||
expect(getDingtalkApprovalNoError('20250230123')).toBe('钉钉审批编号前8位不是有效日期')
|
||||
expect(getDingtalkApprovalNoError('20230229123')).toBe('钉钉审批编号前8位不是有效日期')
|
||||
expect(getDingtalkApprovalNoError('20251301123')).toBe('钉钉审批编号前8位不是有效日期')
|
||||
})
|
||||
|
||||
test('限制异常超长输入', () => {
|
||||
const value = `20250821${'1'.repeat(DINGTALK_APPROVAL_NO_MAX_LENGTH - 7)}`
|
||||
expect(value).toHaveLength(DINGTALK_APPROVAL_NO_MAX_LENGTH + 1)
|
||||
expect(getDingtalkApprovalNoError(value)).toBe(
|
||||
`钉钉审批编号不能超过${DINGTALK_APPROVAL_NO_MAX_LENGTH}位`,
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -63,6 +63,7 @@ describe('material-update shared rules', () => {
|
||||
renderer: 'CR',
|
||||
authorName: '旧作者',
|
||||
taskNo: 'TASK202601141713525624',
|
||||
dingtalkApprovalNo: null,
|
||||
taskDetailRequesterUserId: null,
|
||||
taskDetailRequesterName: null,
|
||||
isFullSet: true,
|
||||
@ -684,6 +685,7 @@ describe('material-update shared rules', () => {
|
||||
authorName: '任务作者',
|
||||
userId: 'user-1',
|
||||
taskNo: 'task202603310001',
|
||||
dingtalkApprovalNo: '202508211445000157693',
|
||||
},
|
||||
})
|
||||
|
||||
@ -695,11 +697,65 @@ describe('material-update shared rules', () => {
|
||||
categoryName: '通用',
|
||||
sceneName: '通用',
|
||||
taskNo: 'TASK202603310001',
|
||||
dingtalkApprovalNo: '202508211445000157693',
|
||||
materialType: '任务-其他',
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
test('任务素材编辑时应拒绝无效的钉钉审批编号', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
...baseMaterial,
|
||||
materialType: '任务-页面',
|
||||
})
|
||||
|
||||
const result = await updateMaterialWithSharedRules({
|
||||
materialId: 'material-1',
|
||||
operatorUserId: 'user-1',
|
||||
operatorRole: 'USER',
|
||||
input: {
|
||||
productModel: 'SPU-001',
|
||||
categoryName: '店铺A',
|
||||
sceneName: '场景A',
|
||||
materialType: '任务-页面',
|
||||
renderer: '',
|
||||
authorName: '任务作者',
|
||||
dingtalkApprovalNo: '20250230123',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
error: '钉钉审批编号前8位不是有效日期',
|
||||
status: 400,
|
||||
})
|
||||
expect(prisma.material.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('非任务素材不能写入钉钉审批编号', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue(baseMaterial)
|
||||
|
||||
const result = await updateMaterialWithSharedRules({
|
||||
materialId: 'material-1',
|
||||
operatorUserId: 'user-1',
|
||||
operatorRole: 'USER',
|
||||
input: {
|
||||
productModel: 'SPU-001',
|
||||
categoryName: '店铺A',
|
||||
sceneName: '场景A',
|
||||
materialType: '3D场景',
|
||||
renderer: 'CR',
|
||||
authorName: '作者',
|
||||
dingtalkApprovalNo: '202508211445000157693',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
error: '钉钉审批编号仅适用于“任务-”素材',
|
||||
status: 400,
|
||||
})
|
||||
expect(prisma.material.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('任务-详情编辑时应按已有资产自动回写 isFullSet', async () => {
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
...baseMaterial,
|
||||
|
||||
@ -7,6 +7,7 @@ import { compressImage, sanitizeFileName } from '@/lib/image-processor'
|
||||
import { createMaterialLog } from '@/lib/material-log'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { uploadPublicDisplayToS3 } from '@/lib/public-display'
|
||||
import { assignTaskNoOnComplete } from '@/lib/product-package'
|
||||
|
||||
jest.mock('next-auth', () => ({
|
||||
getServerSession: jest.fn(),
|
||||
@ -83,6 +84,7 @@ jest.mock('@/lib/public-display', () => ({
|
||||
jest.mock('@/lib/product-package', () => ({
|
||||
validateTaskNo: jest.fn(() => true),
|
||||
isTaskNoExists: jest.fn().mockResolvedValue(false),
|
||||
assignTaskNoOnComplete: jest.fn().mockResolvedValue(null),
|
||||
purgeDeletedMaterialsByTaskNo: jest.fn().mockResolvedValue({
|
||||
deletedMaterials: 0,
|
||||
deletedFiles: 0,
|
||||
@ -143,6 +145,7 @@ describe('materials upload route - 任务-其他', () => {
|
||||
const formData = new FormData()
|
||||
formData.append('materialType', '任务-其他')
|
||||
formData.append('taskNo', 'TASK202603310001')
|
||||
formData.append('dingtalkApprovalNo', '202508211445000157693')
|
||||
formData.append('productModel', '')
|
||||
formData.append('categoryName', '')
|
||||
formData.append('sceneName', '')
|
||||
@ -193,6 +196,18 @@ describe('materials upload route - 任务-其他', () => {
|
||||
expect(prisma.material.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('钉钉审批编号日期无效时应在创建素材前返回失败', async () => {
|
||||
const formData = buildTaskOtherFormData()
|
||||
formData.set('dingtalkApprovalNo', '20250230123')
|
||||
|
||||
const response = await POST(createRequest(formData))
|
||||
const body = await response.json()
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(body.error).toBe('钉钉审批编号前8位不是有效日期')
|
||||
expect(prisma.material.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('正常上传时应自动补通用店铺和场景,并保留原始压缩包文件名', async () => {
|
||||
const formData = buildTaskOtherFormData()
|
||||
|
||||
@ -226,14 +241,10 @@ describe('materials upload route - 任务-其他', () => {
|
||||
]),
|
||||
}),
|
||||
)
|
||||
expect(prisma.material.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: 'material-task-other-1' },
|
||||
data: expect.objectContaining({
|
||||
taskNo: 'TASK202603310001',
|
||||
taskStatus: 'completed',
|
||||
}),
|
||||
}),
|
||||
expect(assignTaskNoOnComplete).toHaveBeenCalledWith(
|
||||
'material-task-other-1',
|
||||
'TASK202603310001',
|
||||
'202508211445000157693',
|
||||
)
|
||||
expect(createMaterialLog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
import { createMaterialLog } from '@/lib/material-log'
|
||||
import { createZipFromFiles } from '@/lib/task-material-processor'
|
||||
import { getOwnedUploadSession } from '@/lib/upload-sessions'
|
||||
import { assignTaskNoOnComplete } from '@/lib/product-package'
|
||||
|
||||
jest.mock('@/lib/prisma', () => ({
|
||||
prisma: {
|
||||
@ -114,6 +115,7 @@ describe('product-package-completion', () => {
|
||||
const result = await completeProductPackageUpload({
|
||||
materialId: 'material-1',
|
||||
taskNo: null,
|
||||
dingtalkApprovalNo: '202508211445000157693',
|
||||
operatorUserId: 'user-1',
|
||||
assets: {
|
||||
main: [
|
||||
@ -148,8 +150,14 @@ describe('product-package-completion', () => {
|
||||
expect(result).toEqual({
|
||||
materialId: 'material-1',
|
||||
taskNo: null,
|
||||
dingtalkApprovalNo: '202508211445000157693',
|
||||
isFullSet: true,
|
||||
})
|
||||
expect(assignTaskNoOnComplete).toHaveBeenCalledWith(
|
||||
'material-1',
|
||||
null,
|
||||
'202508211445000157693',
|
||||
)
|
||||
expect(queueTaskDetailFullSetNotification).toHaveBeenCalledWith('material-1')
|
||||
expect(queueTaskDetailBarcodeImagesUpdatedNotification).toHaveBeenCalledWith('material-1', [
|
||||
'sku-preview-1',
|
||||
@ -231,6 +239,7 @@ describe('product-package-completion', () => {
|
||||
).resolves.toEqual({
|
||||
materialId: 'material-1',
|
||||
taskNo: null,
|
||||
dingtalkApprovalNo: null,
|
||||
isFullSet: true,
|
||||
})
|
||||
})
|
||||
|
||||
69
lib/dingtalk-approval-no.ts
Normal file
69
lib/dingtalk-approval-no.ts
Normal file
@ -0,0 +1,69 @@
|
||||
export const DINGTALK_APPROVAL_NO_MAX_LENGTH = 64
|
||||
|
||||
function isLeapYear(year: number): boolean {
|
||||
return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
|
||||
}
|
||||
|
||||
function getDaysInMonth(year: number, month: number): number {
|
||||
if (month === 2) {
|
||||
return isLeapYear(year) ? 29 : 28
|
||||
}
|
||||
|
||||
return [4, 6, 9, 11].includes(month) ? 30 : 31
|
||||
}
|
||||
|
||||
function hasValidDatePrefix(value: string): boolean {
|
||||
const year = Number.parseInt(value.slice(0, 4), 10)
|
||||
const month = Number.parseInt(value.slice(4, 6), 10)
|
||||
const day = Number.parseInt(value.slice(6, 8), 10)
|
||||
|
||||
if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
return day <= getDaysInMonth(year, month)
|
||||
}
|
||||
|
||||
export function normalizeDingtalkApprovalNo(
|
||||
value: string | null | undefined,
|
||||
): string | null {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = value.trim()
|
||||
return normalized || null
|
||||
}
|
||||
|
||||
export function getDingtalkApprovalNoError(
|
||||
value: string | null | undefined,
|
||||
): string | null {
|
||||
const normalized = normalizeDingtalkApprovalNo(value)
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(normalized)) {
|
||||
return '钉钉审批编号只能输入数字'
|
||||
}
|
||||
|
||||
if (normalized.length < 9) {
|
||||
return '钉钉审批编号需包含8位日期和后续编号'
|
||||
}
|
||||
|
||||
if (normalized.length > DINGTALK_APPROVAL_NO_MAX_LENGTH) {
|
||||
return `钉钉审批编号不能超过${DINGTALK_APPROVAL_NO_MAX_LENGTH}位`
|
||||
}
|
||||
|
||||
if (!hasValidDatePrefix(normalized)) {
|
||||
return '钉钉审批编号前8位不是有效日期'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function validateDingtalkApprovalNo(
|
||||
value: string | null | undefined,
|
||||
): boolean {
|
||||
return getDingtalkApprovalNoError(value) === null
|
||||
}
|
||||
@ -169,6 +169,7 @@ export async function buildMaterialSearchOrConditions(search: string): Promise<a
|
||||
{ productModel: { contains: keyword, mode: 'insensitive' } },
|
||||
{ reportTitle: { contains: keyword, mode: 'insensitive' } },
|
||||
{ taskNo: { contains: keyword, mode: 'insensitive' } },
|
||||
{ dingtalkApprovalNo: { contains: keyword, mode: 'insensitive' } },
|
||||
{ categoryName: { contains: keyword, mode: 'insensitive' } },
|
||||
{ sceneName: { contains: keyword, mode: 'insensitive' } },
|
||||
{ authorName: { contains: keyword, mode: 'insensitive' } },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { is3DOriginalType, isProductPackageType } from './material-type-mapper'
|
||||
import { isProductPackageType, mapMaterialType } from './material-type-mapper'
|
||||
import {
|
||||
DOUYIN_ACCOUNT_MAINTENANCE_VIDEO_TYPE,
|
||||
DOUYIN_QIANCHUAN_VIDEO_TYPE,
|
||||
@ -57,7 +57,7 @@ export function getSelectableMaterialTypeOptions(currentType?: string | null): s
|
||||
}
|
||||
|
||||
export function isTaskMaterialType(materialType: string): boolean {
|
||||
return isProductPackageType(materialType) || is3DOriginalType(materialType)
|
||||
return mapMaterialType(materialType).startsWith('任务-')
|
||||
}
|
||||
|
||||
export function supportsTaskNo(materialType: string): boolean {
|
||||
|
||||
@ -23,6 +23,10 @@ import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set'
|
||||
import { queueTaskDetailFullSetNotification } from '@/lib/external-auto-listing'
|
||||
import { normalizeTaskDetailSourceType } from '@/lib/task-detail-source-type'
|
||||
import { isCertificateReportMaterialType, normalizeReportTitle } from '@/lib/certificate-report'
|
||||
import {
|
||||
getDingtalkApprovalNoError,
|
||||
normalizeDingtalkApprovalNo,
|
||||
} from '@/lib/dingtalk-approval-no'
|
||||
|
||||
export interface MaterialUpdateInput {
|
||||
productModel: string
|
||||
@ -36,6 +40,7 @@ export interface MaterialUpdateInput {
|
||||
authorName: string
|
||||
userId?: string | null
|
||||
taskNo?: string | null
|
||||
dingtalkApprovalNo?: string | null
|
||||
taskDetailSourceType?: string | null
|
||||
taskDetailRequesterUserId?: string | null
|
||||
// 兼容旧调用方传参,实际保存时会按资产自动计算
|
||||
@ -172,6 +177,7 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
||||
|
||||
const material = editable.material as any
|
||||
const hasTaskNoField = Object.prototype.hasOwnProperty.call(input, 'taskNo')
|
||||
const hasDingtalkApprovalNoField = Object.prototype.hasOwnProperty.call(input, 'dingtalkApprovalNo')
|
||||
const hasTaskDetailSourceTypeField = Object.prototype.hasOwnProperty.call(input, 'taskDetailSourceType')
|
||||
const hasTaskDetailRequesterField = Object.prototype.hasOwnProperty.call(input, 'taskDetailRequesterUserId')
|
||||
const productModel = normalizeString(input.productModel)
|
||||
@ -184,6 +190,7 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
||||
const platformName = normalizeOptionalString(input.platformName)
|
||||
const productCategoryName = normalizeOptionalString(input.productCategoryName)
|
||||
const taskNo = normalizeTaskNo(input.taskNo)
|
||||
const dingtalkApprovalNo = normalizeDingtalkApprovalNo(input.dingtalkApprovalNo)
|
||||
const rawTaskDetailSourceType = input.taskDetailSourceType
|
||||
const taskDetailSourceType = normalizeTaskDetailSourceType(rawTaskDetailSourceType)
|
||||
const requestedTaskDetailRequesterUserId = normalizeOptionalString(input.taskDetailRequesterUserId)
|
||||
@ -219,6 +226,21 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
||||
}
|
||||
}
|
||||
|
||||
if (dingtalkApprovalNo && !materialType.startsWith('任务-')) {
|
||||
return {
|
||||
error: '钉钉审批编号仅适用于“任务-”素材',
|
||||
status: 400 as const,
|
||||
}
|
||||
}
|
||||
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
return {
|
||||
error: dingtalkApprovalNoError,
|
||||
status: 400 as const,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
hasTaskDetailSourceTypeField &&
|
||||
rawTaskDetailSourceType != null &&
|
||||
@ -330,6 +352,9 @@ export async function updateMaterialWithSharedRules(options: UpdateMaterialOptio
|
||||
? normalizeOptionalString(input.renderer) || material.renderer || getDefaultRenderer(materialType)
|
||||
: material.renderer,
|
||||
taskNo: hasTaskNoField ? taskNo : material.taskNo,
|
||||
dingtalkApprovalNo: materialType.startsWith('任务-')
|
||||
? (hasDingtalkApprovalNoField ? dingtalkApprovalNo : material.dingtalkApprovalNo)
|
||||
: null,
|
||||
taskDetailSourceType:
|
||||
materialType === '任务-详情'
|
||||
? (hasTaskDetailSourceTypeField ? taskDetailSourceType : material.taskDetailSourceType ?? null)
|
||||
|
||||
@ -22,6 +22,10 @@ import {
|
||||
} from '@/lib/external-auto-listing'
|
||||
import path from 'path'
|
||||
import { mkdir, readFile, writeFile } from 'fs/promises'
|
||||
import {
|
||||
getDingtalkApprovalNoError,
|
||||
normalizeDingtalkApprovalNo,
|
||||
} from '@/lib/dingtalk-approval-no'
|
||||
|
||||
export interface ProductPackageImageInput {
|
||||
uploadId?: string
|
||||
@ -52,6 +56,7 @@ export interface ProductPackageAssetsInput {
|
||||
export interface CompleteProductPackageUploadInput {
|
||||
materialId: string
|
||||
taskNo?: string | null
|
||||
dingtalkApprovalNo?: string | null
|
||||
operatorUserId: string
|
||||
assets: ProductPackageAssetsInput
|
||||
options?: {
|
||||
@ -462,7 +467,7 @@ async function movePsdFilesToPermanentDir(
|
||||
}
|
||||
|
||||
export async function completeProductPackageUpload(input: CompleteProductPackageUploadInput) {
|
||||
const { materialId, taskNo, assets, operatorUserId, options } = input
|
||||
const { materialId, taskNo, dingtalkApprovalNo, assets, operatorUserId, options } = input
|
||||
const tempFiles: string[] = []
|
||||
const uploadIds: string[] = []
|
||||
const uploadedPreviewUrls: string[] = []
|
||||
@ -494,6 +499,11 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
||||
if (normalizedTaskNo && !validateTaskNo(normalizedTaskNo)) {
|
||||
throw new Error('任务号格式错误')
|
||||
}
|
||||
const normalizedDingtalkApprovalNo = normalizeDingtalkApprovalNo(dingtalkApprovalNo)
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(normalizedDingtalkApprovalNo)
|
||||
if (dingtalkApprovalNoError) {
|
||||
throw new Error(dingtalkApprovalNoError)
|
||||
}
|
||||
|
||||
try {
|
||||
const material: any = await prisma.material.findUnique({
|
||||
@ -595,7 +605,11 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
||||
|
||||
const cardPreviewUrl = previewAssets.main[0]?.fileUrl || material.previewUrl
|
||||
|
||||
await assignTaskNoOnComplete(materialId, normalizedTaskNo)
|
||||
await assignTaskNoOnComplete(
|
||||
materialId,
|
||||
normalizedTaskNo,
|
||||
normalizedDingtalkApprovalNo,
|
||||
)
|
||||
await prisma.material.update({
|
||||
where: { id: materialId },
|
||||
data: {
|
||||
@ -619,6 +633,10 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
||||
oldValue: null,
|
||||
newValue: normalizedTaskNo,
|
||||
},
|
||||
dingtalkApprovalNo: {
|
||||
oldValue: null,
|
||||
newValue: normalizedDingtalkApprovalNo,
|
||||
},
|
||||
isFullSet: {
|
||||
oldValue: false,
|
||||
newValue: derivedIsFullSet,
|
||||
@ -658,6 +676,7 @@ export async function completeProductPackageUpload(input: CompleteProductPackage
|
||||
return {
|
||||
materialId,
|
||||
taskNo: normalizedTaskNo,
|
||||
dingtalkApprovalNo: normalizedDingtalkApprovalNo,
|
||||
isFullSet: derivedIsFullSet,
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
|
||||
import { prisma } from './prisma';
|
||||
import path from 'path';
|
||||
import {
|
||||
getDingtalkApprovalNoError,
|
||||
normalizeDingtalkApprovalNo,
|
||||
} from './dingtalk-approval-no';
|
||||
|
||||
// 任务号格式:TASK + 12-18位数字 + 可选的多个子任务号(-2位数字,可重复)
|
||||
// 示例:
|
||||
@ -318,21 +322,32 @@ export async function purgeDeletedMaterialsByTaskNo(taskNo: string): Promise<{
|
||||
}
|
||||
|
||||
/**
|
||||
* 在素材成功完成后再写入任务号,避免失败中的半成品占用任务号
|
||||
* 在素材成功完成后再写入任务关联字段,避免失败中的半成品占用任务号
|
||||
*/
|
||||
export async function assignTaskNoOnComplete(
|
||||
materialId: string,
|
||||
taskNo?: string | null,
|
||||
dingtalkApprovalNo?: string | null,
|
||||
): Promise<{
|
||||
deletedMaterials: number;
|
||||
deletedFiles: number;
|
||||
failedFiles: number;
|
||||
} | null> {
|
||||
const normalizedTaskNo = taskNo?.trim();
|
||||
const hasDingtalkApprovalNo = dingtalkApprovalNo !== undefined;
|
||||
const normalizedDingtalkApprovalNo = normalizeDingtalkApprovalNo(dingtalkApprovalNo);
|
||||
const dingtalkApprovalNoError = getDingtalkApprovalNoError(normalizedDingtalkApprovalNo);
|
||||
if (dingtalkApprovalNoError) {
|
||||
throw new Error(dingtalkApprovalNoError);
|
||||
}
|
||||
|
||||
if (!normalizedTaskNo) {
|
||||
await prisma.material.update({
|
||||
where: { id: materialId },
|
||||
data: {
|
||||
...(hasDingtalkApprovalNo
|
||||
? { dingtalkApprovalNo: normalizedDingtalkApprovalNo }
|
||||
: {}),
|
||||
taskStatus: TASK_STATUS.COMPLETED,
|
||||
} as any,
|
||||
});
|
||||
@ -368,6 +383,9 @@ export async function assignTaskNoOnComplete(
|
||||
where: { id: materialId },
|
||||
data: {
|
||||
taskNo: normalizedTaskNo,
|
||||
...(hasDingtalkApprovalNo
|
||||
? { dingtalkApprovalNo: normalizedDingtalkApprovalNo }
|
||||
: {}),
|
||||
taskStatus: TASK_STATUS.COMPLETED,
|
||||
} as any,
|
||||
});
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "materials"
|
||||
ADD COLUMN "dingtalk_approval_no" TEXT;
|
||||
|
||||
CREATE INDEX "materials_dingtalk_approval_no_idx"
|
||||
ON "materials"("dingtalk_approval_no");
|
||||
@ -98,6 +98,7 @@ model Material {
|
||||
transferredBy String?
|
||||
transferNote String?
|
||||
taskNo String? @map("task_no")
|
||||
dingtalkApprovalNo String? @map("dingtalk_approval_no")
|
||||
taskStatus String? @map("task_status")
|
||||
taskFetchedAt DateTime? @map("task_fetched_at")
|
||||
isFullSet Boolean @default(false) @map("is_full_set")
|
||||
@ -130,6 +131,7 @@ model Material {
|
||||
@@index([originalOwnerId])
|
||||
@@index([transferredAt])
|
||||
@@index([taskNo])
|
||||
@@index([dingtalkApprovalNo])
|
||||
@@index([taskStatus])
|
||||
@@index([isFullSet])
|
||||
@@index([productModel])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user