feat: 接入数据大盘聚合与美团 CPS 对账
改了什么:新增新版大盘日期窗口聚合、广告收益拆分、比价耗时字段、美团 CPS 拉单入库与大盘展示,并同步反馈审核和邀请奖励下线口径。 验证:python -m pytest tests/test_admin_read.py tests/test_admin_write.py tests/test_compare_record.py tests/test_invite.py tests/test_feedback.py -q。
This commit is contained in:
+4
-3
@@ -241,11 +241,12 @@ def ecpm_report(payload: EcpmReportIn, user: CurrentUser, db: DbSession) -> Ecpm
|
||||
db, user.id,
|
||||
ad_type=payload.ad_type, ecpm_raw=payload.ecpm,
|
||||
ad_session_id=payload.ad_session_id,
|
||||
adn=payload.adn, slot_id=payload.slot_id,
|
||||
adn=payload.adn, slot_id=payload.slot_id, feed_scene=payload.feed_scene,
|
||||
)
|
||||
logger.info(
|
||||
"ad ecpm report user_id=%d type=%s session=%s ecpm=%s adn=%s slot=%s",
|
||||
user.id, payload.ad_type, payload.ad_session_id, payload.ecpm, payload.adn, payload.slot_id,
|
||||
"ad ecpm report user_id=%d type=%s scene=%s session=%s ecpm=%s adn=%s slot=%s",
|
||||
user.id, payload.ad_type, payload.feed_scene, payload.ad_session_id,
|
||||
payload.ecpm, payload.adn, payload.slot_id,
|
||||
)
|
||||
return EcpmReportOut(ok=True)
|
||||
|
||||
|
||||
+59
-6
@@ -1,7 +1,8 @@
|
||||
"""帮助与反馈 endpoint。
|
||||
|
||||
路由前缀 `/api/v1/feedback`,需 Bearer 鉴权(反馈绑到登录用户,便于回访)。
|
||||
POST / 提交反馈(multipart:content / contact 必填,images 可选 ≤4 张)
|
||||
POST / 提交反馈(multipart:content,images 可选 ≤4 张)
|
||||
GET /records 我的反馈历史(pending/adopted/rejected)
|
||||
|
||||
截图复用 [app.core.media] 落盘到 /media/feedback/。
|
||||
"""
|
||||
@@ -9,12 +10,18 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Query, UploadFile
|
||||
|
||||
from app.api.deps import CurrentUser, DbSession
|
||||
from app.core import media
|
||||
from app.repositories import feedback as feedback_repo
|
||||
from app.schemas.feedback import FeedbackOut
|
||||
from app.schemas.feedback import (
|
||||
FeedbackConfigOut,
|
||||
FeedbackOut,
|
||||
FeedbackRecordCountsOut,
|
||||
FeedbackRecordOut,
|
||||
FeedbackRecordsOut,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("shagua.feedback")
|
||||
|
||||
@@ -23,6 +30,52 @@ router = APIRouter(prefix="/api/v1/feedback", tags=["feedback"])
|
||||
_MAX_IMAGES = 4
|
||||
_CONTENT_MAX = 2000
|
||||
_CONTACT_MAX = 128
|
||||
_VALID_RECORD_STATUS = {"pending", "adopted", "rejected"}
|
||||
|
||||
|
||||
def _app_status(db_status: str) -> str:
|
||||
return {
|
||||
"new": "pending",
|
||||
"handled": "adopted",
|
||||
"approved": "adopted",
|
||||
}.get(db_status, db_status)
|
||||
|
||||
|
||||
def _record_out(fb) -> FeedbackRecordOut:
|
||||
return FeedbackRecordOut(
|
||||
id=fb.id,
|
||||
content=fb.content,
|
||||
images=fb.images or [],
|
||||
status=_app_status(fb.status),
|
||||
reject_reason=getattr(fb, "reject_reason", None),
|
||||
reward_coins=getattr(fb, "reward_coins", None),
|
||||
created_at=fb.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/config", response_model=FeedbackConfigOut, summary="反馈页配置")
|
||||
def feedback_config(_: CurrentUser) -> FeedbackConfigOut:
|
||||
return FeedbackConfigOut()
|
||||
|
||||
|
||||
@router.get("/records", response_model=FeedbackRecordsOut, summary="我的反馈历史")
|
||||
def feedback_records(
|
||||
user: CurrentUser,
|
||||
db: DbSession,
|
||||
status: str | None = Query(default=None),
|
||||
) -> FeedbackRecordsOut:
|
||||
if status is not None and status not in _VALID_RECORD_STATUS:
|
||||
raise HTTPException(status_code=400, detail="invalid status")
|
||||
|
||||
all_records = [_record_out(fb) for fb in feedback_repo.list_feedback(db, user_id=user.id)]
|
||||
counts = FeedbackRecordCountsOut(
|
||||
all=len(all_records),
|
||||
pending=sum(1 for r in all_records if r.status == "pending"),
|
||||
adopted=sum(1 for r in all_records if r.status == "adopted"),
|
||||
rejected=sum(1 for r in all_records if r.status == "rejected"),
|
||||
)
|
||||
records = [r for r in all_records if r.status == status] if status else all_records
|
||||
return FeedbackRecordsOut(records=records, counts=counts)
|
||||
|
||||
|
||||
@router.post("", response_model=FeedbackOut, summary="提交反馈")
|
||||
@@ -30,7 +83,9 @@ async def submit_feedback(
|
||||
user: CurrentUser,
|
||||
db: DbSession,
|
||||
content: str = Form(...),
|
||||
contact: str = Form(...),
|
||||
# 原型改版后 App 已去掉"联系方式"字段,故 contact 改为可选(默认空串);
|
||||
# 仍兼容旧端/后台带 contact 的提交。DB 列为 NOT NULL,空串即可满足。
|
||||
contact: str = Form(default=""),
|
||||
images: list[UploadFile] = File(default=[]),
|
||||
) -> FeedbackOut:
|
||||
content = content.strip()
|
||||
@@ -39,8 +94,6 @@ async def submit_feedback(
|
||||
raise HTTPException(status_code=400, detail="反馈内容不能为空")
|
||||
if len(content) > _CONTENT_MAX:
|
||||
raise HTTPException(status_code=400, detail="反馈内容过长")
|
||||
if not contact:
|
||||
raise HTTPException(status_code=400, detail="联系方式不能为空")
|
||||
if len(contact) > _CONTACT_MAX:
|
||||
raise HTTPException(status_code=400, detail="联系方式过长")
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
路由前缀 /api/v1/invite,需 Bearer 鉴权(用户级数据);唯一例外是 /landing-track
|
||||
(落地页 dl.html 浏览器访问、无 token,详见任务 3 [[invite-three-tasks]]):
|
||||
GET /me 我的邀请码 + 分享链接 + 已邀人数/已得金币(邀请页展示)
|
||||
GET /me 我的邀请码 + 分享链接 + 已邀人数(邀请页展示)
|
||||
POST /bind 把当前登录用户(被邀请人)绑定到某邀请码;支持三种归因:
|
||||
① clipboard:首启读剪贴板拿邀请码 → 上报码
|
||||
② manual:用户在邀请页输入邀请码 → 上报码
|
||||
③ fingerprint:剪贴板没拿到时上报指纹,后端反查
|
||||
POST /landing-track 落地页 dl.html 访问时上报指纹(剪贴板兜底的服务端一端,无需鉴权)
|
||||
|
||||
绑定的真正逻辑(邀请码生成/反查、幂等、自邀屏蔽、发金币、指纹反查)在
|
||||
绑定的真正逻辑(邀请码生成/反查、幂等、自邀屏蔽、指纹反查)在
|
||||
repositories/invite.py。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -38,7 +38,7 @@ logger = logging.getLogger("shagua.invite")
|
||||
router = APIRouter(prefix="/api/v1/invite", tags=["invite"])
|
||||
|
||||
_BIND_MESSAGES = {
|
||||
"success": "邀请绑定成功,金币已到账",
|
||||
"success": "邀请绑定成功",
|
||||
"already_bound": "你已绑定过邀请人",
|
||||
"invalid_code": "邀请码无效",
|
||||
"self_invite": "不能填写自己的邀请码",
|
||||
@@ -154,7 +154,7 @@ def landing_track(
|
||||
return LandingTrackOut(status="ok")
|
||||
|
||||
|
||||
@router.post("/bind", response_model=BindInviteOut, summary="绑定邀请人(注册即生效,双方发金币)")
|
||||
@router.post("/bind", response_model=BindInviteOut, summary="绑定邀请人(注册即生效,不发邀请金币)")
|
||||
def bind_invite(
|
||||
req: BindInviteIn, user: CurrentUser, db: DbSession, request: Request
|
||||
) -> BindInviteOut:
|
||||
|
||||
Reference in New Issue
Block a user