fix: 皮肤包删除后被自动迁移逻辑复活,IPA 下载连击刷屏操作日志
_migrate_old_themes 之前每次 load_config() 都会重跑,只要证书仍引用某皮肤 且旧版 themes 目录还在,删除后会被立即重新生成;现改为仅首次执行一次并 在 config.json 中打标记跳过后续调用。 打包历史下载 IPA 按钮此前无连击防护,前端加 in-flight 禁用态,后端对同一 用户同一任务的下载操作日志增加 5 秒去重,避免连击刷屏审计日志。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
84a2d3db01
commit
c17b7d5ad9
@@ -300,8 +300,14 @@ def _ensure_upload_keys(config: dict) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def _migrate_old_themes(config: dict) -> bool:
|
def _migrate_old_themes(config: dict) -> bool:
|
||||||
"""将旧式目录皮肤迁移为 ZIP 存入 data/skins/,返回是否有变更"""
|
"""将旧式目录皮肤迁移为 ZIP 存入 data/skins/,返回是否有变更。
|
||||||
changed = False
|
|
||||||
|
仅在首次执行(用旧配置升级时)生效,之后写入标记跳过:否则每次 load_config()
|
||||||
|
都会重新执行——一旦某个皮肤被删除,只要对应证书的 theme 仍引用它、且旧版
|
||||||
|
themes 目录还在,就会被立刻重新生成,导致皮肤包无法真正删除。
|
||||||
|
"""
|
||||||
|
if config.get("_legacy_themes_migrated"):
|
||||||
|
return False
|
||||||
themes_dir = AUTOMATION_DIR / "themes"
|
themes_dir = AUTOMATION_DIR / "themes"
|
||||||
for app_id, app in config.get("apps", {}).items():
|
for app_id, app in config.get("apps", {}).items():
|
||||||
upload_key = app.get("upload_key", "")
|
upload_key = app.get("upload_key", "")
|
||||||
@@ -336,8 +342,8 @@ def _migrate_old_themes(config: dict) -> bool:
|
|||||||
app["skins"] = skins
|
app["skins"] = skins
|
||||||
if theme != skin_name:
|
if theme != skin_name:
|
||||||
cert["theme"] = skin_name
|
cert["theme"] = skin_name
|
||||||
changed = True
|
config["_legacy_themes_migrated"] = True
|
||||||
return changed
|
return True
|
||||||
|
|
||||||
|
|
||||||
def load_config() -> dict:
|
def load_config() -> dict:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
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)])
|
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)
|
@router.post("", response_model=TaskResponse)
|
||||||
async def create_task(task: TaskCreate, request: Request, user: dict = Depends(get_current_user), db: Session = Depends(get_db)):
|
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:
|
except (json.JSONDecodeError, DistributionError) as exc:
|
||||||
raise HTTPException(status_code=502, detail=f"生成下载链接失败:{exc}") from 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,
|
debounce_key = f"{user.get('username')}:{task_id}"
|
||||||
resource_type="ipa", resource_name=task.oss_url,
|
now = time.monotonic()
|
||||||
detail={"controlled": True})
|
last = _recent_download_logs.get(debounce_key)
|
||||||
db.commit()
|
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}
|
return {"url": download_url, "expires_in": DOWNLOAD_URL_EXPIRE_SECONDS}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<img v-if="task.build_type === 'Ad_Hoc' && task.qr_code_path"
|
<img v-if="task.build_type === 'Ad_Hoc' && task.qr_code_path"
|
||||||
:src="task.qr_code_path" alt="QR" class="qr-thumb"
|
:src="task.qr_code_path" alt="QR" class="qr-thumb"
|
||||||
@click="showQrPreview(task)">
|
@click="showQrPreview(task)">
|
||||||
<button v-if="task.build_type === 'App_Store'" class="download-link" @click="downloadIpa(task.id)">下载 IPA</button>
|
<button v-if="task.build_type === 'App_Store'" class="download-link" :disabled="!!downloadingIds[task.id]" @click="downloadIpa(task.id)">{{ downloadingIds[task.id] ? '下载中...' : '下载 IPA' }}</button>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="text-muted">-</span>
|
<span v-else class="text-muted">-</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, nextTick, onUnmounted, inject } from 'vue'
|
import { ref, reactive, computed, onMounted, nextTick, onUnmounted, inject } from 'vue'
|
||||||
|
|
||||||
const getToken = inject('getToken', () => '')
|
const getToken = inject('getToken', () => '')
|
||||||
const isAdmin = inject('isAdmin', ref(false))
|
const isAdmin = inject('isAdmin', ref(false))
|
||||||
@@ -182,6 +182,7 @@ const logContainer = ref(null)
|
|||||||
const showVerboseLogs = ref(false)
|
const showVerboseLogs = ref(false)
|
||||||
const qrPreview = ref(null)
|
const qrPreview = ref(null)
|
||||||
const showSuperseded = ref(false)
|
const showSuperseded = ref(false)
|
||||||
|
const downloadingIds = reactive({})
|
||||||
let logWs = null
|
let logWs = null
|
||||||
|
|
||||||
const getAppVersion = (task) => {
|
const getAppVersion = (task) => {
|
||||||
@@ -313,6 +314,8 @@ const downloadDsym = (taskId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const downloadIpa = async (taskId) => {
|
const downloadIpa = async (taskId) => {
|
||||||
|
if (downloadingIds[taskId]) return
|
||||||
|
downloadingIds[taskId] = true
|
||||||
try {
|
try {
|
||||||
const res = await authFetch(`/api/tasks/${taskId}/download-link`)
|
const res = await authFetch(`/api/tasks/${taskId}/download-link`)
|
||||||
if (!res.ok) throw new Error()
|
if (!res.ok) throw new Error()
|
||||||
@@ -320,6 +323,8 @@ const downloadIpa = async (taskId) => {
|
|||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
} catch {
|
} catch {
|
||||||
alert('下载失败,请稍后重试')
|
alert('下载失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
delete downloadingIds[taskId]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +529,8 @@ const errorCategoryLabel = (cat) => {
|
|||||||
.btn-close-log:hover { border-color: #1890ff; color: #1890ff; }
|
.btn-close-log:hover { border-color: #1890ff; color: #1890ff; }
|
||||||
.download-link { color: #1890ff; text-decoration: none; font-size: 13px; border: none; background: none; cursor: pointer; padding: 0; }
|
.download-link { color: #1890ff; text-decoration: none; font-size: 13px; border: none; background: none; cursor: pointer; padding: 0; }
|
||||||
.download-link:hover { text-decoration: underline; }
|
.download-link:hover { text-decoration: underline; }
|
||||||
|
.download-link:disabled { color: #999; cursor: default; }
|
||||||
|
.download-link:disabled:hover { text-decoration: none; }
|
||||||
.text-muted { color: #999; font-size: 13px; }
|
.text-muted { color: #999; font-size: 13px; }
|
||||||
.qr-thumb { width: 40px; height: 40px; cursor: pointer; border-radius: 4px; border: 1px solid #e8e8e8; }
|
.qr-thumb { width: 40px; height: 40px; cursor: pointer; border-radius: 4px; border: 1px solid #e8e8e8; }
|
||||||
.qr-thumb:hover { border-color: #1890ff; }
|
.qr-thumb:hover { border-color: #1890ff; }
|
||||||
|
|||||||
Reference in New Issue
Block a user