feat: auto-sync file list + clear processing cache
- Auto-sync file_relations on every query (files appear immediately) - Add POST /api/files/reset-cache endpoint to delete output/result files and reset status to pending for reprocessing - Add "清除处理缓存" button to all 3 file views Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ from ..config import MAX_UPLOAD_SIZE, ALLOWED_EXTENSIONS
|
|||||||
from ..services.db_schema import (
|
from ..services.db_schema import (
|
||||||
insert_file_metadata, query_file_history, query_file_stats,
|
insert_file_metadata, query_file_history, query_file_stats,
|
||||||
query_file_relations, delete_file_relations, sync_file_relations,
|
query_file_relations, delete_file_relations, sync_file_relations,
|
||||||
query_file_relations_stats,
|
query_file_relations_stats, reset_file_cache,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -301,9 +301,12 @@ async def get_file_relations(
|
|||||||
page_size: int = Query(50, ge=1, le=200),
|
page_size: int = Query(50, ge=1, le=200),
|
||||||
sort_by: Optional[str] = None,
|
sort_by: Optional[str] = None,
|
||||||
sort_order: str = "desc",
|
sort_order: str = "desc",
|
||||||
|
sync: bool = Query(True, description="Auto-sync file relations before querying"),
|
||||||
current_user: dict = Depends(get_current_user),
|
current_user: dict = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Query file relations with optional view filter."""
|
"""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,
|
items, total = query_file_relations(view=view, status=status, page=page, page_size=page_size,
|
||||||
sort_by=sort_by, sort_order=sort_order)
|
sort_by=sort_by, sort_order=sort_order)
|
||||||
return {"items": items, "total": total}
|
return {"items": items, "total": total}
|
||||||
@@ -326,6 +329,24 @@ async def sync_relations(
|
|||||||
return {"message": "文件关系表已重建"}
|
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")
|
@router.delete("/relations")
|
||||||
async def delete_relations(
|
async def delete_relations(
|
||||||
body: RelationDeleteRequest,
|
body: RelationDeleteRequest,
|
||||||
|
|||||||
@@ -653,6 +653,71 @@ def sync_file_relations():
|
|||||||
conn.close()
|
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:
|
def query_file_relations_stats() -> dict:
|
||||||
"""Get detailed file statistics for Dashboard.
|
"""Get detailed file statistics for Dashboard.
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
<el-button type="danger" :disabled="!selected.length" @click="batchDelete">
|
<el-button type="danger" :disabled="!selected.length" @click="batchDelete">
|
||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button :disabled="!selected.length" @click="resetCache">
|
||||||
|
清除处理缓存
|
||||||
|
</el-button>
|
||||||
<input
|
<input
|
||||||
ref="uploadInput"
|
ref="uploadInput"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -287,6 +290,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)
|
onMounted(loadData)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
<el-button type="danger" :disabled="!selected.length" @click="batchDelete">
|
<el-button type="danger" :disabled="!selected.length" @click="batchDelete">
|
||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button :disabled="!selected.length" @click="resetCache">
|
||||||
|
清除处理缓存
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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)
|
onMounted(loadData)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
<el-button :disabled="!selected.length" @click="batchDelete">
|
<el-button :disabled="!selected.length" @click="batchDelete">
|
||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button :disabled="!selected.length" @click="resetCache">
|
||||||
|
清除处理缓存
|
||||||
|
</el-button>
|
||||||
<el-button type="danger" @click="clearAll">
|
<el-button type="danger" @click="clearAll">
|
||||||
删除全部
|
删除全部
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -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() {
|
async function clearAll() {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm('确定清空所有 Excel 处理文件?此操作不可恢复。', '确认')
|
await ElMessageBox.confirm('确定清空所有 Excel 处理文件?此操作不可恢复。', '确认')
|
||||||
|
|||||||
Reference in New Issue
Block a user