Преглед изворни кода

创意审核检查删除 fix

wangyunpeng пре 5 дана
родитељ
комит
995df66504

+ 123 - 0
examples/auto_put_ad_mini/test_creative_review_scan.py

@@ -751,6 +751,56 @@ class CreativeRejectionCleanupTests(unittest.TestCase):
         )
         )
         self.assertEqual(result["configured_status"], "AD_STATUS_DELETED")
         self.assertEqual(result["configured_status"], "AD_STATUS_DELETED")
 
 
+    def test_tencent_creative_delete_accepts_deleted_system_status(self):
+        from tencent_client import TencentClient
+
+        response = Mock(status_code=200, text="ok")
+        response.raise_for_status.return_value = None
+        response.json.return_value = {"code": 0, "data": {}}
+        client = TencentClient()
+        client.session.post = Mock(return_value=response)
+        client._common_params = Mock(return_value={"nonce": "1"})
+        client._user_token = Mock(return_value="user-token")
+        client.get_dynamic_creative = Mock(return_value={
+            "dynamic_creative_id": 3,
+            "configured_status": "AD_STATUS_NORMAL",
+            "system_status": "DYNAMIC_CREATIVE_STATUS_DELETED",
+            "creative_set_approval_status": (
+                "CREATIVE_SET_APPROVAL_STATUS_PARTIAL_NORMAL"
+            ),
+        })
+
+        result = client.delete_dynamic_creative(1, 3)
+
+        self.assertEqual(result["system_status"], "DYNAMIC_CREATIVE_STATUS_DELETED")
+        self.assertFalse(result["deleted_from_listing"])
+        self.assertEqual(client.get_dynamic_creative.call_count, 1)
+
+    def test_scan_excludes_creative_with_deleted_system_status(self):
+        from tools import creative_rejection_cleanup as cleanup
+
+        tencent = Mock()
+        tencent.get_dynamic_creatives.return_value = [{
+            "dynamic_creative_id": 3,
+            "configured_status": "AD_STATUS_NORMAL",
+            "system_status": "DYNAMIC_CREATIVE_STATUS_DELETED",
+            "creative_set_approval_status": cleanup.CREATIVE_PARTIAL_NORMAL_STATUS,
+        }]
+        tencent.get_ads.return_value = []
+        review_fetcher = Mock(return_value=[])
+
+        result = cleanup._scan_one_account(
+            {"account_id": 1},
+            tencent=tencent,
+            review_fetcher=review_fetcher,
+            spend_start_date=date(2026, 8, 14),
+            spend_end_date=date(2026, 8, 16),
+        )
+
+        self.assertEqual(result[5], 0)
+        review_fetcher.assert_not_called()
+        tencent.get_dynamic_creative_costs.assert_not_called()
+
     def test_tencent_creative_delete_retries_explicit_rate_limit_only(self):
     def test_tencent_creative_delete_retries_explicit_rate_limit_only(self):
         import tencent_client
         import tencent_client
 
 
@@ -2440,6 +2490,79 @@ class CreativeRejectionCleanupTests(unittest.TestCase):
             for _, values in updates
             for _, values in updates
         ))
         ))
 
 
+    def test_unknown_write_result_recovers_from_deleted_status_without_redelete(self):
+        from tools import creative_rejection_cleanup as cleanup
+
+        @contextmanager
+        def acquired_lock(_name):
+            yield True
+
+        tencent = Mock()
+        tencent.get_dynamic_creatives.return_value = []
+        tencent.get_ads.return_value = []
+        tencent.get_dynamic_creative.return_value = {
+            "dynamic_creative_id": 3,
+            "adgroup_id": 2,
+            "configured_status": "AD_STATUS_NORMAL",
+            "system_status": "DYNAMIC_CREATIVE_STATUS_DELETED",
+            "creative_set_approval_status": cleanup.CREATIVE_PARTIAL_NORMAL_STATUS,
+        }
+        retry_item = {
+            "id": 7,
+            "account_id": 1,
+            "agency_name": "代理A",
+            "adgroup_id": 2,
+            "dynamic_creative_id": 3,
+            "cleanup_action": cleanup.DELETE_CREATIVE,
+            "cleanup_status": "WRITE_OUTCOME_UNKNOWN",
+        }
+        updates = []
+        with tempfile.TemporaryDirectory() as directory, patch.dict(
+            os.environ,
+            {
+                "DAILY_REJECTED_CREATIVE_APPLY_ENABLED": "1",
+                "ROI_AGENCY_WEBHOOK_ENABLED": "1",
+                "ROI_AGENCY_WEBHOOKS_JSON": json.dumps({
+                    "代理A": (
+                        "https://open.feishu.cn/open-apis/bot/v2/hook/"
+                        "test-cleanup-route"
+                    )
+                }),
+                "FEISHU_AD_PROJECT_CHAT_ID": "chat-operator",
+            },
+            clear=False,
+        ), patch.object(cleanup, "initialize_schema"), patch.object(
+            cleanup, "resolve_end_date", return_value="20260812"
+        ), patch.object(cleanup, "fetch_daily_data", return_value=pd.DataFrame()), patch.object(
+            cleanup, "fetch_recent_spend_accounts", return_value=[{"account_id": 1}]
+        ), patch.object(cleanup, "fetch_account_agency_fallbacks", return_value={}), patch.object(
+            cleanup, "prefetch_account_access_tokens", return_value={}
+        ), patch.object(cleanup, "upsert_cleanup_candidate"), patch.object(
+            cleanup, "load_retryable_cleanup_items", return_value=[retry_item]
+        ), patch.object(
+            cleanup, "claim_cleanup_item", return_value=True
+        ), patch.object(
+            cleanup,
+            "update_cleanup_item",
+            side_effect=lambda item_id, **values: updates.append((item_id, values)),
+        ), patch.object(cleanup, "load_unnotified_deleted_items", return_value=[]), patch.object(
+            cleanup, "advisory_lock", side_effect=acquired_lock
+        ):
+            summary = cleanup.run_rejected_creative_cleanup(
+                output_dir=Path(directory),
+                now=datetime(2026, 8, 13, 11, 0, tzinfo=ZoneInfo("Asia/Shanghai")),
+                tencent=tencent,
+                odps=Mock(),
+            )
+
+        tencent.delete_dynamic_creative.assert_not_called()
+        self.assertEqual(summary["deleted"], 1)
+        self.assertTrue(any(
+            values.get("cleanup_status") == "CREATIVE_DELETED"
+            and "DYNAMIC_CREATIVE_STATUS_DELETED" in values.get("readback_json", "")
+            for _, values in updates
+        ))
+
     def test_notification_channels_retry_independently(self):
     def test_notification_channels_retry_independently(self):
         from tools import creative_rejection_cleanup as cleanup
         from tools import creative_rejection_cleanup as cleanup
 
 

+ 28 - 2
examples/auto_put_ad_mini/tools/creative_rejection_cleanup.py

@@ -38,6 +38,7 @@ REPORT_VERSION = "creative_rejection_cleanup_v11"
 OPERATOR_SUMMARY_ROUTE = "投放调控汇总"
 OPERATOR_SUMMARY_ROUTE = "投放调控汇总"
 DENIED_SYSTEM_STATUS = "DYNAMIC_CREATIVE_STATUS_DENIED"
 DENIED_SYSTEM_STATUS = "DYNAMIC_CREATIVE_STATUS_DENIED"
 DELETED_STATUS = "AD_STATUS_DELETED"
 DELETED_STATUS = "AD_STATUS_DELETED"
+DYNAMIC_CREATIVE_DELETED_STATUS = "DYNAMIC_CREATIVE_STATUS_DELETED"
 CREATIVE_DENIED_STATUS = "CREATIVE_SET_APPROVAL_STATUS_DENIED"
 CREATIVE_DENIED_STATUS = "CREATIVE_SET_APPROVAL_STATUS_DENIED"
 CREATIVE_PARTIAL_NORMAL_STATUS = "CREATIVE_SET_APPROVAL_STATUS_PARTIAL_NORMAL"
 CREATIVE_PARTIAL_NORMAL_STATUS = "CREATIVE_SET_APPROVAL_STATUS_PARTIAL_NORMAL"
 DELETE_CREATIVE = "DELETE_CREATIVE"
 DELETE_CREATIVE = "DELETE_CREATIVE"
@@ -99,6 +100,13 @@ def _is_rejected_status(value: Any) -> bool:
     return "REJECT" in upper or "DENIED" in upper
     return "REJECT" in upper or "DENIED" in upper
 
 
 
 
+def _is_deleted_creative(creative: dict[str, Any]) -> bool:
+    return (
+        creative.get("configured_status") == DELETED_STATUS
+        or creative.get("system_status") == DYNAMIC_CREATIVE_DELETED_STATUS
+    )
+
+
 def has_rejected_wechat_mini_program_element(raw_result: dict | None) -> bool:
 def has_rejected_wechat_mini_program_element(raw_result: dict | None) -> bool:
     """Return whether a rejected element is explicitly named 微信小程序."""
     """Return whether a rejected element is explicitly named 微信小程序."""
 
 
@@ -1262,7 +1270,7 @@ def _scan_one_account(
             int(row["dynamic_creative_id"])
             int(row["dynamic_creative_id"])
             for row in creatives
             for row in creatives
             if _as_int(row.get("dynamic_creative_id")) is not None
             if _as_int(row.get("dynamic_creative_id")) is not None
-            and row.get("configured_status") != DELETED_STATUS
+            and not _is_deleted_creative(row)
         ]
         ]
         raw_by_id: dict[int, dict] = {}
         raw_by_id: dict[int, dict] = {}
         for batch in _chunks(ids, 100):
         for batch in _chunks(ids, 100):
@@ -1276,7 +1284,7 @@ def _scan_one_account(
             int(row["dynamic_creative_id"])
             int(row["dynamic_creative_id"])
             for row in creatives
             for row in creatives
             if _as_int(row.get("dynamic_creative_id")) is not None
             if _as_int(row.get("dynamic_creative_id")) is not None
-            and row.get("configured_status") != DELETED_STATUS
+            and not _is_deleted_creative(row)
             and row.get("creative_set_approval_status")
             and row.get("creative_set_approval_status")
             == CREATIVE_PARTIAL_NORMAL_STATUS
             == CREATIVE_PARTIAL_NORMAL_STATUS
         ]
         ]
@@ -1741,6 +1749,24 @@ def run_rejected_creative_cleanup(
                                 "delete result ignored because claim ownership was lost"
                                 "delete result ignored because claim ownership was lost"
                             )
                             )
                         raise
                         raise
+                    if (
+                        item.get("cleanup_action") == DELETE_CREATIVE
+                        and _is_deleted_creative(before)
+                    ):
+                        updated = _update_owned_cleanup_item(
+                            item_id,
+                            cleanup_status="CREATIVE_DELETED",
+                            error_message=None,
+                            pre_state_json=_json(before),
+                            readback_json=_json(before),
+                            deleted_at=effective_now,
+                        )
+                        if updated is not False:
+                            return 1, 0, None
+                        return 0, 0, (
+                            f"account={account_id} creative={creative_id}: "
+                            "delete result ignored because claim ownership was lost"
+                        )
                     if item.get("cleanup_status") in {
                     if item.get("cleanup_status") in {
                         "WRITE_OUTCOME_UNKNOWN",
                         "WRITE_OUTCOME_UNKNOWN",
                         "DELETING",
                         "DELETING",

+ 12 - 0
examples/tencent_realtime_control/tencent_client.py

@@ -145,6 +145,7 @@ def _is_rate_limited_payload(payload: Any) -> bool:
 ACTIVE_STATUS = "AD_STATUS_NORMAL"
 ACTIVE_STATUS = "AD_STATUS_NORMAL"
 SUSPEND_STATUS = "AD_STATUS_SUSPEND"
 SUSPEND_STATUS = "AD_STATUS_SUSPEND"
 DELETED_STATUS = "AD_STATUS_DELETED"
 DELETED_STATUS = "AD_STATUS_DELETED"
+DYNAMIC_CREATIVE_DELETED_STATUS = "DYNAMIC_CREATIVE_STATUS_DELETED"
 AD_FIELDS = [
 AD_FIELDS = [
     "adgroup_id",
     "adgroup_id",
     "adgroup_name",
     "adgroup_name",
@@ -712,6 +713,17 @@ class TencentClient:
                     ),
                     ),
                     "dynamic_creative_name": creative.get("dynamic_creative_name"),
                     "dynamic_creative_name": creative.get("dynamic_creative_name"),
                 }
                 }
+                if (
+                    creative.get("configured_status") == DELETED_STATUS
+                    or creative.get("system_status")
+                    == DYNAMIC_CREATIVE_DELETED_STATUS
+                ):
+                    return {
+                        **data,
+                        "dynamic_creative_id": dynamic_creative_id,
+                        **last_actual,
+                        "deleted_from_listing": False,
+                    }
                 logger.info(
                 logger.info(
                     "creative delete readback not converged account=%s creative=%s "
                     "creative delete readback not converged account=%s creative=%s "
                     "attempt=%d/%d readback=%s",
                     "attempt=%d/%d readback=%s",