feat(cps): 后端接入淘宝/京东多平台(payload + 群多平台 + 复制统计 + 迁移)
- 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>
This commit is contained in:
+96
-69
@@ -1,6 +1,7 @@
|
||||
"""admin CPS 分发与对账:群(sid)管理 + 活动池 + 生成带 sid 券链接 + 美团订单对账 + 统计。
|
||||
"""admin CPS 分发与对账:群/活动管理 + 生成落地页短链 + 美团订单对账 + 统计。
|
||||
|
||||
当前仅接美团(get_referral_link / query_order)。淘宝/京东待各自联盟凭证。
|
||||
平台:meituan(actId+sid 转链 + query_order 对账) / taobao(整段淘口令) / jd(链接)。
|
||||
淘宝/京东无 API → 只统计点击(咱落地页 PV/UV + 淘宝复制),对账字段显示 "-"。
|
||||
群/活动管理 = operator;订单对账(涉佣金) = finance;只读列表/统计 = 登录即可。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -15,8 +16,6 @@ from app.admin.audit import write_audit
|
||||
from app.admin.deps import AdminDb, get_client_ip, get_current_admin, require_role
|
||||
from app.admin.repositories import cps as cps_repo
|
||||
from app.admin.schemas.common import CursorPage
|
||||
from app.core.config import settings
|
||||
from app.repositories import cps_link as cps_link_repo
|
||||
from app.admin.schemas.cps import (
|
||||
CpsActivityCreate,
|
||||
CpsActivityOut,
|
||||
@@ -26,13 +25,18 @@ from app.admin.schemas.cps import (
|
||||
CpsGroupUpdate,
|
||||
CpsOrderOut,
|
||||
CpsReconcileResult,
|
||||
CpsReferralLinkOut,
|
||||
CpsReferralLinkRequest,
|
||||
CpsReferralLinkItem,
|
||||
CpsReferralLinksOut,
|
||||
CpsReferralLinksRequest,
|
||||
CpsStatsOut,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.integrations import meituan
|
||||
from app.integrations.meituan import MeituanCpsError
|
||||
from app.models.admin import AdminUser
|
||||
from app.models.cps_activity import CpsActivity
|
||||
from app.models.cps_group import CpsGroup
|
||||
from app.repositories import cps_link as cps_link_repo
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admin/api/cps",
|
||||
@@ -40,9 +44,11 @@ router = APIRouter(
|
||||
dependencies=[Depends(get_current_admin)],
|
||||
)
|
||||
|
||||
_VALID_PLATFORMS = {"meituan", "taobao", "jd"}
|
||||
|
||||
# ───────────── 群(sid) ─────────────
|
||||
@router.get("/groups", response_model=CursorPage[CpsGroupOut], summary="群(sid)列表")
|
||||
|
||||
# ───────────── 群 ─────────────
|
||||
@router.get("/groups", response_model=CursorPage[CpsGroupOut], summary="群列表")
|
||||
def list_groups(
|
||||
db: AdminDb,
|
||||
keyword: Annotated[str | None, Query(max_length=100)] = None,
|
||||
@@ -60,22 +66,27 @@ def list_groups(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/groups", response_model=CpsGroupOut, summary="新建群(分配 sid)")
|
||||
@router.post("/groups", response_model=CpsGroupOut, summary="新建群")
|
||||
def create_group(
|
||||
body: CpsGroupCreate,
|
||||
request: Request,
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> CpsGroupOut:
|
||||
if body.sid and cps_repo.get_group_by_sid(db, body.sid) is not None:
|
||||
bad = [p for p in body.platforms if p not in _VALID_PLATFORMS]
|
||||
if bad:
|
||||
raise HTTPException(status_code=400, detail=f"非法平台: {bad}")
|
||||
has_meituan = "meituan" in body.platforms
|
||||
if has_meituan and body.sid and cps_repo.get_group_by_sid(db, body.sid) is not None:
|
||||
raise HTTPException(status_code=409, detail="sid 已存在")
|
||||
group = cps_repo.create_group(
|
||||
db, name=body.name, sid=body.sid, member_count=body.member_count,
|
||||
remark=body.remark, commit=False,
|
||||
db, name=body.name, platforms=body.platforms, sid=body.sid,
|
||||
member_count=body.member_count, remark=body.remark, commit=False,
|
||||
)
|
||||
write_audit(
|
||||
db, admin, action="cps.group.create", target_type="cps_group", target_id=group.id,
|
||||
detail={"sid": group.sid, "name": group.name}, ip=get_client_ip(request), commit=False,
|
||||
detail={"name": group.name, "platforms": group.platforms, "sid": group.sid},
|
||||
ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(group)
|
||||
@@ -93,9 +104,13 @@ def update_group(
|
||||
group = cps_repo.get_group(db, group_id)
|
||||
if group is None:
|
||||
raise HTTPException(status_code=404, detail="群不存在")
|
||||
if body.platforms is not None:
|
||||
bad = [p for p in body.platforms if p not in _VALID_PLATFORMS]
|
||||
if bad:
|
||||
raise HTTPException(status_code=400, detail=f"非法平台: {bad}")
|
||||
cps_repo.update_group(
|
||||
db, group, name=body.name, member_count=body.member_count,
|
||||
status=body.status, remark=body.remark, commit=False,
|
||||
db, group, name=body.name, platforms=body.platforms,
|
||||
member_count=body.member_count, status=body.status, remark=body.remark, commit=False,
|
||||
)
|
||||
write_audit(
|
||||
db, admin, action="cps.group.update", target_type="cps_group", target_id=group_id,
|
||||
@@ -132,15 +147,21 @@ def create_activity(
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> CpsActivityOut:
|
||||
if not body.act_id and not body.product_view_sign:
|
||||
raise HTTPException(status_code=400, detail="act_id 或 product_view_sign 必填其一")
|
||||
if body.platform == "meituan":
|
||||
if not body.act_id and not body.product_view_sign:
|
||||
raise HTTPException(status_code=400, detail="美团活动需填 actId 或 productViewSign")
|
||||
else: # taobao / jd
|
||||
if not body.payload:
|
||||
label = "淘口令" if body.platform == "taobao" else "推广链接"
|
||||
raise HTTPException(status_code=400, detail=f"{body.platform} 活动需填{label}")
|
||||
activity = cps_repo.create_activity(
|
||||
db, name=body.name, platform=body.platform, act_id=body.act_id,
|
||||
product_view_sign=body.product_view_sign, remark=body.remark, commit=False,
|
||||
product_view_sign=body.product_view_sign, payload=body.payload,
|
||||
remark=body.remark, commit=False,
|
||||
)
|
||||
write_audit(
|
||||
db, admin, action="cps.activity.create", target_type="cps_activity", target_id=activity.id,
|
||||
detail={"name": activity.name, "act_id": activity.act_id, "platform": activity.platform},
|
||||
detail={"name": activity.name, "platform": activity.platform},
|
||||
ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
db.commit()
|
||||
@@ -148,65 +169,71 @@ def create_activity(
|
||||
return CpsActivityOut.model_validate(activity)
|
||||
|
||||
|
||||
# ───────────── 转链 ─────────────
|
||||
@router.post("/referral-link", response_model=CpsReferralLinkOut, summary="生成群发短链(带 sid + 点击统计)")
|
||||
def generate_referral_link(
|
||||
body: CpsReferralLinkRequest,
|
||||
# ───────────── 生成链接(批量,按平台分支) ─────────────
|
||||
def _gen_one_link(db, group: CpsGroup, activity: CpsActivity):
|
||||
"""给一个活动生成一条落地页 link。美团:转链拿短链(需群 sid);淘宝/京东:用 payload。"""
|
||||
if activity.platform == "meituan":
|
||||
if not group.sid:
|
||||
raise HTTPException(status_code=400, detail=f"群「{group.name}」无 sid,无法生成美团链接")
|
||||
try:
|
||||
resp = meituan.get_referral_link(
|
||||
act_id=activity.act_id, product_view_sign=activity.product_view_sign,
|
||||
sid=group.sid, link_type_list=[1, 2, 3],
|
||||
)
|
||||
except MeituanCpsError as e:
|
||||
raise HTTPException(status_code=502, detail=f"美团转链失败: {e}") from e
|
||||
link_map = {str(k): v for k, v in (resp.get("referralLinkMap") or {}).items()}
|
||||
if not link_map and resp.get("data"):
|
||||
link_map = {"1": resp["data"]}
|
||||
target = link_map.get("2") or link_map.get("1") or link_map.get("3")
|
||||
if not target:
|
||||
raise HTTPException(status_code=502, detail=f"美团未返回有效链接(活动「{activity.name}」)")
|
||||
else: # taobao(淘口令) / jd(链接)
|
||||
if not activity.payload:
|
||||
raise HTTPException(status_code=400, detail=f"活动「{activity.name}」缺少内容")
|
||||
target = activity.payload
|
||||
return cps_link_repo.create_link(
|
||||
db, group_id=group.id, activity_id=activity.id, sid=group.sid,
|
||||
target_url=target, platform=activity.platform, commit=False,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/referral-links", response_model=CpsReferralLinksOut, summary="批量生成落地页短链")
|
||||
def generate_referral_links(
|
||||
body: CpsReferralLinksRequest,
|
||||
request: Request,
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> CpsReferralLinkOut:
|
||||
) -> CpsReferralLinksOut:
|
||||
group = cps_repo.get_group(db, body.group_id)
|
||||
if group is None:
|
||||
raise HTTPException(status_code=404, detail="群不存在")
|
||||
activity = cps_repo.get_activity(db, body.activity_id)
|
||||
if activity is None:
|
||||
raise HTTPException(status_code=404, detail="活动不存在")
|
||||
if activity.platform != "meituan":
|
||||
raise HTTPException(status_code=400, detail=f"当前仅支持美团转链(活动平台={activity.platform})")
|
||||
try:
|
||||
# 全要:短链作跳转目标(微信可打开),长链/deeplink 作参考/备用
|
||||
resp = meituan.get_referral_link(
|
||||
act_id=activity.act_id,
|
||||
product_view_sign=activity.product_view_sign,
|
||||
sid=group.sid,
|
||||
link_type_list=[1, 2, 3],
|
||||
)
|
||||
except MeituanCpsError as e:
|
||||
raise HTTPException(status_code=502, detail=f"美团转链失败: {e}") from e
|
||||
group_platforms = set(group.platforms or [])
|
||||
base = settings.CPS_REDIRECT_BASE.rstrip("/")
|
||||
|
||||
link_map = {str(k): v for k, v in (resp.get("referralLinkMap") or {}).items()}
|
||||
if not link_map and resp.get("data"):
|
||||
link_map = {"1": resp["data"]}
|
||||
# 跳转目标优先短链(2,微信可打开)→ 长链(1)→ deeplink(3)
|
||||
target_url = link_map.get("2") or link_map.get("1") or link_map.get("3")
|
||||
if not target_url:
|
||||
raise HTTPException(status_code=502, detail="美团未返回有效链接(检查 actId 是否在推广有效期)")
|
||||
items: list[CpsReferralLinkItem] = []
|
||||
for aid in body.activity_ids:
|
||||
activity = cps_repo.get_activity(db, aid)
|
||||
if activity is None:
|
||||
raise HTTPException(status_code=404, detail=f"活动 {aid} 不存在")
|
||||
if activity.platform not in group_platforms:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"活动「{activity.name}」平台({activity.platform})不在群「{group.name}」范围内",
|
||||
)
|
||||
link = _gen_one_link(db, group, activity)
|
||||
items.append(CpsReferralLinkItem(
|
||||
activity_id=activity.id, activity_name=activity.name, platform=activity.platform,
|
||||
redirect_url=f"{base}/c/{link.code}" if base else f"/c/{link.code}", code=link.code,
|
||||
))
|
||||
|
||||
# 存我们的短链:发群用 /c/{code},用户点 → 我们记点击 → 302 跳 target_url
|
||||
link = cps_link_repo.create_link(
|
||||
db, group_id=group.id, activity_id=activity.id, sid=group.sid,
|
||||
target_url=target_url, platform=activity.platform, commit=False,
|
||||
)
|
||||
write_audit(
|
||||
db, admin, action="cps.referral_link.generate", target_type="cps_link", target_id=link.id,
|
||||
detail={"sid": group.sid, "activity_id": activity.id, "code": link.code},
|
||||
db, admin, action="cps.referral_link.generate", target_type="cps_group", target_id=group.id,
|
||||
detail={"group": group.name, "activity_ids": body.activity_ids, "count": len(items)},
|
||||
ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
db.commit()
|
||||
db.refresh(link)
|
||||
|
||||
base = settings.CPS_REDIRECT_BASE.rstrip("/")
|
||||
redirect_url = f"{base}/c/{link.code}" if base else f"/c/{link.code}"
|
||||
return CpsReferralLinkOut(
|
||||
redirect_url=redirect_url,
|
||||
code=link.code,
|
||||
sid=group.sid,
|
||||
group_name=group.name,
|
||||
activity_name=activity.name,
|
||||
target_url=target_url,
|
||||
link_map=link_map,
|
||||
)
|
||||
return CpsReferralLinksOut(group_name=group.name, results=items)
|
||||
|
||||
|
||||
# ───────────── 订单对账 ─────────────
|
||||
@@ -262,7 +289,7 @@ def get_stats(
|
||||
stats = [CpsGroupStat(**r) for r in rows]
|
||||
return CpsStatsOut(
|
||||
groups=stats,
|
||||
total_order_count=sum(s.order_count for s in stats),
|
||||
total_est_commission_cents=sum(s.est_commission_cents for s in stats),
|
||||
total_settled_commission_cents=sum(s.settled_commission_cents for s in stats),
|
||||
total_order_count=sum(s.order_count or 0 for s in stats),
|
||||
total_est_commission_cents=sum(s.est_commission_cents or 0 for s in stats),
|
||||
total_settled_commission_cents=sum(s.settled_commission_cents or 0 for s in stats),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user