feat(cps): 淘宝活动落地页图做活——每活动关联图,运营上传/选已有

- model: cps_activity.image_url + 迁移 cps_activity_image 回填存量淘宝活动为现有图
- admin: 新建淘宝活动必传图(上传 /upload-image 或选已有 /activity-images),存绝对 URL
- 落地页 cps_redirect: 按 link→activity 取图展示,缺图兜底默认图,img src 经 html.escape
- media: save_cps_image + to_abs_media_url(基于 CPS_REDIRECT_BASE,跨域 admin-web 可显示)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 21:32:10 +08:00
parent 7368a1ca8a
commit d55f47fc59
7 changed files with 122 additions and 9 deletions
+22 -5
View File
@@ -6,13 +6,16 @@
"""
from __future__ import annotations
import html
import json
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session
from app.core import media
from app.db.session import get_db
from app.models.cps_activity import CpsActivity
from app.models.cps_link import CpsLink
from app.repositories import cps_link as cps_link_repo
@@ -21,6 +24,9 @@ router = APIRouter(tags=["cps-redirect"])
# code 不存在/失效时的兜底落地
_FALLBACK_URL = "https://www.meituan.com/"
# 淘宝活动未设图时的兜底主视觉(存量已回填,基本只在老 link/异常时触发)
_DEFAULT_TAOBAO_IMAGE = "/media/taobao_landing.jpg"
def _client_ip(request: Request) -> str | None:
xff = request.headers.get("x-forwarded-for")
@@ -46,7 +52,9 @@ def cps_landing(code: str, request: Request, db: Session = Depends(get_db)):
return RedirectResponse(_FALLBACK_URL, status_code=302)
_record(db, link, request, "visit")
if link.platform == "taobao":
return HTMLResponse(_taobao_landing_html(link.target_url))
activity = db.get(CpsActivity, link.activity_id)
image_url = (activity.image_url if activity else None) or _DEFAULT_TAOBAO_IMAGE
return HTMLResponse(_taobao_landing_html(link.target_url, image_url))
# 美团短链 / 京东链接:直接 302 跳
return RedirectResponse(link.target_url, status_code=302)
@@ -59,9 +67,18 @@ def cps_copy(code: str, request: Request, db: Session = Depends(get_db)) -> dict
return {"ok": True}
def _taobao_landing_html(token: str) -> str:
"""淘宝落地页 H5(主视觉为设计图 /media/taobao_landing.jpg,复制淘口令按钮在 75% 屏高)。token 安全嵌入 JS。"""
return _TAOBAO_HTML.replace("__TOKEN_JS__", json.dumps(token))
def _taobao_landing_html(token: str, image_url: str) -> str:
"""淘宝落地页 H5(主视觉图按活动传入,复制淘口令按钮在 75% 屏高)。
image_url 转绝对(落地页 coupon 域,相对也可达,绝对更稳)后经 html.escape 嵌入
<img src>(防属性注入);token 经 json.dumps 安全嵌入 JS。
"""
safe_img = html.escape(media.to_abs_media_url(image_url) or image_url, quote=True)
return (
_TAOBAO_HTML
.replace("__IMAGE_URL__", safe_img)
.replace("__TOKEN_JS__", json.dumps(token))
)
_TAOBAO_HTML = """<!DOCTYPE html>
@@ -82,7 +99,7 @@ body{font-family:-apple-system,"PingFang SC",sans-serif;background:#fff0ef;color
</style>
</head>
<body>
<img class="hero" src="/media/taobao_landing.jpg" alt="淘宝闪购 天天领红包">
<img class="hero" src="__IMAGE_URL__" alt="淘宝闪购 天天领红包">
<div class="btn-wrap"><button class="btn" onclick="copyToken()">复制口令去淘宝领红包</button></div>
<div class="toast" id="toast"></div>
<script>