46 lines
2.0 KiB
Python
46 lines
2.0 KiB
Python
"""推广归因路由(/invite/* + H5 中间页 /r/{code})
|
||
|
||
三段式归因:分享口令(主)→ H5 点击埋点模糊匹配(辅)→ 注册手填邀请码(兜底)
|
||
"""
|
||
import redis.asyncio as aioredis
|
||
from fastapi import APIRouter, Depends, Request
|
||
from fastapi.responses import HTMLResponse
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.core.response import ok
|
||
from app.database import get_db
|
||
from app.redis_client import get_redis
|
||
from app.schemas.invite import InviteClickReq, InviteMatchReq
|
||
from app.services import invite_service
|
||
|
||
router = APIRouter(prefix="/invite", tags=["推广归因"])
|
||
router_page = APIRouter(tags=["推广落地页"], include_in_schema=False)
|
||
|
||
|
||
def _client_ip(request: Request) -> str:
|
||
# Nginx 反代后取真实 IP
|
||
return request.headers.get("X-Real-IP") or (request.client.host if request.client else "")
|
||
|
||
|
||
@router.post("/click")
|
||
async def invite_click(req: InviteClickReq, request: Request,
|
||
redis: aioredis.Redis = Depends(get_redis)):
|
||
"""H5 中间页点击埋点(打开页面即上报,记录 code+IP+UA,24h 有效)"""
|
||
ua = request.headers.get("User-Agent", "")
|
||
return ok(await invite_service.record_click(redis, req.code.upper(), _client_ip(request), ua))
|
||
|
||
|
||
@router.post("/match")
|
||
async def invite_match(req: InviteMatchReq, request: Request,
|
||
redis: aioredis.Redis = Depends(get_redis)):
|
||
"""App 首启归因匹配:按 IP+时间就近匹配点击记录,结果按 device_id 服务端缓存"""
|
||
ua = request.headers.get("User-Agent", "")
|
||
return ok(await invite_service.match_and_cache(redis, req.device_id, _client_ip(request), ua))
|
||
|
||
|
||
@router_page.get("/r/{code}", response_class=HTMLResponse)
|
||
async def invite_landing(code: str, db: AsyncSession = Depends(get_db),
|
||
redis: aioredis.Redis = Depends(get_redis)):
|
||
"""推广落地页(复制口令 + 下载入口,打开即自动埋点)"""
|
||
return await invite_service.render_landing_page(db, redis, code.upper())
|