3a40f617bd
- model: cps_group.platforms 多选 / cps_activity.payload(淘口令/京东链接) / cps_link.sid 可空 + 复制统计 - admin API: 群与活动平台多选、批量生成落地页短链 referral-links、对账字段可空 - 落地页 cps_redirect: 淘宝展示淘口令(记 copy 事件) / 京东 302 / 美团原逻辑 - 迁移 cps_v2_platforms: 加 platforms/payload/event_type 列, sid 放宽可空(含 downgrade) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
3.2 KiB
Python
96 lines
3.2 KiB
Python
"""CPS 群发短链 + 点击 数据访问(用户侧跳转端点 + admin 生成/统计 共用)。
|
|
|
|
短码生成去掉易混字符;点击按 (ip, ua) 近似去重 UV。点击量级小、admin 低频,统计在
|
|
Python 侧聚合,跨 PG/SQLite 无方言坑(与 admin cps repo 同风格)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.cps_link import CpsClick, CpsLink
|
|
|
|
# 去掉易混字符 0/O/1/l/I,运营/用户肉眼复制不易错
|
|
_CODE_ALPHABET = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
|
|
|
|
def _gen_code(n: int = 8) -> str:
|
|
return "".join(secrets.choice(_CODE_ALPHABET) for _ in range(n))
|
|
|
|
|
|
def get_by_code(db: Session, code: str) -> CpsLink | None:
|
|
return db.execute(select(CpsLink).where(CpsLink.code == code)).scalar_one_or_none()
|
|
|
|
|
|
def create_link(
|
|
db: Session, *, group_id: int, activity_id: int, sid: str | None, target_url: str,
|
|
platform: str = "meituan", commit: bool = True,
|
|
) -> CpsLink:
|
|
"""生成唯一短码并存库。冲突重试 5 次,仍冲突用 12 位兜底(概率近 0)。"""
|
|
code = _gen_code()
|
|
for _ in range(5):
|
|
if get_by_code(db, code) is None:
|
|
break
|
|
code = _gen_code()
|
|
else:
|
|
code = _gen_code(12)
|
|
link = CpsLink(
|
|
code=code, group_id=group_id, activity_id=activity_id, sid=sid,
|
|
target_url=target_url, platform=platform,
|
|
)
|
|
db.add(link)
|
|
if commit:
|
|
db.commit()
|
|
db.refresh(link)
|
|
else:
|
|
db.flush()
|
|
return link
|
|
|
|
|
|
def record_click(
|
|
db: Session, *, link: CpsLink, ip: str | None = None, ua: str | None = None,
|
|
event_type: str = "visit",
|
|
) -> None:
|
|
"""记一条点击。event_type: visit(进落地页/被跳转) | copy(淘宝点了"复制口令")。"""
|
|
db.add(CpsClick(
|
|
link_id=link.id, group_id=link.group_id, sid=link.sid, event_type=event_type,
|
|
ip=ip, ua=(ua[:500] if ua else None),
|
|
))
|
|
db.commit()
|
|
|
|
|
|
def click_stats_by_group(
|
|
db: Session, *, date_from: datetime | None = None, date_to: datetime | None = None,
|
|
) -> dict[int, dict]:
|
|
"""按 group_id 聚合点击 → {group_id: {pv, uv, copy_pv, copy_uv}}。
|
|
visit 计入 pv/uv,copy(淘宝复制口令) 计入 copy_pv/copy_uv;uv 按 (ip, ua) 近似去重。"""
|
|
stmt = select(CpsClick)
|
|
if date_from is not None:
|
|
stmt = stmt.where(CpsClick.clicked_at >= date_from)
|
|
if date_to is not None:
|
|
stmt = stmt.where(CpsClick.clicked_at <= date_to)
|
|
clicks = list(db.execute(stmt).scalars().all())
|
|
|
|
agg: dict[int, dict] = {}
|
|
seen_visit: dict[int, set] = {}
|
|
seen_copy: dict[int, set] = {}
|
|
for c in clicks:
|
|
a = agg.setdefault(c.group_id, {"pv": 0, "uv": 0, "copy_pv": 0, "copy_uv": 0})
|
|
key = (c.ip, c.ua)
|
|
if c.event_type == "copy":
|
|
a["copy_pv"] += 1
|
|
s = seen_copy.setdefault(c.group_id, set())
|
|
if key not in s:
|
|
s.add(key)
|
|
a["copy_uv"] += 1
|
|
else:
|
|
a["pv"] += 1
|
|
s = seen_visit.setdefault(c.group_id, set())
|
|
if key not in s:
|
|
s.add(key)
|
|
a["uv"] += 1
|
|
return agg
|