✅ 任务1: 完善模板验证工具 - 添加详细的错误代码和错误信息 - 添加性能监控(记录验证耗时) - 添加模板状态检查 - 添加批量验证功能 - 添加结构化日志记录 ✅ 任务2: 优化上传API的模板验证 - 集成增强的validateTemplateId函数 - 添加详细的验证日志 - 返回错误代码便于前端处理 ✅ 任务3: 增强字段池同步机制 - 添加增量同步支持 - 添加同步失败重试机制(指数退避) - 添加详细的同步统计信息 - 添加同步日志和监控 - 优化错误处理 相关需求: 1.1-1.4, 3.1-3.4, 6.1-6.4, 24.1-24.5
472 lines
12 KiB
TypeScript
472 lines
12 KiB
TypeScript
/**
|
||
* 模板验证工具
|
||
*
|
||
* 提供统一的模板验证逻辑,支持内置模板和自定义模板
|
||
*
|
||
* @module template-validator
|
||
* @created 2025-12-13
|
||
*/
|
||
|
||
import { prisma } from '@/lib/prisma'
|
||
|
||
// 内置模板key列表
|
||
export const BUILTIN_TEMPLATE_KEYS = [
|
||
'3dmax-material',
|
||
'psd-material',
|
||
'ai-material',
|
||
'video-material',
|
||
'model-material',
|
||
'other-material',
|
||
] as const
|
||
|
||
export type BuiltinTemplateKey = typeof BUILTIN_TEMPLATE_KEYS[number]
|
||
|
||
/**
|
||
* 模板状态枚举
|
||
*/
|
||
export enum TemplateStatus {
|
||
DRAFT = 'draft',
|
||
ACTIVE = 'active',
|
||
ARCHIVED = 'archived',
|
||
}
|
||
|
||
/**
|
||
* 模板验证结果
|
||
*/
|
||
export type TemplateValidationResult = {
|
||
valid: boolean
|
||
isBuiltin: boolean
|
||
templateKey?: string
|
||
templateName?: string
|
||
templateStatus?: string
|
||
error?: string
|
||
errorCode?: string
|
||
validationTime?: number
|
||
}
|
||
|
||
/**
|
||
* 模板验证错误代码
|
||
*/
|
||
export enum TemplateValidationError {
|
||
INVALID_BUILTIN_KEY = 'INVALID_BUILTIN_KEY',
|
||
TEMPLATE_NOT_FOUND = 'TEMPLATE_NOT_FOUND',
|
||
TEMPLATE_NOT_ACTIVE = 'TEMPLATE_NOT_ACTIVE',
|
||
DATABASE_ERROR = 'DATABASE_ERROR',
|
||
}
|
||
|
||
/**
|
||
* 验证模板ID是否有效
|
||
*
|
||
* @param templateId - 模板ID(可以是内置模板或自定义模板)
|
||
* @param options - 验证选项
|
||
* @returns 验证结果
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const result = await validateTemplateId('builtin-video-material')
|
||
* if (!result.valid) {
|
||
* console.error(result.error)
|
||
* }
|
||
* ```
|
||
*/
|
||
export async function validateTemplateId(
|
||
templateId: string | null | undefined,
|
||
options: {
|
||
checkStatus?: boolean // 是否检查模板状态,默认true
|
||
logPerformance?: boolean // 是否记录性能日志,默认true
|
||
} = {}
|
||
): Promise<TemplateValidationResult> {
|
||
const { checkStatus = true, logPerformance = true } = options
|
||
const startTime = Date.now()
|
||
|
||
// 如果没有提供 templateId,视为有效(向后兼容)
|
||
if (!templateId) {
|
||
const result: TemplateValidationResult = {
|
||
valid: true,
|
||
isBuiltin: false,
|
||
validationTime: Date.now() - startTime,
|
||
}
|
||
|
||
if (logPerformance) {
|
||
logValidation('empty', result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 检查是否为内置模板(builtin-* 格式)
|
||
const isBuiltin = templateId.startsWith('builtin-')
|
||
|
||
if (isBuiltin) {
|
||
// 内置模板:验证模板key是否在预设列表中
|
||
const templateKey = templateId.replace('builtin-', '') as BuiltinTemplateKey
|
||
|
||
if (!BUILTIN_TEMPLATE_KEYS.includes(templateKey)) {
|
||
const result: TemplateValidationResult = {
|
||
valid: false,
|
||
isBuiltin: true,
|
||
templateKey,
|
||
error: `无效的内置模板:${templateId}。有效的内置模板包括:${BUILTIN_TEMPLATE_KEYS.map(k => `builtin-${k}`).join(', ')}`,
|
||
errorCode: TemplateValidationError.INVALID_BUILTIN_KEY,
|
||
validationTime: Date.now() - startTime,
|
||
}
|
||
|
||
if (logPerformance) {
|
||
logValidation(templateId, result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 获取内置模板名称
|
||
const { BUILTIN_TEMPLATE_NAMES } = await import('@/lib/upload-template-presets')
|
||
const templateName = BUILTIN_TEMPLATE_NAMES[templateKey]
|
||
|
||
const result: TemplateValidationResult = {
|
||
valid: true,
|
||
isBuiltin: true,
|
||
templateKey,
|
||
templateName,
|
||
templateStatus: TemplateStatus.ACTIVE,
|
||
validationTime: Date.now() - startTime,
|
||
}
|
||
|
||
if (logPerformance) {
|
||
logValidation(templateId, result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 自定义模板:从数据库验证
|
||
try {
|
||
const template = await prisma.uploadTemplate.findUnique({
|
||
where: { id: templateId },
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
status: true,
|
||
},
|
||
})
|
||
|
||
if (!template) {
|
||
const result: TemplateValidationResult = {
|
||
valid: false,
|
||
isBuiltin: false,
|
||
error: `自定义模板不存在:${templateId}。请检查模板是否已被删除或ID是否正确。`,
|
||
errorCode: TemplateValidationError.TEMPLATE_NOT_FOUND,
|
||
validationTime: Date.now() - startTime,
|
||
}
|
||
|
||
if (logPerformance) {
|
||
logValidation(templateId, result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// 检查模板状态
|
||
if (checkStatus && template.status !== TemplateStatus.ACTIVE) {
|
||
const result: TemplateValidationResult = {
|
||
valid: false,
|
||
isBuiltin: false,
|
||
templateName: template.name,
|
||
templateStatus: template.status,
|
||
error: `模板"${template.name}"当前状态为"${template.status}",无法使用。请联系管理员启用该模板。`,
|
||
errorCode: TemplateValidationError.TEMPLATE_NOT_ACTIVE,
|
||
validationTime: Date.now() - startTime,
|
||
}
|
||
|
||
if (logPerformance) {
|
||
logValidation(templateId, result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
const result: TemplateValidationResult = {
|
||
valid: true,
|
||
isBuiltin: false,
|
||
templateName: template.name,
|
||
templateStatus: template.status,
|
||
validationTime: Date.now() - startTime,
|
||
}
|
||
|
||
if (logPerformance) {
|
||
logValidation(templateId, result)
|
||
}
|
||
|
||
return result
|
||
} catch (error) {
|
||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||
console.error('[TemplateValidator] Error checking custom template:', {
|
||
templateId,
|
||
error: errorMessage,
|
||
stack: error instanceof Error ? error.stack : undefined,
|
||
})
|
||
|
||
const result: TemplateValidationResult = {
|
||
valid: false,
|
||
isBuiltin: false,
|
||
error: `验证模板时发生错误:${errorMessage}`,
|
||
errorCode: TemplateValidationError.DATABASE_ERROR,
|
||
validationTime: Date.now() - startTime,
|
||
}
|
||
|
||
if (logPerformance) {
|
||
logValidation(templateId, result)
|
||
}
|
||
|
||
return result
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 记录验证日志
|
||
*
|
||
* @param templateId - 模板ID
|
||
* @param result - 验证结果
|
||
*/
|
||
function logValidation(templateId: string, result: TemplateValidationResult): void {
|
||
const logLevel = result.valid ? 'info' : 'warn'
|
||
const logData = {
|
||
timestamp: new Date().toISOString(),
|
||
module: 'template-validator',
|
||
action: 'validate',
|
||
templateId,
|
||
valid: result.valid,
|
||
isBuiltin: result.isBuiltin,
|
||
templateKey: result.templateKey,
|
||
templateName: result.templateName,
|
||
templateStatus: result.templateStatus,
|
||
error: result.error,
|
||
errorCode: result.errorCode,
|
||
validationTime: result.validationTime,
|
||
}
|
||
|
||
if (logLevel === 'info') {
|
||
console.log(JSON.stringify(logData))
|
||
} else {
|
||
console.warn(JSON.stringify(logData))
|
||
}
|
||
|
||
// 性能警告:如果验证时间超过100ms
|
||
if (result.validationTime && result.validationTime > 100) {
|
||
console.warn(`[TemplateValidator] Slow validation detected: ${result.validationTime}ms for template ${templateId}`)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查模板ID是否为内置模板
|
||
*
|
||
* @param templateId - 模板ID
|
||
* @returns 是否为内置模板
|
||
*/
|
||
export function isBuiltinTemplate(templateId: string | null | undefined): boolean {
|
||
if (!templateId) return false
|
||
return templateId.startsWith('builtin-')
|
||
}
|
||
|
||
/**
|
||
* 从内置模板ID提取模板key
|
||
*
|
||
* @param templateId - 内置模板ID(如 builtin-video-material)
|
||
* @returns 模板key(如 video-material)
|
||
*/
|
||
export function extractBuiltinTemplateKey(templateId: string): BuiltinTemplateKey | null {
|
||
if (!isBuiltinTemplate(templateId)) return null
|
||
const key = templateId.replace('builtin-', '')
|
||
return BUILTIN_TEMPLATE_KEYS.includes(key as BuiltinTemplateKey)
|
||
? (key as BuiltinTemplateKey)
|
||
: null
|
||
}
|
||
|
||
/**
|
||
* 获取模板的完整信息(包括字段)
|
||
*
|
||
* @param templateId - 模板ID
|
||
* @returns 模板信息,如果不存在返回 null
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const template = await getTemplateWithFields('builtin-video-material')
|
||
* if (template) {
|
||
* console.log(template.fields)
|
||
* }
|
||
* ```
|
||
*/
|
||
export async function getTemplateWithFields(templateId: string) {
|
||
const startTime = Date.now()
|
||
|
||
try {
|
||
if (isBuiltinTemplate(templateId)) {
|
||
// 内置模板需要动态生成
|
||
const { createBuiltinTemplate } = await import('@/lib/upload-template-presets')
|
||
const templateKey = extractBuiltinTemplateKey(templateId)
|
||
|
||
if (!templateKey) {
|
||
console.warn(`[TemplateValidator] Invalid builtin template key: ${templateId}`)
|
||
return null
|
||
}
|
||
|
||
const template = createBuiltinTemplate(templateKey)
|
||
const duration = Date.now() - startTime
|
||
|
||
console.log(JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
module: 'template-validator',
|
||
action: 'getTemplateWithFields',
|
||
templateId,
|
||
isBuiltin: true,
|
||
fieldCount: template.fields.length,
|
||
duration,
|
||
}))
|
||
|
||
return template
|
||
}
|
||
|
||
// 自定义模板从数据库读取
|
||
const template = await prisma.uploadTemplate.findUnique({
|
||
where: { id: templateId },
|
||
include: {
|
||
fields: {
|
||
orderBy: { orderIndex: 'asc' },
|
||
},
|
||
},
|
||
})
|
||
|
||
const duration = Date.now() - startTime
|
||
|
||
if (template) {
|
||
console.log(JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
module: 'template-validator',
|
||
action: 'getTemplateWithFields',
|
||
templateId,
|
||
isBuiltin: false,
|
||
templateName: template.name,
|
||
fieldCount: template.fields.length,
|
||
duration,
|
||
}))
|
||
} else {
|
||
console.warn(`[TemplateValidator] Template not found: ${templateId}`)
|
||
}
|
||
|
||
return template
|
||
} catch (error) {
|
||
const duration = Date.now() - startTime
|
||
console.error('[TemplateValidator] Error getting template with fields:', {
|
||
templateId,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
stack: error instanceof Error ? error.stack : undefined,
|
||
duration,
|
||
})
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量验证多个模板ID
|
||
*
|
||
* @param templateIds - 模板ID数组
|
||
* @returns 验证结果数组
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const results = await validateMultipleTemplateIds([
|
||
* 'builtin-video-material',
|
||
* 'custom-template-id'
|
||
* ])
|
||
* ```
|
||
*/
|
||
export async function validateMultipleTemplateIds(
|
||
templateIds: string[]
|
||
): Promise<Map<string, TemplateValidationResult>> {
|
||
const startTime = Date.now()
|
||
const results = new Map<string, TemplateValidationResult>()
|
||
|
||
// 分离内置模板和自定义模板
|
||
const builtinIds: string[] = []
|
||
const customIds: string[] = []
|
||
|
||
for (const id of templateIds) {
|
||
if (isBuiltinTemplate(id)) {
|
||
builtinIds.push(id)
|
||
} else {
|
||
customIds.push(id)
|
||
}
|
||
}
|
||
|
||
// 验证内置模板(无需数据库查询)
|
||
for (const id of builtinIds) {
|
||
const result = await validateTemplateId(id, { logPerformance: false })
|
||
results.set(id, result)
|
||
}
|
||
|
||
// 批量查询自定义模板(减少数据库往返)
|
||
if (customIds.length > 0) {
|
||
try {
|
||
const templates = await prisma.uploadTemplate.findMany({
|
||
where: { id: { in: customIds } },
|
||
select: {
|
||
id: true,
|
||
name: true,
|
||
status: true,
|
||
},
|
||
})
|
||
|
||
const templateMap = new Map(templates.map(t => [t.id, t]))
|
||
|
||
for (const id of customIds) {
|
||
const template = templateMap.get(id)
|
||
|
||
if (!template) {
|
||
results.set(id, {
|
||
valid: false,
|
||
isBuiltin: false,
|
||
error: `自定义模板不存在:${id}`,
|
||
errorCode: TemplateValidationError.TEMPLATE_NOT_FOUND,
|
||
})
|
||
} else if (template.status !== TemplateStatus.ACTIVE) {
|
||
results.set(id, {
|
||
valid: false,
|
||
isBuiltin: false,
|
||
templateName: template.name,
|
||
templateStatus: template.status,
|
||
error: `模板"${template.name}"当前状态为"${template.status}",无法使用`,
|
||
errorCode: TemplateValidationError.TEMPLATE_NOT_ACTIVE,
|
||
})
|
||
} else {
|
||
results.set(id, {
|
||
valid: true,
|
||
isBuiltin: false,
|
||
templateName: template.name,
|
||
templateStatus: template.status,
|
||
})
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('[TemplateValidator] Error in batch validation:', error)
|
||
// 如果批量查询失败,逐个验证
|
||
for (const id of customIds) {
|
||
const result = await validateTemplateId(id, { logPerformance: false })
|
||
results.set(id, result)
|
||
}
|
||
}
|
||
}
|
||
|
||
const duration = Date.now() - startTime
|
||
console.log(JSON.stringify({
|
||
timestamp: new Date().toISOString(),
|
||
module: 'template-validator',
|
||
action: 'validateMultiple',
|
||
totalCount: templateIds.length,
|
||
builtinCount: builtinIds.length,
|
||
customCount: customIds.length,
|
||
validCount: Array.from(results.values()).filter(r => r.valid).length,
|
||
duration,
|
||
}))
|
||
|
||
return results
|
||
}
|