diff --git a/backend/routers/tasks.py b/backend/routers/tasks.py
index 4538e01..cf5ca80 100644
--- a/backend/routers/tasks.py
+++ b/backend/routers/tasks.py
@@ -180,7 +180,11 @@ async def delete_task(task_id: str, db: Session = Depends(get_db)):
build_config = json.loads(task.config_json)
from .config import load_config
from ..services.distribution import DistributionError, delete_published_artifacts
- delete_published_artifacts(build_config, load_config().get("upload", {}))
+ delete_published_artifacts(
+ build_config,
+ load_config().get("upload", {}),
+ task.oss_url,
+ )
except (json.JSONDecodeError, DistributionError) as exc:
raise HTTPException(status_code=502, detail=f"远端产物删除失败,记录未删除:{exc}") from exc
diff --git a/backend/services/distribution.py b/backend/services/distribution.py
index 3c93b43..b81a3fb 100644
--- a/backend/services/distribution.py
+++ b/backend/services/distribution.py
@@ -5,7 +5,7 @@ import posixpath
import re
import shutil
from pathlib import Path
-from urllib.parse import quote
+from urllib.parse import quote, unquote, urlparse
import httpx
@@ -115,16 +115,53 @@ def _delete_oss(config: dict, remote_paths: list[str]):
raise DistributionError(f"OSS 删除失败: {relative_path}")
-def delete_published_artifacts(build_config: dict, upload_config: dict):
- """删除任务对应的远端分发产物。"""
+def _remote_paths_from_published_url(
+ build_config: dict, upload_config: dict, published_url: str,
+) -> list[str] | None:
+ """从已保存的公开链接还原实际对象键,兼容历史命名规则。"""
+ parsed = urlparse(published_url)
+ if not parsed.path:
+ return None
+
+ remote_path = unquote(parsed.path).lstrip("/")
+ # base_url 允许带路径前缀;该前缀是公开地址的一部分,不属于 OSS 对象键。
+ base_url = upload_config.get("oss", {}).get("base_url", "")
+ base = urlparse(base_url)
+ if base_url and base.netloc == parsed.netloc:
+ base_path = unquote(base.path).strip("/")
+ if base_path and remote_path.startswith(f"{base_path}/"):
+ remote_path = remote_path[len(base_path) + 1:]
+
+ path = Path(remote_path)
+ if path.suffix not in {".ipa", ".plist", ".html", ".png"}:
+ return None
+ if build_config.get("BUILD_TYPE") == "App_Store":
+ return [str(path.with_suffix(".ipa"))]
+ return [str(path.with_suffix(suffix)) for suffix in (".ipa", ".plist", ".html", ".png")]
+
+
+def delete_published_artifacts(
+ build_config: dict, upload_config: dict, published_url: str = "",
+):
+ """删除任务对应的远端分发产物。
+
+ 优先按任务保存的下载链接还原对象键。这样即使后续升级了文件命名
+ 规则,或历史快照含有新的分支字段,仍会删除当时实际上传的文件。
+ """
mode = upload_config.get("mode", "")
if mode not in {"oss", "webdav"}:
raise DistributionError("请选择 OSS 或 WebDAV 上传方式")
- ipa_remote, manifest_remote, html_remote, qr_remote = _remote_paths(build_config)
- remote_paths = [ipa_remote]
- if build_config.get("BUILD_TYPE") != "App_Store":
- remote_paths.extend([manifest_remote, html_remote, qr_remote])
+ remote_paths = None
+ if mode == "oss" and published_url:
+ remote_paths = _remote_paths_from_published_url(
+ build_config, upload_config, published_url,
+ )
+ if not remote_paths:
+ ipa_remote, manifest_remote, html_remote, qr_remote = _remote_paths(build_config)
+ remote_paths = [ipa_remote]
+ if build_config.get("BUILD_TYPE") != "App_Store":
+ remote_paths.extend([manifest_remote, html_remote, qr_remote])
deleter = _delete_oss if mode == "oss" else _delete_webdav
deleter(upload_config, remote_paths)
diff --git a/frontend/src/__tests__/HistoryView.test.js b/frontend/src/__tests__/HistoryView.test.js
index 2cc5256..b133e78 100644
--- a/frontend/src/__tests__/HistoryView.test.js
+++ b/frontend/src/__tests__/HistoryView.test.js
@@ -78,7 +78,7 @@ describe('HistoryView.vue', () => {
expect(wrapper.text()).toContain('等待中')
})
- it('按打包类型过滤', async () => {
+ it('版本号筛选为下拉选项', async () => {
const router = createMockRouter()
const wrapper = mount(HistoryView, {
global: { plugins: [router] },
@@ -86,7 +86,9 @@ describe('HistoryView.vue', () => {
await flushPromises()
const selects = wrapper.findAll('.filter-select')
- await selects[2].setValue('Ad_Hoc')
+ expect(selects).toHaveLength(3)
+ expect(wrapper.text()).not.toContain('全部类型')
+ await selects[1].setValue('2.196.0')
await wrapper.vm.$nextTick()
const rows = wrapper.findAll('tbody tr')
@@ -101,14 +103,14 @@ describe('HistoryView.vue', () => {
await flushPromises()
const selects = wrapper.findAll('.filter-select')
- await selects[3].setValue('failed')
+ await selects[2].setValue('failed')
await wrapper.vm.$nextTick()
const rows = wrapper.findAll('tbody tr')
expect(rows.length).toBe(1) // App2
})
- it('按 App 名称和版本号筛选', async () => {
+ it('按 App 名称和版本号下拉选项筛选', async () => {
const router = createMockRouter()
const wrapper = mount(HistoryView, {
global: { plugins: [router] },
@@ -117,7 +119,7 @@ describe('HistoryView.vue', () => {
const selects = wrapper.findAll('.filter-select')
await selects[0].setValue('App1')
- await wrapper.find('.version-filter').setValue('2.196')
+ await wrapper.find('.version-filter').setValue('2.196.0')
await wrapper.vm.$nextTick()
const rows = wrapper.findAll('tbody tr')
diff --git a/frontend/src/views/HistoryView.vue b/frontend/src/views/HistoryView.vue
index 180f858..b284cb4 100644
--- a/frontend/src/views/HistoryView.vue
+++ b/frontend/src/views/HistoryView.vue
@@ -8,11 +8,9 @@
-
-