From 0ac73bdb183cb99c35b9d7c19cfb79c0bae41e82 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sat, 18 Jul 2026 10:48:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20=E5=89=AA=E8=BE=91=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E8=AF=A6=E6=83=85/=E5=88=97=E8=A1=A8=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3rendered=5Furl=E7=AD=BE=E5=90=8D=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8DOSS=20403?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #454 OSS 403收尾: - generation status接口的video_url签名已在#467修复 - 本次修复plan详情/列表接口直接返回config的问题 - _to_response中对config.rendered_url自动做签名转换 - 覆盖GET /{plan_id}、列表、创建、更新、复制等所有返回EditPlanResponse的接口 - 新增3个单元测试,47个测试全过 - 失败降级:签名异常时返回原始URL,不阻塞主流程 --- apps/api/app/api/routes/edit_plans.py | 13 ++++++- tests/unit/test_edit_plans_api.py | 53 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) mode change 100755 => 100644 apps/api/app/api/routes/edit_plans.py diff --git a/apps/api/app/api/routes/edit_plans.py b/apps/api/app/api/routes/edit_plans.py old mode 100755 new mode 100644 index d624b7e75..146a5efe1 --- a/apps/api/app/api/routes/edit_plans.py +++ b/apps/api/app/api/routes/edit_plans.py @@ -22,6 +22,7 @@ from datetime import datetime from typing import Any, List, Optional from app.auth import AuthenticatedUser, get_current_user +from app.core.storage import get_storage_service from app.dependencies import ( get_asset_library_repository, get_asset_repository, @@ -251,6 +252,16 @@ class GenerateFromTemplateResponse(BaseModel): def _to_response(p: EditPlan) -> EditPlanResponse: + # 对 config 中的 rendered_url 做签名转换(私有bucket裸URL会403) + config = dict(p.config) if p.config else {} + raw_rendered_url = config.get("rendered_url", "") + if raw_rendered_url: + try: + storage = get_storage_service() + config["rendered_url"] = storage.get_download_url(raw_rendered_url, expires_seconds=86400) + except Exception as e: + logger.warning("剪辑计划rendered_url签名失败,返回原始URL: plan_id=%s error=%s", p.id, e) + return EditPlanResponse( id=p.id, template_id=p.template_id, @@ -260,7 +271,7 @@ def _to_response(p: EditPlan) -> EditPlanResponse: result_count=getattr(p, "result_count", 0), project_id=p.project_id or "", created_by_user_id=p.created_by_user_id or "", - config=p.config, + config=config, created_at=p.created_at, updated_at=p.updated_at, ) diff --git a/tests/unit/test_edit_plans_api.py b/tests/unit/test_edit_plans_api.py index 5e44e6575..1dab5494c 100755 --- a/tests/unit/test_edit_plans_api.py +++ b/tests/unit/test_edit_plans_api.py @@ -487,6 +487,59 @@ class TestGetPlan: assert resp.status_code == 404 assert "剪辑计划不存在" in resp.json()["detail"] + def test_get_plan_rendered_url_is_signed(self, client): + """plan详情接口返回的rendered_url应该是签名URL,不是裸OSS URL""" + c, repo = client + raw_url = "https://bucket.oss-cn-hangzhou.aliyuncs.com/rendered/plan-001/output.mp4" + signed_url = raw_url + "?OSSAccessKeyId=xxx&Expires=123456&Signature=yyy" + plan = EditPlan.create("tpl-001", "测试计划", config={"rendered_url": raw_url, "other": "val"}) + plan.status = EditPlanStatus.COMPLETED + repo.create(plan) + + mock_storage = MagicMock() + mock_storage.get_download_url.return_value = signed_url + with patch("app.api.routes.edit_plans.get_storage_service", return_value=mock_storage): + resp = c.get(f"/api/v1/edit-plans/{plan.id}") + + assert resp.status_code == 200 + data = resp.json() + assert data["config"]["rendered_url"] == signed_url + assert data["config"]["other"] == "val" + mock_storage.get_download_url.assert_called_once_with(raw_url, expires_seconds=86400) + + def test_list_plans_rendered_url_is_signed(self, client): + """plan列表接口返回的rendered_url也应该是签名URL""" + c, repo = client + raw_url = "https://bucket.oss-cn-hangzhou.aliyuncs.com/rendered/plan-list/output.mp4" + signed_url = raw_url + "?OSSAccessKeyId=xxx&Expires=123456&Signature=yyy" + plan = EditPlan.create("tpl-001", "测试计划", config={"rendered_url": raw_url}) + plan.status = EditPlanStatus.COMPLETED + repo.create(plan) + + mock_storage = MagicMock() + mock_storage.get_download_url.return_value = signed_url + with patch("app.api.routes.edit_plans.get_storage_service", return_value=mock_storage): + resp = c.get("/api/v1/edit-plans") + + assert resp.status_code == 200 + data = resp.json() + assert data["items"][0]["config"]["rendered_url"] == signed_url + + def test_get_plan_no_rendered_url_skips_signing(self, client): + """没有rendered_url的计划不调用签名服务""" + c, repo = client + plan = EditPlan.create("tpl-001", "测试计划", config={"key": "val"}) + repo.create(plan) + + mock_storage = MagicMock() + with patch("app.api.routes.edit_plans.get_storage_service", return_value=mock_storage): + resp = c.get(f"/api/v1/edit-plans/{plan.id}") + + assert resp.status_code == 200 + data = resp.json() + assert data["config"] == {"key": "val"} + mock_storage.get_download_url.assert_not_called() + # --------------------------------------------------------------------------- # 更新测试