diff --git a/web/backend/routers/files.py b/web/backend/routers/files.py index b36bc3d..7b1713c 100644 --- a/web/backend/routers/files.py +++ b/web/backend/routers/files.py @@ -15,7 +15,7 @@ from ..config import MAX_UPLOAD_SIZE, ALLOWED_EXTENSIONS from ..services.db_schema import ( insert_file_metadata, query_file_history, query_file_stats, query_file_relations, delete_file_relations, sync_file_relations, - query_file_relations_stats, + query_file_relations_stats, reset_file_cache, ) logger = logging.getLogger(__name__) @@ -301,9 +301,12 @@ async def get_file_relations( page_size: int = Query(50, ge=1, le=200), sort_by: Optional[str] = None, sort_order: str = "desc", + sync: bool = Query(True, description="Auto-sync file relations before querying"), current_user: dict = Depends(get_current_user), ): """Query file relations with optional view filter.""" + if sync: + sync_file_relations() items, total = query_file_relations(view=view, status=status, page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order) return {"items": items, "total": total} @@ -326,6 +329,24 @@ async def sync_relations( return {"message": "文件关系表已重建"} +class ResetCacheRequest(BaseModel): + files: list[dict] # [{input_image, output_excel, result_purchase}, ...] + + +@router.post("/reset-cache") +async def reset_cache( + req: ResetCacheRequest, + current_user: dict = Depends(get_current_user), +): + """Delete output/result files and reset status to pending for reprocessing. + + Each item in files should have: {input_image?, output_excel?, result_purchase?} + The corresponding files on disk are deleted, and the relation status is reset. + """ + result = reset_file_cache(req.files) + return result + + @router.delete("/relations") async def delete_relations( body: RelationDeleteRequest, diff --git a/web/backend/services/db_schema.py b/web/backend/services/db_schema.py index 5975366..ab1bd14 100644 --- a/web/backend/services/db_schema.py +++ b/web/backend/services/db_schema.py @@ -653,6 +653,71 @@ def sync_file_relations(): conn.close() +def reset_file_cache(files: list[dict]) -> dict: + """Delete output/result files and reset relation status to pending. + + Each item: {input_image?, output_excel?, result_purchase?} + Deletes the corresponding files from disk and resets status. + """ + project_root = Path(__file__).resolve().parent.parent.parent.parent + output_dir = project_root / 'data' / 'output' + result_dir = project_root / 'data' / 'result' + + deleted_files = 0 + reset_count = 0 + errors = [] + + conn = sqlite3.connect(_db_path) + conn.row_factory = sqlite3.Row + try: + for item in files: + input_image = item.get('input_image') + output_excel = item.get('output_excel') + result_purchase = item.get('result_purchase') + + # Delete output file from disk + if output_excel: + out_path = output_dir / output_excel + if out_path.exists(): + try: + out_path.unlink() + deleted_files += 1 + except Exception as e: + errors.append(f"{output_excel}: {e}") + + # Delete result file from disk + if result_purchase: + res_path = result_dir / result_purchase + if res_path.exists(): + try: + res_path.unlink() + deleted_files += 1 + except Exception as e: + errors.append(f"{result_purchase}: {e}") + + # Reset relation status to pending + if input_image: + conn.execute( + "UPDATE file_relations SET output_excel = NULL, result_purchase = NULL, " + "status = 'pending', updated_at = ? WHERE input_image = ?", + (datetime.now().isoformat(), input_image), + ) + reset_count += conn.total_changes + elif output_excel: + conn.execute( + "UPDATE file_relations SET output_excel = NULL, result_purchase = NULL, " + "status = 'pending', updated_at = ? WHERE output_excel = ?", + (datetime.now().isoformat(), output_excel), + ) + reset_count += conn.total_changes + + conn.commit() + finally: + conn.close() + + return {"deleted_files": deleted_files, "reset_relations": reset_count, "errors": errors} + + def query_file_relations_stats() -> dict: """Get detailed file statistics for Dashboard. diff --git a/web/frontend/src/views/files/Images.vue b/web/frontend/src/views/files/Images.vue index e2b6d4f..ca159f8 100644 --- a/web/frontend/src/views/files/Images.vue +++ b/web/frontend/src/views/files/Images.vue @@ -19,6 +19,9 @@ 批量删除 + + 清除处理缓存 + ({ + input_image: r.input_image, + output_excel: r.output_excel, + result_purchase: r.result_purchase, + })) + const res = await api.post('/files/reset-cache', { files }) + ElMessage.success(`已清除 ${res.data.deleted_files} 个缓存文件`) + loadData() + } catch (err: any) { + if (err !== 'cancel') ElMessage.error('清除缓存失败') + } +} + onMounted(loadData) diff --git a/web/frontend/src/views/files/Orders.vue b/web/frontend/src/views/files/Orders.vue index be290a4..e7f5f00 100644 --- a/web/frontend/src/views/files/Orders.vue +++ b/web/frontend/src/views/files/Orders.vue @@ -15,6 +15,9 @@ 批量删除 + + 清除处理缓存 + @@ -238,6 +241,22 @@ async function batchDelete() { } } +async function resetCache() { + try { + await ElMessageBox.confirm(`确定清除选中的 ${selected.value.length} 个文件的处理缓存?删除后可重新处理。`, '确认') + const files = selected.value.map(r => ({ + input_image: r.input_image, + output_excel: r.output_excel, + result_purchase: r.result_purchase, + })) + const res = await api.post('/files/reset-cache', { files }) + ElMessage.success(`已清除 ${res.data.deleted_files} 个缓存文件`) + loadData() + } catch (err: any) { + if (err !== 'cancel') ElMessage.error('清除缓存失败') + } +} + onMounted(loadData) diff --git a/web/frontend/src/views/files/Tables.vue b/web/frontend/src/views/files/Tables.vue index a35aa80..6137a71 100644 --- a/web/frontend/src/views/files/Tables.vue +++ b/web/frontend/src/views/files/Tables.vue @@ -13,6 +13,9 @@ 批量删除 + + 清除处理缓存 + 删除全部 @@ -246,6 +249,22 @@ async function batchDelete() { } } +async function resetCache() { + try { + await ElMessageBox.confirm(`确定清除选中的 ${selected.value.length} 个文件的处理缓存?删除后可重新处理。`, '确认') + const files = selected.value.map(r => ({ + input_image: r.input_image, + output_excel: r.output_excel, + result_purchase: r.result_purchase, + })) + const res = await api.post('/files/reset-cache', { files }) + ElMessage.success(`已清除 ${res.data.deleted_files} 个缓存文件`) + loadData() + } catch (err: any) { + if (err !== 'cancel') ElMessage.error('清除缓存失败') + } +} + async function clearAll() { try { await ElMessageBox.confirm('确定清空所有 Excel 处理文件?此操作不可恢复。', '确认')