fix: 修复模板导入功能的关键问题

修复问题:
- 修复数据库状态格式不一致导致的列表显示错误
- 修复单个导入完成后统计信息显示为0的问题
- 修复日期时间解析错误导致的panic问题
- 修复所有unwrap()调用导致的潜在崩溃

 技术改进:
- 统一使用Debug格式保存和查询import_status
- 改进日期解析支持多种格式(RFC3339和SQLite格式)
- 优化进度监控逻辑,保留最后有效统计数据
- 完善错误处理,避免锁中毒和解析错误

 功能完善:
- 模板导入完成后正确显示'已完成'状态
- 统计信息准确显示成功/失败数量
- 进度监控稳定运行,无无限轮询问题
This commit is contained in:
imeepos 2025-07-14 21:50:29 +08:00
parent 939efd70d4
commit 2d88274c3a
4 changed files with 72 additions and 35 deletions

View File

@ -478,14 +478,7 @@ impl TemplateImportService {
let conn = self.database.get_connection(); let conn = self.database.get_connection();
let conn = conn.lock().map_err(|e| anyhow!("获取数据库连接失败: {}", e))?; let conn = conn.lock().map_err(|e| anyhow!("获取数据库连接失败: {}", e))?;
let status_str = match status { let status_str = format!("{:?}", status);
ImportStatus::Pending => "pending",
ImportStatus::Parsing => "parsing",
ImportStatus::Uploading => "uploading",
ImportStatus::Processing => "processing",
ImportStatus::Completed => "completed",
ImportStatus::Failed => "failed",
};
conn.execute( conn.execute(
"UPDATE templates SET import_status = ?, updated_at = datetime('now') WHERE id = ?", "UPDATE templates SET import_status = ?, updated_at = datetime('now') WHERE id = ?",

View File

@ -399,12 +399,34 @@ impl TemplateService {
tracks: Vec::new(), // 稍后填充 tracks: Vec::new(), // 稍后填充
import_status, import_status,
source_file_path: row.get("source_file_path")?, source_file_path: row.get("source_file_path")?,
created_at: chrono::DateTime::parse_from_rfc3339(&row.get::<_, String>("created_at")?) created_at: {
.map_err(|_e| rusqlite::Error::InvalidColumnType(0, "created_at".to_string(), rusqlite::types::Type::Text))? let created_at_str: String = row.get("created_at")?;
.with_timezone(&chrono::Utc), chrono::DateTime::parse_from_rfc3339(&created_at_str)
updated_at: chrono::DateTime::parse_from_rfc3339(&row.get::<_, String>("updated_at")?) .or_else(|_| {
.map_err(|_e| rusqlite::Error::InvalidColumnType(0, "updated_at".to_string(), rusqlite::types::Type::Text))? // 尝试解析SQLite格式的日期
.with_timezone(&chrono::Utc), chrono::NaiveDateTime::parse_from_str(&created_at_str, "%Y-%m-%d %H:%M:%S")
.map(|dt| dt.and_utc().fixed_offset())
})
.map_err(|e| {
eprintln!("❌ 解析created_at失败: '{}', 错误: {}", created_at_str, e);
rusqlite::Error::InvalidColumnType(0, "created_at".to_string(), rusqlite::types::Type::Text)
})?
.with_timezone(&chrono::Utc)
},
updated_at: {
let updated_at_str: String = row.get("updated_at")?;
chrono::DateTime::parse_from_rfc3339(&updated_at_str)
.or_else(|_| {
// 尝试解析SQLite格式的日期
chrono::NaiveDateTime::parse_from_str(&updated_at_str, "%Y-%m-%d %H:%M:%S")
.map(|dt| dt.and_utc().fixed_offset())
})
.map_err(|e| {
eprintln!("❌ 解析updated_at失败: '{}', 错误: {}", updated_at_str, e);
rusqlite::Error::InvalidColumnType(0, "updated_at".to_string(), rusqlite::types::Type::Text)
})?
.with_timezone(&chrono::Utc)
},
is_active: row.get("is_active")?, is_active: row.get("is_active")?,
}) })
} }
@ -447,12 +469,32 @@ impl TemplateService {
height: row.get::<_, Option<i64>>("height")?.map(|h| h as u32), height: row.get::<_, Option<i64>>("height")?.map(|h| h as u32),
upload_status, upload_status,
metadata: row.get("metadata")?, metadata: row.get("metadata")?,
created_at: chrono::DateTime::parse_from_rfc3339(&row.get::<_, String>("created_at")?) created_at: {
.unwrap() let created_at_str: String = row.get("created_at")?;
.with_timezone(&chrono::Utc), chrono::DateTime::parse_from_rfc3339(&created_at_str)
updated_at: chrono::DateTime::parse_from_rfc3339(&row.get::<_, String>("updated_at")?) .or_else(|_| {
.unwrap() chrono::NaiveDateTime::parse_from_str(&created_at_str, "%Y-%m-%d %H:%M:%S")
.with_timezone(&chrono::Utc), .map(|dt| dt.and_utc().fixed_offset())
})
.map_err(|e| {
eprintln!("❌ 解析素材created_at失败: '{}', 错误: {}", created_at_str, e);
rusqlite::Error::InvalidColumnType(0, "created_at".to_string(), rusqlite::types::Type::Text)
})?
.with_timezone(&chrono::Utc)
},
updated_at: {
let updated_at_str: String = row.get("updated_at")?;
chrono::DateTime::parse_from_rfc3339(&updated_at_str)
.or_else(|_| {
chrono::NaiveDateTime::parse_from_str(&updated_at_str, "%Y-%m-%d %H:%M:%S")
.map(|dt| dt.and_utc().fixed_offset())
})
.map_err(|e| {
eprintln!("❌ 解析素材updated_at失败: '{}', 错误: {}", updated_at_str, e);
rusqlite::Error::InvalidColumnType(0, "updated_at".to_string(), rusqlite::types::Type::Text)
})?
.with_timezone(&chrono::Utc)
},
}) })
} }

View File

@ -15,6 +15,7 @@ export const ImportProgressModal: React.FC<ImportProgressModalProps> = ({
onComplete, onComplete,
}) => { }) => {
const [progress, setProgress] = useState<ImportProgress | null>(null); const [progress, setProgress] = useState<ImportProgress | null>(null);
const [lastValidProgress, setLastValidProgress] = useState<ImportProgress | null>(null);
const { getImportProgress } = useTemplateStore(); const { getImportProgress } = useTemplateStore();
useEffect(() => { useEffect(() => {
@ -28,23 +29,24 @@ export const ImportProgressModal: React.FC<ImportProgressModalProps> = ({
const progressData = await getImportProgress(templateId); const progressData = await getImportProgress(templateId);
if (!isMounted) return; if (!isMounted) return;
// 如果有有效的进度数据,保存它并更新显示
if (progressData) {
setProgress(progressData); setProgress(progressData);
setLastValidProgress(progressData);
}
// 如果进度数据为空(已被清除),说明导入已完成,停止轮询 // 如果进度数据为空(已被清除),说明导入已完成,停止轮询
if (!progressData) { if (!progressData) {
// 设置一个完成状态的进度对象 // 使用最后一次有效的进度数据,但更新状态为完成
if (lastValidProgress) {
const completedProgress: ImportProgress = { const completedProgress: ImportProgress = {
template_id: templateId, ...lastValidProgress,
template_name: "模板",
status: ImportStatus.Completed, status: ImportStatus.Completed,
total_materials: 0,
uploaded_materials: 0,
failed_materials: 0,
current_operation: "导入完成", current_operation: "导入完成",
error_message: undefined,
progress_percentage: 100 progress_percentage: 100
}; };
setProgress(completedProgress); setProgress(completedProgress);
}
// 清除轮询 // 清除轮询
if (interval) { if (interval) {

View File

@ -45,7 +45,7 @@ const TemplateManagement: React.FC = () => {
page: currentPage, page: currentPage,
page_size: pageSize, page_size: pageSize,
}); });
console.log(response)
setTemplates(response.templates); setTemplates(response.templates);
setTotalPages(Math.ceil(response.total / pageSize)); setTotalPages(Math.ceil(response.total / pageSize));
} catch (error) { } catch (error) {