|
@@ -33,7 +33,7 @@ from tools.creative_review import (
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
SHANGHAI = ZoneInfo("Asia/Shanghai")
|
|
SHANGHAI = ZoneInfo("Asia/Shanghai")
|
|
|
-REPORT_VERSION = "creative_rejection_cleanup_v7"
|
|
|
|
|
|
|
+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"
|
|
@@ -43,7 +43,7 @@ DELETE_CREATIVE = "DELETE_CREATIVE"
|
|
|
ALERT_ONLY = "ALERT_ONLY"
|
|
ALERT_ONLY = "ALERT_ONLY"
|
|
|
DEFAULT_PARTIAL_CREATIVE_COST_THRESHOLD_YUAN = 50.0
|
|
DEFAULT_PARTIAL_CREATIVE_COST_THRESHOLD_YUAN = 50.0
|
|
|
DEFAULT_WECHAT_MINI_PROGRAM_COST_THRESHOLD_YUAN = 100.0
|
|
DEFAULT_WECHAT_MINI_PROGRAM_COST_THRESHOLD_YUAN = 100.0
|
|
|
-REPORT_COLUMNS = (
|
|
|
|
|
|
|
+AGENCY_REPORT_COLUMNS = (
|
|
|
"代理名称",
|
|
"代理名称",
|
|
|
"账户ID",
|
|
"账户ID",
|
|
|
"账户名称",
|
|
"账户名称",
|
|
@@ -51,6 +51,22 @@ REPORT_COLUMNS = (
|
|
|
"广告名称",
|
|
"广告名称",
|
|
|
"创意ID",
|
|
"创意ID",
|
|
|
"创意名称",
|
|
"创意名称",
|
|
|
|
|
+ "近3天累计历史消耗(元)",
|
|
|
|
|
+ "配置状态",
|
|
|
|
|
+ "创意审核状态",
|
|
|
|
|
+ "审核不通过原因",
|
|
|
|
|
+ "执行操作",
|
|
|
|
|
+)
|
|
|
|
|
+OPERATOR_REPORT_COLUMNS = (
|
|
|
|
|
+ "代理名称",
|
|
|
|
|
+ "账户ID",
|
|
|
|
|
+ "账户名称",
|
|
|
|
|
+ "广告ID",
|
|
|
|
|
+ "广告名称",
|
|
|
|
|
+ "创意ID",
|
|
|
|
|
+ "创意名称",
|
|
|
|
|
+ "近3天累计历史消耗(元)",
|
|
|
|
|
+ "消耗日期范围",
|
|
|
"配置状态",
|
|
"配置状态",
|
|
|
"创意审核状态",
|
|
"创意审核状态",
|
|
|
"元素粒度审核状态",
|
|
"元素粒度审核状态",
|
|
@@ -60,7 +76,10 @@ REPORT_COLUMNS = (
|
|
|
"审核不通过原因",
|
|
"审核不通过原因",
|
|
|
"检查时间",
|
|
"检查时间",
|
|
|
"执行操作",
|
|
"执行操作",
|
|
|
|
|
+ "操作判断原因",
|
|
|
)
|
|
)
|
|
|
|
|
+# Compatibility for callers that treat the agency report as the default report.
|
|
|
|
|
+REPORT_COLUMNS = AGENCY_REPORT_COLUMNS
|
|
|
|
|
|
|
|
|
|
|
|
|
def _json(value: Any) -> str:
|
|
def _json(value: Any) -> str:
|
|
@@ -542,16 +561,20 @@ def update_cleanup_item(item_id: int, **values: Any) -> None:
|
|
|
connection.close()
|
|
connection.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
-def load_pending_notification_items() -> list[dict[str, Any]]:
|
|
|
|
|
|
|
+def load_pending_notification_items(
|
|
|
|
|
+ *,
|
|
|
|
|
+ include_discovered: bool = False,
|
|
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
connection = get_connection()
|
|
connection = get_connection()
|
|
|
try:
|
|
try:
|
|
|
with connection.cursor() as cursor:
|
|
with connection.cursor() as cursor:
|
|
|
|
|
+ statuses = "'CREATIVE_DELETED','ALERT_PENDING'"
|
|
|
|
|
+ if include_discovered:
|
|
|
|
|
+ statuses += ",'DISCOVERED'"
|
|
|
cursor.execute(
|
|
cursor.execute(
|
|
|
- """
|
|
|
|
|
|
|
+ f"""
|
|
|
SELECT * FROM creative_rejection_cleanup_item
|
|
SELECT * FROM creative_rejection_cleanup_item
|
|
|
- WHERE cleanup_status IN (
|
|
|
|
|
- 'CREATIVE_DELETED','ALERT_PENDING'
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ WHERE cleanup_status IN ({statuses})
|
|
|
AND (
|
|
AND (
|
|
|
agency_notified_at IS NULL
|
|
agency_notified_at IS NULL
|
|
|
OR operator_notified_at IS NULL
|
|
OR operator_notified_at IS NULL
|
|
@@ -565,10 +588,13 @@ def load_pending_notification_items() -> list[dict[str, Any]]:
|
|
|
connection.close()
|
|
connection.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
-def load_unnotified_deleted_items() -> list[dict[str, Any]]:
|
|
|
|
|
|
|
+def load_unnotified_deleted_items(
|
|
|
|
|
+ *,
|
|
|
|
|
+ include_discovered: bool = False,
|
|
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
"""Compatibility entry point for pending cleanup notifications."""
|
|
"""Compatibility entry point for pending cleanup notifications."""
|
|
|
|
|
|
|
|
- return load_pending_notification_items()
|
|
|
|
|
|
|
+ return load_pending_notification_items(include_discovered=include_discovered)
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mark_cleanup_channel_notified(
|
|
def _mark_cleanup_channel_notified(
|
|
@@ -735,7 +761,7 @@ def publish_cleanup_operator_summary(
|
|
|
raise FileNotFoundError(path)
|
|
raise FileNotFoundError(path)
|
|
|
target_chat_id = str(chat_id or "").strip()
|
|
target_chat_id = str(chat_id or "").strip()
|
|
|
if not target_chat_id:
|
|
if not target_chat_id:
|
|
|
- raise RuntimeError("FEISHU_OPERATOR_CHAT_ID 未配置")
|
|
|
|
|
|
|
+ raise RuntimeError("FEISHU_AD_PROJECT_CHAT_ID 未配置")
|
|
|
delivery = upsert_cleanup_delivery(
|
|
delivery = upsert_cleanup_delivery(
|
|
|
{
|
|
{
|
|
|
"run_id": run_id,
|
|
"run_id": run_id,
|
|
@@ -773,8 +799,15 @@ def publish_cleanup_operator_summary(
|
|
|
message_id = publisher.send_report_card(
|
|
message_id = publisher.send_report_card(
|
|
|
title=str(report.get("title") or path.stem),
|
|
title=str(report.get("title") or path.stem),
|
|
|
content=(
|
|
content=(
|
|
|
- f"本批次共 **{int(report.get('creative_rows') or 0)}** 条创意,"
|
|
|
|
|
- "包含各代理自动删除及需人工判断的完整汇总。"
|
|
|
|
|
|
|
+ (
|
|
|
|
|
+ f"本次预演共 **{int(report.get('creative_rows') or 0)}** 条创意,"
|
|
|
|
|
+ "未执行任何删除,包含建议删除项及需人工判断项。"
|
|
|
|
|
+ )
|
|
|
|
|
+ if report.get("dry_run")
|
|
|
|
|
+ else (
|
|
|
|
|
+ f"本批次共 **{int(report.get('creative_rows') or 0)}** 条创意,"
|
|
|
|
|
+ "包含各代理自动删除及需人工判断的完整汇总。"
|
|
|
|
|
+ )
|
|
|
),
|
|
),
|
|
|
sheet_url=sheet_url,
|
|
sheet_url=sheet_url,
|
|
|
chat_id=target_chat_id,
|
|
chat_id=target_chat_id,
|
|
@@ -853,11 +886,16 @@ def _display_date(value: Any) -> str:
|
|
|
return str(value or "")[:10]
|
|
return str(value or "")[:10]
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _write_report(path: Path, rows: list[dict[str, Any]]) -> None:
|
|
|
|
|
|
|
+def _write_report(
|
|
|
|
|
+ path: Path,
|
|
|
|
|
+ rows: list[dict[str, Any]],
|
|
|
|
|
+ *,
|
|
|
|
|
+ columns: tuple[str, ...],
|
|
|
|
|
+) -> None:
|
|
|
workbook = Workbook()
|
|
workbook = Workbook()
|
|
|
sheet = workbook.active
|
|
sheet = workbook.active
|
|
|
sheet.title = "审核不通过创意清理"
|
|
sheet.title = "审核不通过创意清理"
|
|
|
- sheet.append(list(REPORT_COLUMNS))
|
|
|
|
|
|
|
+ sheet.append(list(columns))
|
|
|
for row in rows:
|
|
for row in rows:
|
|
|
raw_result = _json_object(
|
|
raw_result = _json_object(
|
|
|
row.get("review_result") or row.get("review_result_json")
|
|
row.get("review_result") or row.get("review_result_json")
|
|
@@ -865,7 +903,12 @@ def _write_report(path: Path, rows: list[dict[str, Any]]) -> None:
|
|
|
pre_state = _json_object(row.get("pre_state") or row.get("pre_state_json"))
|
|
pre_state = _json_object(row.get("pre_state") or row.get("pre_state_json"))
|
|
|
granular = review_granularity_fields(raw_result)
|
|
granular = review_granularity_fields(raw_result)
|
|
|
action = str(row.get("cleanup_action") or "")
|
|
action = str(row.get("cleanup_action") or "")
|
|
|
- if action == DELETE_CREATIVE:
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ action == DELETE_CREATIVE
|
|
|
|
|
+ and row.get("cleanup_status") == "DISCOVERED"
|
|
|
|
|
+ ):
|
|
|
|
|
+ execution_action = "建议删除创意(未执行)"
|
|
|
|
|
+ elif action == DELETE_CREATIVE:
|
|
|
execution_action = "删除创意"
|
|
execution_action = "删除创意"
|
|
|
else:
|
|
else:
|
|
|
execution_action = "需人工判断"
|
|
execution_action = "需人工判断"
|
|
@@ -875,49 +918,81 @@ def _write_report(path: Path, rows: list[dict[str, Any]]) -> None:
|
|
|
or row.get("updated_at")
|
|
or row.get("updated_at")
|
|
|
or row.get("deleted_at")
|
|
or row.get("deleted_at")
|
|
|
)
|
|
)
|
|
|
- sheet.append(
|
|
|
|
|
- [
|
|
|
|
|
- row.get("agency_name") or "",
|
|
|
|
|
- str(row["account_id"]),
|
|
|
|
|
- row.get("account_name") or "",
|
|
|
|
|
- str(row["adgroup_id"]),
|
|
|
|
|
- row.get("adgroup_name") or "",
|
|
|
|
|
- str(row["dynamic_creative_id"]),
|
|
|
|
|
- row.get("dynamic_creative_name") or "",
|
|
|
|
|
- status_desc(
|
|
|
|
|
- pre_state.get("configured_status")
|
|
|
|
|
- or row.get("configured_status")
|
|
|
|
|
- ),
|
|
|
|
|
- status_desc(
|
|
|
|
|
- pre_state.get("creative_set_approval_status")
|
|
|
|
|
- or row.get("creative_set_approval_status")
|
|
|
|
|
- ),
|
|
|
|
|
- granular["element_review_status"],
|
|
|
|
|
- granular["element_reject_reason"],
|
|
|
|
|
- granular["site_review_status"],
|
|
|
|
|
- granular["site_reject_reason"],
|
|
|
|
|
- row["reject_reason"],
|
|
|
|
|
- (
|
|
|
|
|
- checked_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
- if isinstance(checked_at, (date, datetime))
|
|
|
|
|
- else str(checked_at or "")
|
|
|
|
|
- ),
|
|
|
|
|
- execution_action,
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ recent_cost_fen = row.get("recent_cost_fen")
|
|
|
|
|
+ recent_cost_yuan = (
|
|
|
|
|
+ ""
|
|
|
|
|
+ if recent_cost_fen is None
|
|
|
|
|
+ else f"{int(recent_cost_fen) / 100:.2f}"
|
|
|
)
|
|
)
|
|
|
|
|
+ cost_start = _display_date(row.get("cost_start_date"))
|
|
|
|
|
+ cost_end = _display_date(row.get("cost_end_date"))
|
|
|
|
|
+ values = {
|
|
|
|
|
+ "代理名称": row.get("agency_name") or "",
|
|
|
|
|
+ "账户ID": str(row["account_id"]),
|
|
|
|
|
+ "账户名称": row.get("account_name") or "",
|
|
|
|
|
+ "广告ID": str(row["adgroup_id"]),
|
|
|
|
|
+ "广告名称": row.get("adgroup_name") or "",
|
|
|
|
|
+ "创意ID": str(row["dynamic_creative_id"]),
|
|
|
|
|
+ "创意名称": row.get("dynamic_creative_name") or "",
|
|
|
|
|
+ "近3天累计历史消耗(元)": recent_cost_yuan,
|
|
|
|
|
+ "消耗日期范围": (
|
|
|
|
|
+ f"{cost_start} ~ {cost_end}" if cost_start and cost_end else ""
|
|
|
|
|
+ ),
|
|
|
|
|
+ "执行操作": execution_action,
|
|
|
|
|
+ "操作判断原因": row.get("action_reason") or "",
|
|
|
|
|
+ "配置状态": status_desc(
|
|
|
|
|
+ pre_state.get("configured_status")
|
|
|
|
|
+ or row.get("configured_status")
|
|
|
|
|
+ ),
|
|
|
|
|
+ "创意审核状态": status_desc(
|
|
|
|
|
+ pre_state.get("creative_set_approval_status")
|
|
|
|
|
+ or row.get("creative_set_approval_status")
|
|
|
|
|
+ ),
|
|
|
|
|
+ "元素粒度审核状态": granular["element_review_status"],
|
|
|
|
|
+ "元素粒度审核不通过原因": granular["element_reject_reason"],
|
|
|
|
|
+ "版位粒度审核状态": granular["site_review_status"],
|
|
|
|
|
+ "版位粒度审核不通过原因": granular["site_reject_reason"],
|
|
|
|
|
+ "审核不通过原因": row["reject_reason"],
|
|
|
|
|
+ "检查时间": (
|
|
|
|
|
+ checked_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
+ if isinstance(checked_at, (date, datetime))
|
|
|
|
|
+ else str(checked_at or "")
|
|
|
|
|
+ ),
|
|
|
|
|
+ }
|
|
|
|
|
+ sheet.append([values[column] for column in columns])
|
|
|
header_fill = PatternFill("solid", fgColor="C65911")
|
|
header_fill = PatternFill("solid", fgColor="C65911")
|
|
|
for cell in sheet[1]:
|
|
for cell in sheet[1]:
|
|
|
cell.fill = header_fill
|
|
cell.fill = header_fill
|
|
|
cell.font = Font(color="FFFFFF", bold=True)
|
|
cell.font = Font(color="FFFFFF", bold=True)
|
|
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
|
- widths = [22, 14, 22, 14, 30, 16, 30, 20, 24, 40, 60, 40, 60, 60, 20, 16]
|
|
|
|
|
- for index, width in enumerate(widths, start=1):
|
|
|
|
|
- sheet.column_dimensions[get_column_letter(index)].width = width
|
|
|
|
|
|
|
+ widths = {
|
|
|
|
|
+ "代理名称": 22,
|
|
|
|
|
+ "账户ID": 14,
|
|
|
|
|
+ "账户名称": 22,
|
|
|
|
|
+ "广告ID": 14,
|
|
|
|
|
+ "广告名称": 30,
|
|
|
|
|
+ "创意ID": 16,
|
|
|
|
|
+ "创意名称": 30,
|
|
|
|
|
+ "近3天累计历史消耗(元)": 22,
|
|
|
|
|
+ "消耗日期范围": 24,
|
|
|
|
|
+ "执行操作": 16,
|
|
|
|
|
+ "操作判断原因": 60,
|
|
|
|
|
+ "配置状态": 20,
|
|
|
|
|
+ "创意审核状态": 24,
|
|
|
|
|
+ "元素粒度审核状态": 40,
|
|
|
|
|
+ "元素粒度审核不通过原因": 60,
|
|
|
|
|
+ "版位粒度审核状态": 40,
|
|
|
|
|
+ "版位粒度审核不通过原因": 60,
|
|
|
|
|
+ "审核不通过原因": 60,
|
|
|
|
|
+ "检查时间": 20,
|
|
|
|
|
+ }
|
|
|
|
|
+ for index, column in enumerate(columns, start=1):
|
|
|
|
|
+ sheet.column_dimensions[get_column_letter(index)].width = widths[column]
|
|
|
for row in sheet.iter_rows(min_row=2):
|
|
for row in sheet.iter_rows(min_row=2):
|
|
|
for cell in row:
|
|
for cell in row:
|
|
|
cell.alignment = Alignment(vertical="top", wrap_text=True)
|
|
cell.alignment = Alignment(vertical="top", wrap_text=True)
|
|
|
- for column_index in (2, 4, 6):
|
|
|
|
|
- row[column_index - 1].number_format = "@"
|
|
|
|
|
|
|
+ for id_column in ("账户ID", "广告ID", "创意ID"):
|
|
|
|
|
+ row[columns.index(id_column)].number_format = "@"
|
|
|
sheet.freeze_panes = "A2"
|
|
sheet.freeze_panes = "A2"
|
|
|
sheet.auto_filter.ref = sheet.dimensions
|
|
sheet.auto_filter.ref = sheet.dimensions
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -960,16 +1035,28 @@ def write_cleanup_reports(
|
|
|
path = output_dir / (
|
|
path = output_dir / (
|
|
|
f"{report_date}_{safe_agency}_创意审核异常处理_{agency_digest}.xlsx"
|
|
f"{report_date}_{safe_agency}_创意审核异常处理_{agency_digest}.xlsx"
|
|
|
)
|
|
)
|
|
|
- _write_report(path, agency_rows)
|
|
|
|
|
|
|
+ dry_run = any(
|
|
|
|
|
+ row.get("cleanup_status") == "DISCOVERED"
|
|
|
|
|
+ for row in agency_rows
|
|
|
|
|
+ )
|
|
|
|
|
+ _write_report(path, agency_rows, columns=AGENCY_REPORT_COLUMNS)
|
|
|
reports.append(
|
|
reports.append(
|
|
|
{
|
|
{
|
|
|
"agency_name": agency,
|
|
"agency_name": agency,
|
|
|
"report_version": REPORT_VERSION,
|
|
"report_version": REPORT_VERSION,
|
|
|
"report": str(path),
|
|
"report": str(path),
|
|
|
- "title": f"{report_date}_{agency}_创意审核异常处理通知",
|
|
|
|
|
|
|
+ "title": (
|
|
|
|
|
+ f"{report_date}_{agency}_创意审核异常处理预演通知"
|
|
|
|
|
+ if dry_run
|
|
|
|
|
+ else f"{report_date}_{agency}_创意审核异常处理通知"
|
|
|
|
|
+ ),
|
|
|
"creative_rows": len(agency_rows),
|
|
"creative_rows": len(agency_rows),
|
|
|
"ad_rows": 0,
|
|
"ad_rows": 0,
|
|
|
- "notification_type": "creative_rejection_cleanup",
|
|
|
|
|
|
|
+ "notification_type": (
|
|
|
|
|
+ "creative_rejection_dry_run"
|
|
|
|
|
+ if dry_run
|
|
|
|
|
+ else "creative_rejection_cleanup"
|
|
|
|
|
+ ),
|
|
|
"run_id": f"reject_{report_date}_{agency_digest}",
|
|
"run_id": f"reject_{report_date}_{agency_digest}",
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
@@ -1002,13 +1089,19 @@ def write_cleanup_operator_summary(
|
|
|
path = output_dir / (
|
|
path = output_dir / (
|
|
|
f"{report_date}_投放调控_创意审核异常处理汇总_{digest}.xlsx"
|
|
f"{report_date}_投放调控_创意审核异常处理汇总_{digest}.xlsx"
|
|
|
)
|
|
)
|
|
|
- _write_report(path, rows)
|
|
|
|
|
|
|
+ dry_run = any(row.get("cleanup_status") == "DISCOVERED" for row in rows)
|
|
|
|
|
+ _write_report(path, rows, columns=OPERATOR_REPORT_COLUMNS)
|
|
|
return {
|
|
return {
|
|
|
"report_version": f"{REPORT_VERSION}_operator_summary",
|
|
"report_version": f"{REPORT_VERSION}_operator_summary",
|
|
|
"report": str(path),
|
|
"report": str(path),
|
|
|
- "title": f"{report_date}_创意审核异常处理汇总通知",
|
|
|
|
|
|
|
+ "title": (
|
|
|
|
|
+ f"{report_date}_创意审核异常处理预演汇总通知"
|
|
|
|
|
+ if dry_run
|
|
|
|
|
+ else f"{report_date}_创意审核异常处理汇总通知"
|
|
|
|
|
+ ),
|
|
|
"creative_rows": len(rows),
|
|
"creative_rows": len(rows),
|
|
|
"run_id": f"reject_{report_date}_{digest}",
|
|
"run_id": f"reject_{report_date}_{digest}",
|
|
|
|
|
+ "dry_run": dry_run,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1141,10 +1234,10 @@ def run_rejected_creative_cleanup(
|
|
|
"DAILY_REJECTED_CREATIVE_APPLY_ENABLED=1 requires "
|
|
"DAILY_REJECTED_CREATIVE_APPLY_ENABLED=1 requires "
|
|
|
"ROI_AGENCY_WEBHOOK_ENABLED=1"
|
|
"ROI_AGENCY_WEBHOOK_ENABLED=1"
|
|
|
)
|
|
)
|
|
|
- if apply_enabled and not os.getenv("FEISHU_OPERATOR_CHAT_ID", "").strip():
|
|
|
|
|
|
|
+ if apply_enabled and not os.getenv("FEISHU_AD_PROJECT_CHAT_ID", "").strip():
|
|
|
raise RuntimeError(
|
|
raise RuntimeError(
|
|
|
"DAILY_REJECTED_CREATIVE_APPLY_ENABLED=1 requires "
|
|
"DAILY_REJECTED_CREATIVE_APPLY_ENABLED=1 requires "
|
|
|
- "FEISHU_OPERATOR_CHAT_ID"
|
|
|
|
|
|
|
+ "FEISHU_AD_PROJECT_CHAT_ID"
|
|
|
)
|
|
)
|
|
|
initialize_schema()
|
|
initialize_schema()
|
|
|
if odps is None:
|
|
if odps is None:
|
|
@@ -1271,66 +1364,112 @@ def run_rejected_creative_cleanup(
|
|
|
bool(error),
|
|
bool(error),
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- for (
|
|
|
|
|
- account,
|
|
|
|
|
- creatives,
|
|
|
|
|
- ads,
|
|
|
|
|
- raw_by_id,
|
|
|
|
|
- cost_by_id,
|
|
|
|
|
- spend_error,
|
|
|
|
|
- ) in scan_results:
|
|
|
|
|
- account_id = int(account["account_id"])
|
|
|
|
|
- for creative in creatives:
|
|
|
|
|
- creative_id = _as_int(creative.get("dynamic_creative_id"))
|
|
|
|
|
- adgroup_id = _as_int(creative.get("adgroup_id"))
|
|
|
|
|
- if creative_id is None or adgroup_id is None:
|
|
|
|
|
- continue
|
|
|
|
|
- raw_result = raw_by_id.get(creative_id)
|
|
|
|
|
- system_status = str(creative.get("system_status") or "")
|
|
|
|
|
- is_partial = (
|
|
|
|
|
- creative.get("creative_set_approval_status")
|
|
|
|
|
- == CREATIVE_PARTIAL_NORMAL_STATUS
|
|
|
|
|
- )
|
|
|
|
|
- action = determine_cleanup_action(
|
|
|
|
|
- creative,
|
|
|
|
|
- raw_result,
|
|
|
|
|
- recent_cost_fen=cost_by_id.get(creative_id),
|
|
|
|
|
- cost_threshold_fen=cost_threshold_fen,
|
|
|
|
|
- wechat_cost_threshold_fen=wechat_cost_threshold_fen,
|
|
|
|
|
- spend_error=(spend_error if is_partial else None),
|
|
|
|
|
- )
|
|
|
|
|
- if action is None:
|
|
|
|
|
|
|
+ def process_creative(task):
|
|
|
|
|
+ (
|
|
|
|
|
+ account,
|
|
|
|
|
+ account_id,
|
|
|
|
|
+ creative,
|
|
|
|
|
+ ads,
|
|
|
|
|
+ raw_by_id,
|
|
|
|
|
+ cost_by_id,
|
|
|
|
|
+ spend_error,
|
|
|
|
|
+ ) = task
|
|
|
|
|
+ creative_id = _as_int(creative.get("dynamic_creative_id"))
|
|
|
|
|
+ adgroup_id = _as_int(creative.get("adgroup_id"))
|
|
|
|
|
+ if creative_id is None or adgroup_id is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ raw_result = raw_by_id.get(creative_id)
|
|
|
|
|
+ system_status = str(creative.get("system_status") or "")
|
|
|
|
|
+ is_partial = (
|
|
|
|
|
+ creative.get("creative_set_approval_status")
|
|
|
|
|
+ == CREATIVE_PARTIAL_NORMAL_STATUS
|
|
|
|
|
+ )
|
|
|
|
|
+ action = determine_cleanup_action(
|
|
|
|
|
+ creative,
|
|
|
|
|
+ raw_result,
|
|
|
|
|
+ recent_cost_fen=cost_by_id.get(creative_id),
|
|
|
|
|
+ cost_threshold_fen=cost_threshold_fen,
|
|
|
|
|
+ wechat_cost_threshold_fen=wechat_cost_threshold_fen,
|
|
|
|
|
+ spend_error=(spend_error if is_partial else None),
|
|
|
|
|
+ )
|
|
|
|
|
+ if action is None:
|
|
|
|
|
+ return None
|
|
|
|
|
+ ad = ads.get(adgroup_id) or {}
|
|
|
|
|
+ upsert_cleanup_candidate(
|
|
|
|
|
+ {
|
|
|
|
|
+ "account_id": account_id,
|
|
|
|
|
+ "account_name": context["account_names"].get(account_id)
|
|
|
|
|
+ or account.get("account_name")
|
|
|
|
|
+ or "",
|
|
|
|
|
+ "agency_name": _resolve_agency(
|
|
|
|
|
+ context, account_id, creative_id
|
|
|
|
|
+ ),
|
|
|
|
|
+ "adgroup_id": adgroup_id,
|
|
|
|
|
+ "adgroup_name": ad.get("adgroup_name") or "",
|
|
|
|
|
+ "dynamic_creative_id": creative_id,
|
|
|
|
|
+ "dynamic_creative_name": creative.get(
|
|
|
|
|
+ "dynamic_creative_name"
|
|
|
|
|
+ )
|
|
|
|
|
+ or "",
|
|
|
|
|
+ "check_date": effective_now.date(),
|
|
|
|
|
+ **action,
|
|
|
|
|
+ "action_reason": action.get("action_reason")
|
|
|
|
|
+ or _cleanup_reason(action, raw_result, system_status),
|
|
|
|
|
+ "reject_reason": _reject_reason(raw_result, system_status),
|
|
|
|
|
+ "cost_start_date": spend_start_date,
|
|
|
|
|
+ "cost_end_date": spend_end_date,
|
|
|
|
|
+ "review_result": raw_result or {},
|
|
|
|
|
+ "pre_state": creative,
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ return account_id, creative_id, action
|
|
|
|
|
+
|
|
|
|
|
+ tasks = [
|
|
|
|
|
+ (
|
|
|
|
|
+ account,
|
|
|
|
|
+ int(account["account_id"]),
|
|
|
|
|
+ creative,
|
|
|
|
|
+ ads,
|
|
|
|
|
+ raw_by_id,
|
|
|
|
|
+ cost_by_id,
|
|
|
|
|
+ spend_error,
|
|
|
|
|
+ )
|
|
|
|
|
+ for account, creatives, ads, raw_by_id, cost_by_id, spend_error
|
|
|
|
|
+ in scan_results
|
|
|
|
|
+ for creative in creatives
|
|
|
|
|
+ ]
|
|
|
|
|
+ process_workers = (
|
|
|
|
|
+ min(
|
|
|
|
|
+ int(os.getenv("TENCENT_AD_CREATIVE_PROCESS_WORKERS", "8")),
|
|
|
|
|
+ len(tasks),
|
|
|
|
|
+ 32,
|
|
|
|
|
+ )
|
|
|
|
|
+ if tasks
|
|
|
|
|
+ else 1
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "creative processing started creatives=%d workers=%d",
|
|
|
|
|
+ len(tasks),
|
|
|
|
|
+ process_workers,
|
|
|
|
|
+ )
|
|
|
|
|
+ with ThreadPoolExecutor(
|
|
|
|
|
+ max_workers=process_workers,
|
|
|
|
|
+ thread_name_prefix="creative-process",
|
|
|
|
|
+ ) as executor:
|
|
|
|
|
+ results = executor.map(process_creative, tasks)
|
|
|
|
|
+ for completed, result in enumerate(results, start=1):
|
|
|
|
|
+ if result is None:
|
|
|
continue
|
|
continue
|
|
|
|
|
+ account_id, creative_id, action = result
|
|
|
confirmed_actions[(account_id, creative_id)] = action
|
|
confirmed_actions[(account_id, creative_id)] = action
|
|
|
- ad = ads.get(adgroup_id) or {}
|
|
|
|
|
- upsert_cleanup_candidate(
|
|
|
|
|
- {
|
|
|
|
|
- "account_id": account_id,
|
|
|
|
|
- "account_name": context["account_names"].get(account_id)
|
|
|
|
|
- or account.get("account_name")
|
|
|
|
|
- or "",
|
|
|
|
|
- "agency_name": _resolve_agency(
|
|
|
|
|
- context, account_id, creative_id
|
|
|
|
|
- ),
|
|
|
|
|
- "adgroup_id": adgroup_id,
|
|
|
|
|
- "adgroup_name": ad.get("adgroup_name") or "",
|
|
|
|
|
- "dynamic_creative_id": creative_id,
|
|
|
|
|
- "dynamic_creative_name": creative.get(
|
|
|
|
|
- "dynamic_creative_name"
|
|
|
|
|
- )
|
|
|
|
|
- or "",
|
|
|
|
|
- "check_date": effective_now.date(),
|
|
|
|
|
- **action,
|
|
|
|
|
- "action_reason": action.get("action_reason")
|
|
|
|
|
- or _cleanup_reason(action, raw_result, system_status),
|
|
|
|
|
- "reject_reason": _reject_reason(raw_result, system_status),
|
|
|
|
|
- "cost_start_date": spend_start_date,
|
|
|
|
|
- "cost_end_date": spend_end_date,
|
|
|
|
|
- "review_result": raw_result or {},
|
|
|
|
|
- "pre_state": creative,
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
discovered += 1
|
|
discovered += 1
|
|
|
|
|
+ if completed % 500 == 0:
|
|
|
|
|
+ logger.info(
|
|
|
|
|
+ "creative processing progress=%d/%d confirmed=%d",
|
|
|
|
|
+ completed,
|
|
|
|
|
+ len(tasks),
|
|
|
|
|
+ discovered,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
deleted = 0
|
|
deleted = 0
|
|
|
deferred = 0
|
|
deferred = 0
|
|
@@ -1547,7 +1686,9 @@ def run_rejected_creative_cleanup(
|
|
|
f"account={account_id} creative={creative_id}: {exc}"
|
|
f"account={account_id} creative={creative_id}: {exc}"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- pending_notifications = load_unnotified_deleted_items()
|
|
|
|
|
|
|
+ pending_notifications = load_unnotified_deleted_items(
|
|
|
|
|
+ include_discovered=not apply_enabled,
|
|
|
|
|
+ )
|
|
|
deliveries: list[dict[str, object]] = []
|
|
deliveries: list[dict[str, object]] = []
|
|
|
operator_deliveries: list[dict[str, object]] = []
|
|
operator_deliveries: list[dict[str, object]] = []
|
|
|
notification_errors: list[str] = []
|
|
notification_errors: list[str] = []
|
|
@@ -1613,7 +1754,7 @@ def run_rejected_creative_cleanup(
|
|
|
operator_outcome = publish_cleanup_operator_summary(
|
|
operator_outcome = publish_cleanup_operator_summary(
|
|
|
run_id=str(operator_report["run_id"]),
|
|
run_id=str(operator_report["run_id"]),
|
|
|
report=operator_report,
|
|
report=operator_report,
|
|
|
- chat_id=os.getenv("FEISHU_OPERATOR_CHAT_ID", ""),
|
|
|
|
|
|
|
+ chat_id=os.getenv("FEISHU_AD_PROJECT_CHAT_ID", ""),
|
|
|
publisher=sheet_publisher,
|
|
publisher=sheet_publisher,
|
|
|
now=effective_now,
|
|
now=effective_now,
|
|
|
)
|
|
)
|