diff --git a/backend/routers/config.py b/backend/routers/config.py
index 515e3e5..5abc917 100644
--- a/backend/routers/config.py
+++ b/backend/routers/config.py
@@ -300,8 +300,14 @@ def _ensure_upload_keys(config: dict) -> bool:
def _migrate_old_themes(config: dict) -> bool:
- """将旧式目录皮肤迁移为 ZIP 存入 data/skins/,返回是否有变更"""
- changed = False
+ """将旧式目录皮肤迁移为 ZIP 存入 data/skins/,返回是否有变更。
+
+ 仅在首次执行(用旧配置升级时)生效,之后写入标记跳过:否则每次 load_config()
+ 都会重新执行——一旦某个皮肤被删除,只要对应证书的 theme 仍引用它、且旧版
+ themes 目录还在,就会被立刻重新生成,导致皮肤包无法真正删除。
+ """
+ if config.get("_legacy_themes_migrated"):
+ return False
themes_dir = AUTOMATION_DIR / "themes"
for app_id, app in config.get("apps", {}).items():
upload_key = app.get("upload_key", "")
@@ -336,8 +342,8 @@ def _migrate_old_themes(config: dict) -> bool:
app["skins"] = skins
if theme != skin_name:
cert["theme"] = skin_name
- changed = True
- return changed
+ config["_legacy_themes_migrated"] = True
+ return True
def load_config() -> dict:
diff --git a/backend/routers/tasks.py b/backend/routers/tasks.py
index b0a94f9..2982b6f 100644
--- a/backend/routers/tasks.py
+++ b/backend/routers/tasks.py
@@ -3,6 +3,7 @@ import os
import json
import shutil
import tempfile
+import time
import uuid
from datetime import datetime
from typing import List
@@ -18,6 +19,10 @@ from ..deps import get_current_user
router = APIRouter(prefix="/api/tasks", tags=["tasks"], dependencies=[Depends(get_current_user)])
+# 同一用户对同一任务连击下载按钮时,短时间内只记一次操作日志,避免刷屏
+_DOWNLOAD_LOG_DEBOUNCE_SECONDS = 5
+_recent_download_logs: dict[str, float] = {}
+
@router.post("", response_model=TaskResponse)
async def create_task(task: TaskCreate, request: Request, user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
@@ -276,11 +281,22 @@ async def get_download_link(task_id: str, request: Request, user: dict = Depends
)
except (json.JSONDecodeError, DistributionError) as exc:
raise HTTPException(status_code=502, detail=f"生成下载链接失败:{exc}") from exc
- from ..services.audit_log import record_operation
- record_operation(db, user, "ipa_download_requested", request=request, task=task,
- resource_type="ipa", resource_name=task.oss_url,
- detail={"controlled": True})
- db.commit()
+
+ debounce_key = f"{user.get('username')}:{task_id}"
+ now = time.monotonic()
+ last = _recent_download_logs.get(debounce_key)
+ if last is None or now - last > _DOWNLOAD_LOG_DEBOUNCE_SECONDS:
+ if len(_recent_download_logs) > 500:
+ cutoff = now - _DOWNLOAD_LOG_DEBOUNCE_SECONDS
+ for key, ts in list(_recent_download_logs.items()):
+ if ts < cutoff:
+ del _recent_download_logs[key]
+ _recent_download_logs[debounce_key] = now
+ from ..services.audit_log import record_operation
+ record_operation(db, user, "ipa_download_requested", request=request, task=task,
+ resource_type="ipa", resource_name=task.oss_url,
+ detail={"controlled": True})
+ db.commit()
return {"url": download_url, "expires_in": DOWNLOAD_URL_EXPIRE_SECONDS}
diff --git a/frontend/src/views/HistoryView.vue b/frontend/src/views/HistoryView.vue
index 95ab8a0..26806d9 100644
--- a/frontend/src/views/HistoryView.vue
+++ b/frontend/src/views/HistoryView.vue
@@ -56,7 +56,7 @@
-
+
-
@@ -159,7 +159,7 @@