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 处理文件?此操作不可恢复。', '确认')