"""推广归因路由(/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())