fix: 修复一键匹配失败入库和成功率超过100%的问题
## 修复内容 ### 1. 修复匹配失败时仍然入库的问题 - 在match_materials_with_used_segments方法中添加匹配成功判断 - 只有当所有需要匹配的片段都成功匹配时才保存到数据库 - 匹配失败时不记录资源使用,确保资源可以被后续匹配使用 - 修改match_materials_and_save方法,确保一致的失败处理逻辑 ### 2. 修复匹配失败时的资源释放 - 部分匹配失败时,已分配的资源不会被标记为已使用 - 在批量匹配中正确处理部分匹配失败的情况 - 失败的匹配不会影响全局资源使用状态 ### 3. 修复成功率计算超过100%的问题 - 统一所有地方的成功率计算逻辑,确保基于可匹配片段计算 - 在前端显示时添加Math.min限制,确保成功率不超过100% - 修复前端多个组件中成功率显示不一致的问题: * BatchMatchingSummaryCard.tsx * BatchMatchingResultDialog.tsx * TemplateMatchingResultCard.tsx * TemplateMatchingResultDetailModal.tsx * TemplateMatchingResultStatsPanel.tsx * materialMatchingService.ts ### 4. 改进批量匹配逻辑 - 区分完全匹配失败和部分匹配失败 - 部分匹配失败时提供详细的失败原因 - 保持匹配结果用于分析,但不保存到数据库 ## 技术细节 - 后端成功率统一为0-1的小数格式 - 前端显示时统一乘以100并限制最大值为100 - 确保匹配失败时的事务一致性 - 添加详细的日志输出便于调试
This commit is contained in:
parent
66ceaf3274
commit
483d63caaa
|
|
@ -294,22 +294,30 @@ impl MaterialMatchingService {
|
||||||
|
|
||||||
let matching_duration_ms = start_time.elapsed().as_millis() as u64;
|
let matching_duration_ms = start_time.elapsed().as_millis() as u64;
|
||||||
|
|
||||||
// 如果配置了结果保存服务,则自动保存结果
|
// 判断匹配是否成功:没有失败的片段
|
||||||
if let Some(result_service) = &self.matching_result_service {
|
let is_matching_successful = matching_result.failed_segments.is_empty();
|
||||||
match result_service.save_matching_result(
|
|
||||||
&matching_result,
|
// 只有匹配完全成功时才保存到数据库
|
||||||
result_name,
|
if is_matching_successful {
|
||||||
description,
|
// 如果配置了结果保存服务,则自动保存结果
|
||||||
matching_duration_ms,
|
if let Some(result_service) = &self.matching_result_service {
|
||||||
).await {
|
match result_service.save_matching_result(
|
||||||
Ok(saved_result) => {
|
&matching_result,
|
||||||
return Ok((matching_result, Some(saved_result)));
|
result_name,
|
||||||
}
|
description,
|
||||||
Err(e) => {
|
matching_duration_ms,
|
||||||
// 保存失败时记录错误但不影响匹配结果的返回
|
).await {
|
||||||
eprintln!("保存匹配结果失败: {}", e);
|
Ok(saved_result) => {
|
||||||
|
return Ok((matching_result, Some(saved_result)));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// 保存失败时记录错误但不影响匹配结果的返回
|
||||||
|
eprintln!("保存匹配结果失败: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
println!("⚠️ 匹配失败,不保存到数据库。失败片段数: {}", matching_result.failed_segments.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((matching_result, None))
|
Ok((matching_result, None))
|
||||||
|
|
@ -734,26 +742,55 @@ impl MaterialMatchingService {
|
||||||
|
|
||||||
match self.match_materials_with_used_segments(matching_request, result_name, &global_used_segment_ids).await {
|
match self.match_materials_with_used_segments(matching_request, result_name, &global_used_segment_ids).await {
|
||||||
Ok((matching_result, saved_result, newly_used_segments)) => {
|
Ok((matching_result, saved_result, newly_used_segments)) => {
|
||||||
round_successful_matches += 1;
|
// 检查匹配是否完全成功(没有失败的片段)
|
||||||
successful_matches += 1;
|
let is_fully_successful = matching_result.failed_segments.is_empty();
|
||||||
|
|
||||||
// 更新全局已使用片段列表
|
if is_fully_successful {
|
||||||
global_used_segment_ids.extend(newly_used_segments);
|
round_successful_matches += 1;
|
||||||
|
successful_matches += 1;
|
||||||
|
|
||||||
matching_results.push(BatchMatchingItemResult {
|
// 更新全局已使用片段列表
|
||||||
binding_id: binding_detail.binding.id.clone(),
|
global_used_segment_ids.extend(newly_used_segments);
|
||||||
template_id: binding_detail.binding.template_id.clone(),
|
|
||||||
template_name: binding_detail.template_name.clone(),
|
matching_results.push(BatchMatchingItemResult {
|
||||||
binding_name: binding_detail.binding.binding_name.clone(),
|
binding_id: binding_detail.binding.id.clone(),
|
||||||
status: BatchMatchingItemStatus::Success,
|
template_id: binding_detail.binding.template_id.clone(),
|
||||||
matching_result: Some(matching_result),
|
template_name: binding_detail.template_name.clone(),
|
||||||
saved_result_id: saved_result.map(|r| r.id),
|
binding_name: binding_detail.binding.binding_name.clone(),
|
||||||
error_message: None,
|
status: BatchMatchingItemStatus::Success,
|
||||||
duration_ms: binding_start_time.elapsed().as_millis() as u64,
|
matching_result: Some(matching_result),
|
||||||
round_number: total_rounds,
|
saved_result_id: saved_result.map(|r| r.id),
|
||||||
attempts_count: 1,
|
error_message: None,
|
||||||
failure_reason: None,
|
duration_ms: binding_start_time.elapsed().as_millis() as u64,
|
||||||
});
|
round_number: total_rounds,
|
||||||
|
attempts_count: 1,
|
||||||
|
failure_reason: None,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 部分匹配失败,视为失败
|
||||||
|
round_failed_matches += 1;
|
||||||
|
failed_matches += 1;
|
||||||
|
|
||||||
|
let failure_reason = format!("部分匹配失败:{} 个片段匹配成功,{} 个片段匹配失败",
|
||||||
|
matching_result.matches.len(),
|
||||||
|
matching_result.failed_segments.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
matching_results.push(BatchMatchingItemResult {
|
||||||
|
binding_id: binding_detail.binding.id.clone(),
|
||||||
|
template_id: binding_detail.binding.template_id.clone(),
|
||||||
|
template_name: binding_detail.template_name.clone(),
|
||||||
|
binding_name: binding_detail.binding.binding_name.clone(),
|
||||||
|
status: BatchMatchingItemStatus::Failed,
|
||||||
|
matching_result: Some(matching_result), // 仍然返回结果用于分析
|
||||||
|
saved_result_id: None, // 但不保存到数据库
|
||||||
|
error_message: Some(failure_reason.clone()),
|
||||||
|
duration_ms: binding_start_time.elapsed().as_millis() as u64,
|
||||||
|
round_number: total_rounds,
|
||||||
|
attempts_count: 1,
|
||||||
|
failure_reason: Some(failure_reason),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
round_failed_matches += 1;
|
round_failed_matches += 1;
|
||||||
|
|
@ -990,12 +1027,18 @@ impl MaterialMatchingService {
|
||||||
let matched_segments = matches.len() as u32;
|
let matched_segments = matches.len() as u32;
|
||||||
let failed_segments_count = failed_segments.len() as u32;
|
let failed_segments_count = failed_segments.len() as u32;
|
||||||
let fixed_segments_count = fixed_segments.len() as u32;
|
let fixed_segments_count = fixed_segments.len() as u32;
|
||||||
let success_rate = if total_segments > 0 {
|
|
||||||
(matched_segments + fixed_segments_count) as f64 / total_segments as f64
|
// 修正成功率计算:只考虑需要匹配的片段
|
||||||
|
let matchable_segments = total_segments - fixed_segments_count;
|
||||||
|
let success_rate = if matchable_segments > 0 {
|
||||||
|
matched_segments as f64 / matchable_segments as f64
|
||||||
} else {
|
} else {
|
||||||
0.0
|
1.0 // 如果没有需要匹配的片段,成功率为100%
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 判断匹配是否成功:所有需要匹配的片段都成功匹配
|
||||||
|
let is_matching_successful = failed_segments_count == 0;
|
||||||
|
|
||||||
// 创建匹配结果
|
// 创建匹配结果
|
||||||
let matching_result = MaterialMatchingResult {
|
let matching_result = MaterialMatchingResult {
|
||||||
binding_id: request.binding_id.clone(),
|
binding_id: request.binding_id.clone(),
|
||||||
|
|
@ -1003,7 +1046,7 @@ impl MaterialMatchingService {
|
||||||
project_id: request.project_id.clone(),
|
project_id: request.project_id.clone(),
|
||||||
matches,
|
matches,
|
||||||
statistics: MatchingStatistics {
|
statistics: MatchingStatistics {
|
||||||
total_segments,
|
total_segments: matchable_segments, // 只统计需要匹配的片段
|
||||||
matched_segments,
|
matched_segments,
|
||||||
failed_segments: failed_segments_count,
|
failed_segments: failed_segments_count,
|
||||||
success_rate,
|
success_rate,
|
||||||
|
|
@ -1013,21 +1056,30 @@ impl MaterialMatchingService {
|
||||||
failed_segments,
|
failed_segments,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存匹配结果到数据库
|
// 只有匹配完全成功时才保存到数据库和记录资源使用
|
||||||
let saved_result = if let Some(result_service) = &self.matching_result_service {
|
let (saved_result, final_used_segments) = if is_matching_successful {
|
||||||
let saved = result_service.save_matching_result(
|
// 保存匹配结果到数据库
|
||||||
&matching_result,
|
let saved_result = if let Some(result_service) = &self.matching_result_service {
|
||||||
result_name,
|
let saved = result_service.save_matching_result(
|
||||||
None,
|
&matching_result,
|
||||||
0, // 匹配耗时,这里简化为0
|
result_name,
|
||||||
).await?;
|
None,
|
||||||
|
0, // 匹配耗时,这里简化为0
|
||||||
|
).await?;
|
||||||
|
|
||||||
Some(saved)
|
Some(saved)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
(saved_result, local_used_segment_ids)
|
||||||
} else {
|
} else {
|
||||||
None
|
// 匹配失败时不保存到数据库,也不记录资源使用
|
||||||
|
println!("⚠️ 匹配失败,不保存到数据库。失败片段数: {}, 成功片段数: {}", failed_segments_count, matched_segments);
|
||||||
|
(None, HashSet::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((matching_result, saved_result, local_used_segment_ids))
|
Ok((matching_result, saved_result, final_used_segments))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 计算批量匹配汇总信息
|
/// 计算批量匹配汇总信息
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
||||||
lines.push(` 失败原因: ${item.failure_reason}`);
|
lines.push(` 失败原因: ${item.failure_reason}`);
|
||||||
}
|
}
|
||||||
if (item.matching_result) {
|
if (item.matching_result) {
|
||||||
lines.push(` 成功率: ${(item.matching_result.statistics?.success_rate * 100 || 0).toFixed(1)}%`);
|
lines.push(` 成功率: ${Math.min((item.matching_result.statistics?.success_rate * 100 || 0), 100).toFixed(1)}%`);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
});
|
});
|
||||||
|
|
@ -335,7 +335,7 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
|
||||||
<p className="text-sm text-gray-600">耗时: {formatDuration(item.duration_ms)}</p>
|
<p className="text-sm text-gray-600">耗时: {formatDuration(item.duration_ms)}</p>
|
||||||
{item.matching_result && (
|
{item.matching_result && (
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
成功率: {(item.matching_result.statistics?.success_rate * 100 || 0).toFixed(1)}%
|
成功率: {Math.min((item.matching_result.statistics?.success_rate * 100 || 0), 100).toFixed(1)}%
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ export const BatchMatchingSummaryCard: React.FC<BatchMatchingSummaryCardProps> =
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<TrendingUp className="w-4 h-4 text-gray-600" />
|
<TrendingUp className="w-4 h-4 text-gray-600" />
|
||||||
<span className="text-gray-600">平均成功率:</span>
|
<span className="text-gray-600">平均成功率:</span>
|
||||||
<span className="font-medium">{(result.summary.average_success_rate * 100).toFixed(1)}%</span>
|
<span className="font-medium">{Math.min(result.summary.average_success_rate * 100, 100).toFixed(1)}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{result.summary.best_matching_template && (
|
{result.summary.best_matching_template && (
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,8 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
|
||||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||||
{/* 成功率 */}
|
{/* 成功率 */}
|
||||||
<div className="text-center p-3 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl">
|
<div className="text-center p-3 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl">
|
||||||
<div className={`text-2xl font-bold ${getSuccessRateColor(result.success_rate)}`}>
|
<div className={`text-2xl font-bold ${getSuccessRateColor(Math.min(result.success_rate * 100, 100))}`}>
|
||||||
{result.success_rate.toFixed(1)}%
|
{Math.min(result.success_rate * 100, 100).toFixed(1)}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-600 font-medium mt-1">成功率</div>
|
<div className="text-xs text-gray-600 font-medium mt-1">成功率</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,7 @@ export const TemplateMatchingResultDetailModal: React.FC<TemplateMatchingResultD
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
||||||
<div className="text-center p-4 bg-gradient-to-br from-primary-50 to-primary-100 rounded-xl">
|
<div className="text-center p-4 bg-gradient-to-br from-primary-50 to-primary-100 rounded-xl">
|
||||||
<div className="text-2xl font-bold text-primary-700">
|
<div className="text-2xl font-bold text-primary-700">
|
||||||
{matching_result.success_rate.toFixed(1)}%
|
{Math.min(matching_result.success_rate * 100, 100).toFixed(1)}%
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-primary-600 font-medium">成功率</div>
|
<div className="text-sm text-primary-600 font-medium">成功率</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -167,8 +167,8 @@ export const TemplateMatchingResultStatsPanel: React.FC<TemplateMatchingResultSt
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex justify-between items-center py-2">
|
<div className="flex justify-between items-center py-2">
|
||||||
<span className="text-sm text-gray-600">平均成功率</span>
|
<span className="text-sm text-gray-600">平均成功率</span>
|
||||||
<span className={`text-sm font-semibold ${getSuccessRateColor(statistics.average_success_rate)}`}>
|
<span className={`text-sm font-semibold ${getSuccessRateColor(Math.min(statistics.average_success_rate * 100, 100))}`}>
|
||||||
{statistics.average_success_rate.toFixed(1)}%
|
{Math.min(statistics.average_success_rate * 100, 100).toFixed(1)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center py-2">
|
<div className="flex justify-between items-center py-2">
|
||||||
|
|
|
||||||
|
|
@ -153,14 +153,14 @@ export class MaterialMatchingService {
|
||||||
} {
|
} {
|
||||||
const { statistics } = result;
|
const { statistics } = result;
|
||||||
|
|
||||||
const summary = `匹配完成:${statistics.matched_segments}/${statistics.total_segments} 个片段 (${(statistics.success_rate * 100).toFixed(1)}%)`;
|
const summary = `匹配完成:${statistics.matched_segments}/${statistics.total_segments} 个片段 (${Math.min(statistics.success_rate * 100, 100).toFixed(1)}%)`;
|
||||||
|
|
||||||
const details = [
|
const details = [
|
||||||
`成功匹配:${statistics.matched_segments} 个片段`,
|
`成功匹配:${statistics.matched_segments} 个片段`,
|
||||||
`匹配失败:${statistics.failed_segments} 个片段`,
|
`匹配失败:${statistics.failed_segments} 个片段`,
|
||||||
`使用素材:${statistics.used_materials} 个`,
|
`使用素材:${statistics.used_materials} 个`,
|
||||||
`涉及模特:${statistics.used_models} 个`,
|
`涉及模特:${statistics.used_models} 个`,
|
||||||
`成功率:${(statistics.success_rate * 100).toFixed(1)}%`
|
`成功率:${Math.min(statistics.success_rate * 100, 100).toFixed(1)}%`
|
||||||
];
|
];
|
||||||
|
|
||||||
return { summary, details };
|
return { summary, details };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue