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:
imeepos 2025-07-18 13:16:45 +08:00
parent 66ceaf3274
commit 483d63caaa
7 changed files with 110 additions and 58 deletions

View File

@ -294,22 +294,30 @@ impl MaterialMatchingService {
let matching_duration_ms = start_time.elapsed().as_millis() as u64;
// 如果配置了结果保存服务,则自动保存结果
if let Some(result_service) = &self.matching_result_service {
match result_service.save_matching_result(
&matching_result,
result_name,
description,
matching_duration_ms,
).await {
Ok(saved_result) => {
return Ok((matching_result, Some(saved_result)));
}
Err(e) => {
// 保存失败时记录错误但不影响匹配结果的返回
eprintln!("保存匹配结果失败: {}", e);
// 判断匹配是否成功:没有失败的片段
let is_matching_successful = matching_result.failed_segments.is_empty();
// 只有匹配完全成功时才保存到数据库
if is_matching_successful {
// 如果配置了结果保存服务,则自动保存结果
if let Some(result_service) = &self.matching_result_service {
match result_service.save_matching_result(
&matching_result,
result_name,
description,
matching_duration_ms,
).await {
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))
@ -734,26 +742,55 @@ impl MaterialMatchingService {
match self.match_materials_with_used_segments(matching_request, result_name, &global_used_segment_ids).await {
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();
// 更新全局已使用片段列表
global_used_segment_ids.extend(newly_used_segments);
if is_fully_successful {
round_successful_matches += 1;
successful_matches += 1;
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::Success,
matching_result: Some(matching_result),
saved_result_id: saved_result.map(|r| r.id),
error_message: None,
duration_ms: binding_start_time.elapsed().as_millis() as u64,
round_number: total_rounds,
attempts_count: 1,
failure_reason: None,
});
// 更新全局已使用片段列表
global_used_segment_ids.extend(newly_used_segments);
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::Success,
matching_result: Some(matching_result),
saved_result_id: saved_result.map(|r| r.id),
error_message: 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) => {
round_failed_matches += 1;
@ -990,12 +1027,18 @@ impl MaterialMatchingService {
let matched_segments = matches.len() as u32;
let failed_segments_count = failed_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 {
0.0
1.0 // 如果没有需要匹配的片段成功率为100%
};
// 判断匹配是否成功:所有需要匹配的片段都成功匹配
let is_matching_successful = failed_segments_count == 0;
// 创建匹配结果
let matching_result = MaterialMatchingResult {
binding_id: request.binding_id.clone(),
@ -1003,7 +1046,7 @@ impl MaterialMatchingService {
project_id: request.project_id.clone(),
matches,
statistics: MatchingStatistics {
total_segments,
total_segments: matchable_segments, // 只统计需要匹配的片段
matched_segments,
failed_segments: failed_segments_count,
success_rate,
@ -1013,21 +1056,30 @@ impl MaterialMatchingService {
failed_segments,
};
// 保存匹配结果到数据库
let saved_result = if let Some(result_service) = &self.matching_result_service {
let saved = result_service.save_matching_result(
&matching_result,
result_name,
None,
0, // 匹配耗时这里简化为0
).await?;
// 只有匹配完全成功时才保存到数据库和记录资源使用
let (saved_result, final_used_segments) = if is_matching_successful {
// 保存匹配结果到数据库
let saved_result = if let Some(result_service) = &self.matching_result_service {
let saved = result_service.save_matching_result(
&matching_result,
result_name,
None,
0, // 匹配耗时这里简化为0
).await?;
Some(saved)
Some(saved)
} else {
None
};
(saved_result, local_used_segment_ids)
} 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))
}
/// 计算批量匹配汇总信息

View File

@ -125,7 +125,7 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
lines.push(` 失败原因: ${item.failure_reason}`);
}
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('');
});
@ -335,7 +335,7 @@ export const BatchMatchingResultDialog: React.FC<BatchMatchingResultDialogProps>
<p className="text-sm text-gray-600">: {formatDuration(item.duration_ms)}</p>
{item.matching_result && (
<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>
)}
</div>

View File

@ -145,7 +145,7 @@ export const BatchMatchingSummaryCard: React.FC<BatchMatchingSummaryCardProps> =
<div className="flex items-center space-x-2">
<TrendingUp className="w-4 h-4 text-gray-600" />
<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>
{result.summary.best_matching_template && (

View File

@ -138,8 +138,8 @@ export const TemplateMatchingResultCard: React.FC<TemplateMatchingResultCardProp
<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-2xl font-bold ${getSuccessRateColor(result.success_rate)}`}>
{result.success_rate.toFixed(1)}%
<div className={`text-2xl font-bold ${getSuccessRateColor(Math.min(result.success_rate * 100, 100))}`}>
{Math.min(result.success_rate * 100, 100).toFixed(1)}%
</div>
<div className="text-xs text-gray-600 font-medium mt-1"></div>
</div>

View File

@ -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="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">
{matching_result.success_rate.toFixed(1)}%
{Math.min(matching_result.success_rate * 100, 100).toFixed(1)}%
</div>
<div className="text-sm text-primary-600 font-medium"></div>
</div>

View File

@ -167,8 +167,8 @@ export const TemplateMatchingResultStatsPanel: React.FC<TemplateMatchingResultSt
<div className="space-y-3">
<div className="flex justify-between items-center py-2">
<span className="text-sm text-gray-600"></span>
<span className={`text-sm font-semibold ${getSuccessRateColor(statistics.average_success_rate)}`}>
{statistics.average_success_rate.toFixed(1)}%
<span className={`text-sm font-semibold ${getSuccessRateColor(Math.min(statistics.average_success_rate * 100, 100))}`}>
{Math.min(statistics.average_success_rate * 100, 100).toFixed(1)}%
</span>
</div>
<div className="flex justify-between items-center py-2">

View File

@ -153,14 +153,14 @@ export class MaterialMatchingService {
} {
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 = [
`成功匹配:${statistics.matched_segments} 个片段`,
`匹配失败:${statistics.failed_segments} 个片段`,
`使用素材:${statistics.used_materials}`,
`涉及模特:${statistics.used_models}`,
`成功率:${(statistics.success_rate * 100).toFixed(1)}%`
`成功率:${Math.min(statistics.success_rate * 100, 100).toFixed(1)}%`
];
return { summary, details };