Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6e5ce2f6e |
+1
-1
@@ -29,7 +29,7 @@ JG_REQUEST_TIMEOUT_SEC=15
|
||||
|
||||
# ===== 无障碍保护存活监控(pull 后置检测;本期不接推送)=====
|
||||
HEARTBEAT_MONITOR_ENABLED=true
|
||||
HEARTBEAT_TIMEOUT_MINUTES=60
|
||||
HEARTBEAT_TIMEOUT_MINUTES=10
|
||||
HEARTBEAT_SCAN_INTERVAL_SEC=60
|
||||
|
||||
# ===== 短信 (mock 模式) =====
|
||||
|
||||
@@ -48,12 +48,3 @@ secrets/*
|
||||
# 运行日志(run.sh 输出, 不入库)
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Claude Code 自动持久化的权限 allowlist / 个人本地设置(会话专属,不入库)。
|
||||
# 需要团队共享的 Claude 配置(commands/ 等)可单独 git add -f,不受此忽略影响。
|
||||
.claude/settings.json
|
||||
.claude/settings.local.json
|
||||
tests/meituan_coupon_bj.tsv
|
||||
tests/meituan_coupon_data.tsv
|
||||
tests/meituan_coupon_fz.tsv
|
||||
tests/meituan_coupon_xm.tsv
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""merge jd_cps_order_fields and coupon_session_origin_package heads
|
||||
|
||||
Revision ID: 761ef181ce7c
|
||||
Revises: coupon_session_origin_package, jd_cps_order_fields
|
||||
Create Date: 2026-07-01 13:52:16.068808
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '761ef181ce7c'
|
||||
down_revision: Union[str, Sequence[str], None] = ('coupon_session_origin_package', 'jd_cps_order_fields')
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1,32 +0,0 @@
|
||||
"""coupon_session 加 origin_package 列(发起来源 App 包名 → admin「发起平台」)
|
||||
|
||||
null=App 内(傻瓜比价首页)发起,非空=从美团/淘宝/京东弹券发起。与 trace_url 同理单独成迁移,
|
||||
已建表环境靠它补列、全新环境顺序应用,不重复加列。
|
||||
|
||||
Revision ID: coupon_session_origin_package
|
||||
Revises: coupon_session_trace_url
|
||||
Create Date: 2026-06-30 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "coupon_session_origin_package"
|
||||
down_revision: str | Sequence[str] | None = "coupon_session_trace_url"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.batch_alter_table("coupon_session", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("origin_package", sa.String(length=64), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("coupon_session", schema=None) as batch_op:
|
||||
batch_op.drop_column("origin_package")
|
||||
@@ -1,81 +0,0 @@
|
||||
"""coupon session table(领券任务全程流水 → admin「领券数据」看板数据源)
|
||||
|
||||
一次领券一行(trace_id 唯一):客户端 POST /api/v1/coupon/session 两段上报 —— 发起建行
|
||||
(status=started)、收尾(completed/failed/abandoned)按 trace_id 更新同一行。记全程耗时
|
||||
elapsed_ms + 各平台耗时 platform_elapsed + 机型/ROM,供 admin 算发起/完成数、耗时分位、机型维度。
|
||||
|
||||
Revision ID: coupon_session_table
|
||||
Revises: feedback_submit_env
|
||||
Create Date: 2026-06-30 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "coupon_session_table"
|
||||
down_revision: str | Sequence[str] | None = "feedback_submit_env"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
# PG 用 JSONB,SQLite(本地/测试)退化为通用 JSON(同 model 的 _JSON variant)。
|
||||
_JSON = sa.JSON().with_variant(postgresql.JSONB(), "postgresql")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"coupon_session",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("trace_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("device_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("app_env", sa.String(length=16), nullable=True),
|
||||
sa.Column("platforms", _JSON, nullable=True),
|
||||
sa.Column("device_model", sa.String(length=128), nullable=True),
|
||||
sa.Column("rom", sa.String(length=64), nullable=True),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("started_date", sa.Date(), nullable=False),
|
||||
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("elapsed_ms", sa.Integer(), nullable=True),
|
||||
sa.Column("platform_elapsed", _JSON, nullable=True),
|
||||
sa.Column("claimed_count", sa.Integer(), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("(CURRENT_TIMESTAMP)"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("trace_id", name="uq_coupon_session_trace"),
|
||||
)
|
||||
with op.batch_alter_table("coupon_session", schema=None) as batch_op:
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_coupon_session_user_id"), ["user_id"], unique=False
|
||||
)
|
||||
batch_op.create_index(
|
||||
batch_op.f("ix_coupon_session_app_env"), ["app_env"], unique=False
|
||||
)
|
||||
# admin 主聚合/筛选:按上海自然日 + 环境。
|
||||
batch_op.create_index(
|
||||
"ix_coupon_session_date_env", ["started_date", "app_env"], unique=False
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("coupon_session", schema=None) as batch_op:
|
||||
batch_op.drop_index("ix_coupon_session_date_env")
|
||||
batch_op.drop_index(batch_op.f("ix_coupon_session_app_env"))
|
||||
batch_op.drop_index(batch_op.f("ix_coupon_session_user_id"))
|
||||
op.drop_table("coupon_session")
|
||||
@@ -1,32 +0,0 @@
|
||||
"""coupon_session 加 trace_url 列(pricebot done 帧公网调试链接)
|
||||
|
||||
建表迁移 coupon_session_table 落地后才追加本列,故单独成一个迁移:已建表的环境(本地/已 upgrade 过)
|
||||
靠它补列,全新环境则「建表(无 trace_url)→ 本迁移加列」,两条路一致、不重复加列。
|
||||
|
||||
Revision ID: coupon_session_trace_url
|
||||
Revises: coupon_session_table
|
||||
Create Date: 2026-06-30 00:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "coupon_session_trace_url"
|
||||
down_revision: str | Sequence[str] | None = "coupon_session_table"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.batch_alter_table("coupon_session", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("trace_url", sa.String(length=512), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("coupon_session", schema=None) as batch_op:
|
||||
batch_op.drop_column("trace_url")
|
||||
@@ -1,48 +0,0 @@
|
||||
"""feedback 加反馈来源/场景/运营回复(source / scene / admin_reply)
|
||||
|
||||
admin「用户反馈」页要展示并筛选「反馈类型」(比价反馈 / 普通反馈),并支持审核时给用户留言:
|
||||
- source: 反馈来源入口(profile=「我的」页 / comparison=比价结果页)。NOT NULL,
|
||||
旧数据 + 普通反馈默认 profile(server_default)。加索引供 admin 按类型筛选。
|
||||
- scene: 比价反馈的问题场景(找错商品/优惠不对…),普通反馈为 NULL。
|
||||
- admin_reply: 运营给用户的回复留言(用户端可见),随「我的反馈」历史下发。
|
||||
|
||||
均为新增列(SQLite 原生支持 add_column);downgrade 的 drop_column 在 SQLite 走 batch 兜底。
|
||||
|
||||
Revision ID: feedback_type_reply
|
||||
Revises: 761ef181ce7c
|
||||
Create Date: 2026-07-02 00:00:00.000000
|
||||
"""
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "feedback_type_reply"
|
||||
down_revision: str | Sequence[str] | None = "761ef181ce7c"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"feedback",
|
||||
sa.Column(
|
||||
"source",
|
||||
sa.String(length=16),
|
||||
nullable=False,
|
||||
server_default="profile",
|
||||
),
|
||||
)
|
||||
op.add_column("feedback", sa.Column("scene", sa.String(length=32), nullable=True))
|
||||
op.add_column("feedback", sa.Column("admin_reply", sa.String(length=256), nullable=True))
|
||||
op.create_index("ix_feedback_source", "feedback", ["source"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("feedback") as batch_op:
|
||||
batch_op.drop_index("ix_feedback_source")
|
||||
batch_op.drop_column("admin_reply")
|
||||
batch_op.drop_column("scene")
|
||||
batch_op.drop_column("source")
|
||||
@@ -21,7 +21,6 @@ from app.admin.routers.audit import router as audit_router
|
||||
from app.admin.routers.auth import router as auth_router
|
||||
from app.admin.routers.comparison import router as comparison_router
|
||||
from app.admin.routers.config import router as config_router
|
||||
from app.admin.routers.coupon_data import router as coupon_data_router
|
||||
from app.admin.routers.cps import router as cps_router
|
||||
from app.admin.routers.dashboard import router as dashboard_router
|
||||
from app.admin.routers.device_liveness import router as device_liveness_router
|
||||
@@ -102,7 +101,6 @@ admin_app.include_router(audit_router)
|
||||
admin_app.include_router(config_router)
|
||||
admin_app.include_router(comparison_router)
|
||||
admin_app.include_router(cps_router)
|
||||
admin_app.include_router(coupon_data_router)
|
||||
admin_app.include_router(ad_audit_router)
|
||||
admin_app.include_router(ad_config_router)
|
||||
admin_app.include_router(ad_revenue_router)
|
||||
|
||||
@@ -136,16 +136,12 @@ def _feed_scene_matches(rec: AdFeedRewardRecord, scene: str | None) -> bool:
|
||||
"""该信息流记录是否落入请求的展示筛选 scene。
|
||||
- scene=="feed":ad_type in ("feed", NULL)(旧数据 NULL 视为 feed,向后兼容)
|
||||
- scene=="draw":ad_type=="draw"
|
||||
- scene=="feed_all":所有信息流(feed/draw/NULL 都要)——业务已全切 Draw 信息流,收益报表把「Draw 信息流」
|
||||
当作整个信息流口径(含历史误标 feed/NULL),用它避免筛选漏历史。
|
||||
- scene 为 None:不筛(两类都要)。
|
||||
"""
|
||||
if scene == "feed":
|
||||
return rec.ad_type in (None, "feed")
|
||||
if scene == "draw":
|
||||
return rec.ad_type == "draw"
|
||||
if scene == "feed_all":
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
@@ -188,7 +184,6 @@ def _feed_rows(
|
||||
"record_id": rec.id,
|
||||
"user_id": rec.user_id,
|
||||
"ad_session_id": rec.ad_session_id,
|
||||
"trace_id": rec.trace_id,
|
||||
"app_env": rec.app_env,
|
||||
"our_code_id": rec.our_code_id,
|
||||
"created_at": rec.created_at,
|
||||
@@ -214,7 +209,6 @@ def _feed_rows(
|
||||
"record_id": rec.id,
|
||||
"user_id": rec.user_id,
|
||||
"ad_session_id": rec.ad_session_id,
|
||||
"trace_id": rec.trace_id,
|
||||
"app_env": rec.app_env,
|
||||
"our_code_id": rec.our_code_id,
|
||||
"created_at": rec.created_at,
|
||||
@@ -247,7 +241,7 @@ def audit_rows(
|
||||
rows: list[dict] = []
|
||||
if scene in (None, "reward_video"):
|
||||
rows.extend(_reward_video_rows(db, date=date, user_id=user_id))
|
||||
if scene in (None, "feed", "draw", "feed_all"):
|
||||
if scene in (None, "feed", "draw"):
|
||||
rows.extend(_feed_rows(db, date=date, user_id=user_id, scene=scene))
|
||||
return rows
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
只读。每行 = 一次广告事件(不再按用户聚合):
|
||||
- **激励视频**:一次观看 = 1 条展示(ad_ecpm)+ 1 条发奖(ad_reward),按 ad_session_id 合并成一行,
|
||||
直接给出 eCPM / 收益 + 状态 / 应发 / 实发 / 一致;点开看该条金币复算因子。
|
||||
- **信息流(比价/领券)**:一次比价 / 一次领券 = 一条整场发奖(ad_feed_reward)一行,给出 eCPM /
|
||||
发奖金币 + 应发 / 实发 / 一致;点开看金币复算因子。⚠️ draw 的逐条展示(ad_ecpm,impressionId 各自
|
||||
独立、与整场发奖无公共键、无法归到「哪一次」)**不再单独占行**(2026-07 按「一次比价/领券放一块」调整)——
|
||||
其展示数 / eCPM / 预估收益仍进全量统计(合计 / 趋势 / 分类大盘 / 穿山甲对照),只是主表不逐条铺开。
|
||||
- 兜底:激励视频有展示无发奖(中途关 / 未达发奖)、有发奖无展示(未上报 eCPM)仍各自成行。
|
||||
- **信息流**:轮播每条展示各一行(impressionId 各自独立);整场发奖(ad_feed_reward,client_event_id)
|
||||
与逐条展示无法对应,单独成「纯发奖」行。
|
||||
- 兜底:有展示无发奖(中途关 / 未达发奖)、有发奖无展示(未上报 eCPM)都各自成行。
|
||||
|
||||
展示与收益来自 ad_ecpm_record(收益 = eCPM元 ÷ 1000);应发 / 实发金币复用金币审计逐条复算
|
||||
(ad_audit.audit_rows,与正式发奖同一公式口径,不另写公式)。合计与对账在全量上统计,
|
||||
@@ -60,6 +58,14 @@ def _date_range(date_from: str, date_to: str) -> list[str]:
|
||||
_AUDIT_SCENES = {"reward_video", "feed", "draw"}
|
||||
|
||||
|
||||
def _event_ad_type(row: dict) -> str:
|
||||
"""纯发奖事件行的 ad_type:信息流行用 audit 带回的真实 ad_type(feed/draw),回退 feed;
|
||||
激励视频行恒 reward_video。不再用 scene 硬映射,避免把 draw 丢成 feed。"""
|
||||
if row["scene"] == "reward_video":
|
||||
return "reward_video"
|
||||
return row.get("ad_type") or "feed"
|
||||
|
||||
|
||||
# 发奖复算明细字段(展开下钻看「金币怎么算出来的」)——从 audit 行原样取这些 key。
|
||||
_REWARD_DETAIL_KEYS = (
|
||||
"record_id", "created_at", "status", "ecpm", "ecpm_factor", "units",
|
||||
@@ -102,14 +108,9 @@ def ad_revenue_report(
|
||||
# 同时保留全量列表,未被展示合并的成「纯发奖」事件。
|
||||
reward_by_session: dict[tuple[int, str], list[dict]] = {}
|
||||
all_reward_rows: list[dict] = []
|
||||
# 报表 ad_type → audit scene:reward_video/feed 直传;**draw(前端「Draw 信息流」)映射成 feed_all**
|
||||
# ——业务已全切 Draw,把「Draw 信息流」当作整个信息流口径(含历史误标 feed/NULL),否则筛选会漏历史。
|
||||
if ad_type == "draw":
|
||||
audit_scene = "feed_all"
|
||||
elif ad_type in _AUDIT_SCENES:
|
||||
audit_scene = ad_type
|
||||
else:
|
||||
audit_scene = None
|
||||
# 报表 ad_type 直接当 audit scene 用(取值一致);未知/无效 ad_type 不取发奖行。draw 在此被
|
||||
# 正确传成 scene="draw",audit 会按 ad_type 筛出 Draw 发奖,不再丢成 feed。
|
||||
audit_scene = ad_type if ad_type in _AUDIT_SCENES else None
|
||||
if ad_type is None or audit_scene is not None:
|
||||
for d in _date_range(date_from, date_to):
|
||||
for row in ad_audit.audit_rows(db, date=d, user_id=user_id, scene=audit_scene):
|
||||
@@ -139,10 +140,7 @@ def ad_revenue_report(
|
||||
)
|
||||
if user_id is not None:
|
||||
stmt = stmt.where(AdEcpmRecord.user_id == user_id)
|
||||
if ad_type == "draw":
|
||||
# draw = 所有信息流展示(业务已全 Draw,含历史误标 feed);展示行只进统计,不占主表行
|
||||
stmt = stmt.where(AdEcpmRecord.ad_type.in_(["draw", "feed"]))
|
||||
elif ad_type is not None:
|
||||
if ad_type is not None:
|
||||
stmt = stmt.where(AdEcpmRecord.ad_type == ad_type)
|
||||
for rec in db.execute(stmt).scalars():
|
||||
rwd = _pop_reward(rec.user_id, rec.ad_session_id)
|
||||
@@ -167,8 +165,6 @@ def ad_revenue_report(
|
||||
),
|
||||
"adn": rec.adn,
|
||||
"slot_id": rec.slot_id,
|
||||
"sub_rewards": [],
|
||||
"sub_count": 1,
|
||||
}
|
||||
if rwd is not None:
|
||||
ev.update({
|
||||
@@ -188,88 +184,33 @@ def ad_revenue_report(
|
||||
})
|
||||
events.append(ev)
|
||||
|
||||
# 3) 未被展示合并的发奖行 → 事件:
|
||||
# - 激励视频(reward_video):逐条成「纯发奖」事件(每次一个 ad_session_id;有发奖无展示等)。
|
||||
# - 信息流(feed/draw):同一次比价/领券的多条广告共享**整场 ad_session_id**(客户端整场复用),
|
||||
# 按 (user_id, ad_session_id) 聚成**一次比价 / 一次领券**父事件;sub_rewards 为组内逐条明细,
|
||||
# 应发/实发取组内合计;业务已全 Draw → 类型统一 "draw"。session 缺失(极少旧数据)各自单独成组。
|
||||
feed_groups: dict[tuple[int, str], list[dict]] = {}
|
||||
# 3) 未被展示合并的发奖行 → 「纯发奖」事件(信息流整场发奖 / 有发奖无展示)。
|
||||
# 收益恒 0(收益只算展示侧,避免与展示行重复计)。
|
||||
for row in all_reward_rows:
|
||||
if row["record_id"] in used_reward_ids:
|
||||
continue
|
||||
if row["scene"] == "reward_video":
|
||||
events.append({
|
||||
"event_key": f"rwd-{row['record_id']}",
|
||||
"report_date": row["_report_date"],
|
||||
"user_id": row["user_id"],
|
||||
"ad_type": "reward_video",
|
||||
"feed_scene": row.get("feed_scene"),
|
||||
"app_env": row.get("app_env"),
|
||||
"our_code_id": row.get("our_code_id"),
|
||||
"created_at": row["created_at"],
|
||||
"hour": _cn_hour(row["created_at"]) if by_hour else None,
|
||||
"has_impression": False,
|
||||
"impressions": 0,
|
||||
"ecpm": row["ecpm"],
|
||||
"revenue_yuan": 0.0,
|
||||
"adn": None,
|
||||
"slot_id": None,
|
||||
"has_reward": True,
|
||||
"status": row["status"],
|
||||
"expected_coin": int(row["expected_coin"]),
|
||||
"actual_coin": int(row["actual_coin"]),
|
||||
"matched": bool(row["matched"]),
|
||||
"reward_detail": _reward_detail(row),
|
||||
"sub_rewards": [],
|
||||
"sub_count": 1,
|
||||
})
|
||||
else:
|
||||
# 聚合单位 = 一次完整比价/领券流程:优先用 trace_id(比价带 comparisonTraceId、领券带 sessionTraceId,
|
||||
# 整个流程不变;即使中途点广告致浮层关闭重弹、ad_session_id 变了,trace_id 仍不变 → 全流程聚成一行)。
|
||||
# 无 trace_id(历史领券未上报 / 旧数据)回退整场 ad_session_id;再无则 record_id 各自成组、不误并。
|
||||
grp_key = row.get("trace_id") or row.get("ad_session_id") or f"_rid-{row['record_id']}"
|
||||
feed_groups.setdefault((row["user_id"], grp_key), []).append(row)
|
||||
|
||||
# 信息流分组 → 「一次比价 / 一次领券」父事件(收益恒 0:收益只算展示侧,避免与展示行重复计)。
|
||||
for (uid, grp_key), group in feed_groups.items():
|
||||
group.sort(key=lambda r: (r["created_at"], r["record_id"]))
|
||||
rep = group[-1] # 代表条(最新一条):时间/场景/应用/代码位取它
|
||||
expected_sum = sum(int(g["expected_coin"]) for g in group)
|
||||
actual_sum = sum(int(g["actual_coin"]) for g in group)
|
||||
# 父行 eCPM:组内各条 eCPM(分)均值(展示用,各条不同);无有效值则取代表条
|
||||
ecpm_fens = [rewards.parse_ecpm_fen(g["ecpm"]) for g in group if g.get("ecpm")]
|
||||
avg_ecpm = str(round(sum(ecpm_fens) / len(ecpm_fens))) if ecpm_fens else rep.get("ecpm")
|
||||
# 主表逐行显示用:这次发奖广告的预估收益之和(发奖侧 eCPM 折算,钳顶同展示侧)。只放进
|
||||
# row_revenue_yuan 给主表逐行展示,不进 revenue_yuan/合计/趋势——避免与展示侧 total 重复计。
|
||||
row_revenue = round(sum(
|
||||
min(rewards.parse_ecpm_yuan(g["ecpm"]), rewards.AD_ECPM_MAX_FEN / 100.0) / 1000.0
|
||||
for g in group if g.get("ecpm")
|
||||
), 6)
|
||||
events.append({
|
||||
"event_key": f"feedgrp-{uid}-{grp_key}",
|
||||
"report_date": rep["_report_date"],
|
||||
"user_id": uid,
|
||||
"ad_type": "draw", # 业务已全切 Draw 信息流,聚合行统一 draw
|
||||
"feed_scene": rep.get("feed_scene"),
|
||||
"app_env": rep.get("app_env"),
|
||||
"our_code_id": rep.get("our_code_id"),
|
||||
"created_at": rep["created_at"],
|
||||
"hour": _cn_hour(rep["created_at"]) if by_hour else None,
|
||||
"event_key": f"rwd-{row['record_id']}",
|
||||
"report_date": row["_report_date"],
|
||||
"user_id": row["user_id"],
|
||||
"ad_type": _event_ad_type(row),
|
||||
"feed_scene": row.get("feed_scene"),
|
||||
"app_env": row.get("app_env"),
|
||||
"our_code_id": row.get("our_code_id"),
|
||||
"created_at": row["created_at"],
|
||||
"hour": _cn_hour(row["created_at"]) if by_hour else None,
|
||||
"has_impression": False,
|
||||
"impressions": 0,
|
||||
"ecpm": avg_ecpm,
|
||||
"ecpm": row["ecpm"],
|
||||
"revenue_yuan": 0.0,
|
||||
"row_revenue_yuan": row_revenue,
|
||||
"adn": None,
|
||||
"slot_id": None,
|
||||
"has_reward": True,
|
||||
"status": rep["status"], # 代表状态(逐条见展开)
|
||||
"expected_coin": expected_sum,
|
||||
"actual_coin": actual_sum,
|
||||
"matched": all(bool(g["matched"]) for g in group),
|
||||
"reward_detail": None,
|
||||
"sub_rewards": [_reward_detail(g) for g in group],
|
||||
"sub_count": len(group),
|
||||
"status": row["status"],
|
||||
"expected_coin": int(row["expected_coin"]),
|
||||
"actual_coin": int(row["actual_coin"]),
|
||||
"matched": bool(row["matched"]),
|
||||
"reward_detail": _reward_detail(row),
|
||||
})
|
||||
|
||||
# 「场景」作为全局筛选(与 user_id/ad_type 一致):同时作用于明细、合计与 daily/hourly 趋势。
|
||||
@@ -390,20 +331,9 @@ def ad_revenue_report(
|
||||
is_today = date_from == date_to == rewards.cn_today().isoformat()
|
||||
dau = admin_stats.today_dau(db) if is_today else None
|
||||
|
||||
# 主表「逐行」= 单次广告行为(2026-07 按「一次比价/领券放一块」聚合):激励视频 = 一次观看一行(展示+发奖
|
||||
# 按 ad_session_id 合并);一次比价 / 一次领券 = 该次整场多条广告按 ad_session_id 聚成一行(展开看逐条)。
|
||||
# 信息流(draw/feed)的逐条展示(ad_ecpm,impressionId 各自独立、与整场发奖无公共键)不再单独占行
|
||||
# ——其展示数 / eCPM / 预估收益已计入上面的全量统计(total_*、daily / hourly、type_stats、穿山甲对照),
|
||||
# 只是主表不逐条铺开;逐条明细在父行展开里看(sub_rewards)。合计 / 趋势 / 分类大盘均基于全量 events,
|
||||
# 不受此过滤影响;total / 分页只作用于主表行。
|
||||
main_rows = [
|
||||
e for e in events
|
||||
if not (e["ad_type"] in ("draw", "feed") and e["has_impression"] and not e["has_reward"])
|
||||
]
|
||||
|
||||
return {
|
||||
"total": len(main_rows),
|
||||
"truncated": len(main_rows) > offset + limit,
|
||||
"total": len(events),
|
||||
"truncated": len(events) > offset + limit,
|
||||
"total_impressions": total_impressions,
|
||||
"total_revenue_yuan": total_revenue_yuan,
|
||||
# 穿山甲后台收益合计(元):预估 revenue + 收益Api;非全量视图(带 user/类型/场景过滤)或无数据为 None。
|
||||
@@ -417,5 +347,5 @@ def ad_revenue_report(
|
||||
"hourly": hourly,
|
||||
"type_stats": type_stats,
|
||||
"dau": dau,
|
||||
"items": main_rows[offset:offset + limit],
|
||||
"items": events[offset:offset + limit],
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
"""admin「领券数据」看板聚合:发起/完成数、耗时均值与分位、按天/小时趋势、逐条明细。
|
||||
|
||||
数据源 coupon_session(一次领券一行,客户端 /api/v1/coupon/session 两段上报)。量级不大,全量拉
|
||||
区间数据后 Python 聚合(分位 SQLite 无 percentile,统一 Python 算,PG 上也一致)。
|
||||
- 发起数 = 区间内全部 session(含 started/completed/failed/abandoned),= 流失统计的基数。
|
||||
- 完成数 / 耗时均值 / 分位 = 仅 status==completed 子集(成功跑完才有可比的"领券耗时")。
|
||||
- summary/daily/hourly/total 在全量上算,不受分页;items 为排序后当前页。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, date as _date, datetime
|
||||
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core import rewards
|
||||
from app.models.coupon_state import CouponSession
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def _cn_hour(dt: datetime) -> int:
|
||||
"""started_at(UTC 口径)→ 北京时间小时(0–23)。naive 当 UTC(sqlite),tz-aware 直接换算(pg)。"""
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=UTC)
|
||||
return dt.astimezone(rewards.CN_TZ).hour
|
||||
|
||||
|
||||
def _percentile(sorted_vals: list[int], q: float) -> int | None:
|
||||
"""线性插值分位(q=0..100,numpy 默认法)。sorted_vals 须已升序;空返回 None。"""
|
||||
if not sorted_vals:
|
||||
return None
|
||||
if len(sorted_vals) == 1:
|
||||
return sorted_vals[0]
|
||||
idx = (len(sorted_vals) - 1) * q / 100.0
|
||||
lo = int(idx)
|
||||
hi = min(lo + 1, len(sorted_vals) - 1)
|
||||
frac = idx - lo
|
||||
return round(sorted_vals[lo] * (1 - frac) + sorted_vals[hi] * frac)
|
||||
|
||||
|
||||
def _avg(vals: list[int]) -> int | None:
|
||||
return round(sum(vals) / len(vals)) if vals else None
|
||||
|
||||
|
||||
def _session_to_row(r, phone: str | None = None, nickname: str | None = None) -> dict:
|
||||
"""CouponSession ORM → 明细行 dict(主表「领券数据」与「用户全部领券」抽屉共用)。"""
|
||||
return {
|
||||
"id": r.id,
|
||||
"trace_id": r.trace_id,
|
||||
"user_id": r.user_id,
|
||||
"user_phone": phone,
|
||||
"user_nickname": nickname,
|
||||
"status": r.status,
|
||||
"platforms": r.platforms,
|
||||
"origin_package": r.origin_package,
|
||||
"elapsed_ms": r.elapsed_ms,
|
||||
"platform_elapsed": r.platform_elapsed,
|
||||
"device_model": r.device_model,
|
||||
"rom": r.rom,
|
||||
"app_env": r.app_env,
|
||||
"started_at": r.started_at,
|
||||
"claimed_count": r.claimed_count,
|
||||
"trace_url": r.trace_url,
|
||||
}
|
||||
|
||||
|
||||
def _empty_result() -> dict:
|
||||
return {
|
||||
"summary": {
|
||||
"started_count": 0, "completed_count": 0, "avg_elapsed_ms": None,
|
||||
"p5_ms": None, "p50_ms": None, "p95_ms": None, "p99_ms": None,
|
||||
},
|
||||
"daily": [],
|
||||
"hourly": [],
|
||||
"total": 0,
|
||||
"items": [],
|
||||
}
|
||||
|
||||
|
||||
def coupon_data_report(
|
||||
db: Session,
|
||||
*,
|
||||
date_from: str,
|
||||
date_to: str,
|
||||
user: str | None = None,
|
||||
app_env: str | None = None,
|
||||
granularity: str = "day",
|
||||
limit: int = 500,
|
||||
offset: int = 0,
|
||||
sort: str = "time",
|
||||
) -> dict:
|
||||
"""日期区间(北京自然日 started_date,闭区间)领券数据:汇总卡 + 趋势 + 逐条明细。
|
||||
|
||||
- user:手机号/昵称模糊搜(匹配不到任何用户 → 空结果)。
|
||||
- app_env:prod/dev 精确;None=全部。
|
||||
- sort:time=发起时刻倒序(默认) / elapsed=全程耗时倒序(None 末尾)。
|
||||
"""
|
||||
by_hour = granularity == "hour"
|
||||
d_from = _date.fromisoformat(date_from)
|
||||
d_to = _date.fromisoformat(date_to)
|
||||
|
||||
# user 模糊 → 先定位匹配用户 id;匹配不到直接空结果(不全表扫)。
|
||||
user_ids: set[int] | None = None
|
||||
if user:
|
||||
like = f"%{user}%"
|
||||
user_ids = set(db.execute(
|
||||
select(User.id).where(or_(User.phone.like(like), User.nickname.like(like)))
|
||||
).scalars().all())
|
||||
if not user_ids:
|
||||
return _empty_result()
|
||||
|
||||
stmt = select(CouponSession).where(
|
||||
CouponSession.started_date >= d_from,
|
||||
CouponSession.started_date <= d_to,
|
||||
)
|
||||
if app_env is not None:
|
||||
stmt = stmt.where(CouponSession.app_env == app_env)
|
||||
if user_ids is not None:
|
||||
stmt = stmt.where(CouponSession.user_id.in_(user_ids))
|
||||
rows = list(db.execute(stmt).scalars())
|
||||
|
||||
# ── 汇总卡 ──
|
||||
completed_elapsed = sorted(
|
||||
r.elapsed_ms for r in rows if r.status == "completed" and r.elapsed_ms is not None
|
||||
)
|
||||
summary = {
|
||||
"started_count": len(rows),
|
||||
"completed_count": sum(1 for r in rows if r.status == "completed"),
|
||||
"avg_elapsed_ms": _avg(completed_elapsed),
|
||||
"p5_ms": _percentile(completed_elapsed, 5),
|
||||
"p50_ms": _percentile(completed_elapsed, 50),
|
||||
"p95_ms": _percentile(completed_elapsed, 95),
|
||||
"p99_ms": _percentile(completed_elapsed, 99),
|
||||
}
|
||||
|
||||
# ── 按天趋势(柱=发起/完成数,线=平均耗时)──
|
||||
daily_map: dict[str, dict] = {}
|
||||
for r in rows:
|
||||
d = r.started_date.isoformat()
|
||||
b = daily_map.get(d)
|
||||
if b is None:
|
||||
b = {"date": d, "started_count": 0, "completed_count": 0, "_elapsed": []}
|
||||
daily_map[d] = b
|
||||
b["started_count"] += 1
|
||||
if r.status == "completed":
|
||||
b["completed_count"] += 1
|
||||
if r.elapsed_ms is not None:
|
||||
b["_elapsed"].append(r.elapsed_ms)
|
||||
daily = [
|
||||
{
|
||||
"date": b["date"],
|
||||
"started_count": b["started_count"],
|
||||
"completed_count": b["completed_count"],
|
||||
"avg_elapsed_ms": _avg(b["_elapsed"]),
|
||||
}
|
||||
for b in sorted(daily_map.values(), key=lambda x: x["date"])
|
||||
]
|
||||
|
||||
# ── 按小时趋势(单日 hour 粒度)──
|
||||
hourly: list[dict] = []
|
||||
if by_hour:
|
||||
hour_map: dict[int, dict] = {}
|
||||
for r in rows:
|
||||
h = _cn_hour(r.started_at)
|
||||
b = hour_map.get(h)
|
||||
if b is None:
|
||||
b = {"hour": h, "started_count": 0, "completed_count": 0, "_elapsed": []}
|
||||
hour_map[h] = b
|
||||
b["started_count"] += 1
|
||||
if r.status == "completed":
|
||||
b["completed_count"] += 1
|
||||
if r.elapsed_ms is not None:
|
||||
b["_elapsed"].append(r.elapsed_ms)
|
||||
hourly = [
|
||||
{
|
||||
"hour": b["hour"],
|
||||
"started_count": b["started_count"],
|
||||
"completed_count": b["completed_count"],
|
||||
"avg_elapsed_ms": _avg(b["_elapsed"]),
|
||||
}
|
||||
for b in sorted(hour_map.values(), key=lambda x: x["hour"])
|
||||
]
|
||||
|
||||
# ── 明细:排序 + 分页 + 补用户手机号/昵称(批量,防 N+1)──
|
||||
if sort == "elapsed":
|
||||
rows.sort(key=lambda r: (r.elapsed_ms is None, -(r.elapsed_ms or 0)))
|
||||
else: # time:发起时刻倒序
|
||||
rows.sort(key=lambda r: r.started_at, reverse=True)
|
||||
page = rows[offset:offset + limit]
|
||||
|
||||
uids = {r.user_id for r in page if r.user_id is not None}
|
||||
user_map: dict[int, tuple[str | None, str | None]] = {}
|
||||
if uids:
|
||||
user_map = {
|
||||
uid: (phone, nickname)
|
||||
for uid, phone, nickname in db.execute(
|
||||
select(User.id, User.phone, User.nickname).where(User.id.in_(uids))
|
||||
).all()
|
||||
}
|
||||
items = []
|
||||
for r in page:
|
||||
phone, nickname = user_map.get(r.user_id, (None, None)) if r.user_id is not None else (None, None)
|
||||
items.append(_session_to_row(r, phone, nickname))
|
||||
|
||||
return {
|
||||
"summary": summary,
|
||||
"daily": daily,
|
||||
"hourly": hourly,
|
||||
"total": len(rows),
|
||||
"items": items,
|
||||
}
|
||||
|
||||
|
||||
def coupon_user_records(db: Session, *, user_id: int, limit: int = 100) -> dict:
|
||||
"""某用户全部领券记录(点手机号抽屉用):按发起时刻倒序、不限日期,total=该用户领券总次数。"""
|
||||
rows = list(db.execute(
|
||||
select(CouponSession)
|
||||
.where(CouponSession.user_id == user_id)
|
||||
.order_by(CouponSession.started_at.desc())
|
||||
.limit(limit)
|
||||
).scalars())
|
||||
total = db.execute(
|
||||
select(func.count()).select_from(CouponSession).where(CouponSession.user_id == user_id)
|
||||
).scalar_one()
|
||||
return {"items": [_session_to_row(r) for r in rows], "total": int(total)}
|
||||
@@ -65,19 +65,16 @@ def review_feedback(
|
||||
reward_coins: int | None = None,
|
||||
reject_reason: str | None = None,
|
||||
review_note: str | None = None,
|
||||
admin_reply: str | None = None,
|
||||
commit: bool = True,
|
||||
) -> Feedback:
|
||||
"""审核反馈:置 adopted/rejected + 记录奖励/原因/回复留言/审核人/审核时间。
|
||||
"""审核反馈:置 adopted/rejected + 记录奖励/原因/审核人/审核时间。
|
||||
|
||||
发金币(wallet.grant_coins)由 router 在同一事务里调,确保状态、金币流水、审计一起提交。
|
||||
admin_reply=给用户的回复留言(用户端可见),与 review_note(内部备注)区分。
|
||||
"""
|
||||
feedback.status = status
|
||||
feedback.reward_coins = reward_coins
|
||||
feedback.reject_reason = reject_reason
|
||||
feedback.review_note = review_note
|
||||
feedback.admin_reply = admin_reply
|
||||
feedback.reviewed_by_admin_id = reviewed_by_admin_id
|
||||
feedback.reviewed_at = datetime.now(CN_TZ).replace(tzinfo=None)
|
||||
if commit:
|
||||
|
||||
@@ -390,7 +390,6 @@ def list_all_withdraw_orders(
|
||||
*,
|
||||
user_id: int | None = None,
|
||||
status: str | None = None,
|
||||
source: str | None = None,
|
||||
keyword: str | None = None,
|
||||
date_from: datetime | None = None,
|
||||
date_to: datetime | None = None,
|
||||
@@ -410,9 +409,6 @@ def list_all_withdraw_orders(
|
||||
stmt = stmt.where(WithdrawOrder.user_id == user_id)
|
||||
if status:
|
||||
stmt = stmt.where(WithdrawOrder.status == status)
|
||||
# 提现类型:coin_cash(福利页提现)/ invite_cash(邀请提现);None=全部
|
||||
if source:
|
||||
stmt = stmt.where(WithdrawOrder.source == source)
|
||||
|
||||
kw = (keyword or "").strip()
|
||||
if kw:
|
||||
@@ -538,7 +534,6 @@ def list_feedbacks(
|
||||
db: Session,
|
||||
*,
|
||||
status: str | None = None,
|
||||
source: str | None = None,
|
||||
user_id: int | None = None,
|
||||
content: str | None = None,
|
||||
created_from: datetime | None = None,
|
||||
@@ -548,15 +543,13 @@ def list_feedbacks(
|
||||
limit: int = 20,
|
||||
cursor: int | None = None,
|
||||
) -> tuple[list[Feedback], int | None, int]:
|
||||
"""反馈工单列表。支持 状态 / 反馈类型(source) / 用户ID / 内容模糊 / 提交时间范围 筛选,
|
||||
按 id·提交时间排序。**offset 分页**(cursor=offset):任意列排序下游标语义统一(同 [list_users]),
|
||||
代价是翻页期间数据变动可能错位一条——admin 低频场景可接受。返回 (items, next_cursor, total),
|
||||
total 供页码分页。created_at 为 timestamptz,日期入参统一转 tz-aware UTC 比较。"""
|
||||
"""反馈工单列表。支持 状态 / 用户ID / 内容模糊 / 提交时间范围 筛选,按 id·提交时间排序。
|
||||
**offset 分页**(cursor=offset):任意列排序下游标语义统一(同 [list_users]),代价是翻页期间
|
||||
数据变动可能错位一条——admin 低频场景可接受。返回 (items, next_cursor, total),total 供页码分页。
|
||||
created_at 为 timestamptz,日期入参统一转 tz-aware UTC 比较。"""
|
||||
stmt = select(Feedback)
|
||||
if status:
|
||||
stmt = stmt.where(Feedback.status == status)
|
||||
if source:
|
||||
stmt = stmt.where(Feedback.source == source)
|
||||
if user_id is not None:
|
||||
stmt = stmt.where(Feedback.user_id == user_id)
|
||||
if content and content.strip():
|
||||
@@ -813,12 +806,6 @@ def get_user_overview(db: Session, user_id: int) -> dict | None:
|
||||
}
|
||||
|
||||
|
||||
def _as_utc_naive(value: datetime) -> datetime:
|
||||
"""窗口入参 → UTC naive(= _as_utc 去时区),与库里按 naive UTC 存取的 created_at 同口径比较。
|
||||
历史遗留:_window_conds 一直引用本函数却未定义(自定义区间会 NameError),此处补上。"""
|
||||
return _as_utc(value).replace(tzinfo=None)
|
||||
|
||||
|
||||
def _window_conds(col, date_from: datetime | None, date_to: datetime | None) -> list:
|
||||
"""把 [date_from, date_to] 转成对 col(created_at)的过滤条件;都为 None = 全量(注册至今)。"""
|
||||
conds = []
|
||||
@@ -908,13 +895,6 @@ def user_reward_stats(
|
||||
}
|
||||
|
||||
|
||||
def _cn_wall_to_utc(dt: datetime) -> datetime:
|
||||
"""coin_transaction 存的是北京 wall-clock(naive,见 wallet.grant_coins「存北京 wall-clock」),转成 UTC naive,
|
||||
与广告表(func.now() UTC)统一 —— 让本函数按同一绝对时刻排序、且前端 apiTime(把无时区时间当 UTC 再 +8 展示)
|
||||
口径一致;否则签到会比实际多显示 8 小时(北京时间又被 +8)。"""
|
||||
return dt.replace(tzinfo=rewards.CN_TZ).astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
|
||||
def user_coin_records(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
@@ -935,9 +915,6 @@ def user_coin_records(
|
||||
offset = max(cursor or 0, 0)
|
||||
fetch = offset + limit + 1
|
||||
rows: list[dict] = []
|
||||
# coin_transaction 存北京 wall-clock(其余表存 UTC);签到窗口边界 +8h 对齐北京,过滤/计数才不偏移 8 小时
|
||||
signin_from = date_from + timedelta(hours=8) if date_from is not None else None
|
||||
signin_to = date_to + timedelta(hours=8) if date_to is not None else None
|
||||
|
||||
for rec in db.execute(
|
||||
select(AdRewardRecord)
|
||||
@@ -981,7 +958,7 @@ def user_coin_records(
|
||||
.where(
|
||||
CoinTransaction.user_id == user_id,
|
||||
CoinTransaction.biz_type == "signin",
|
||||
*_window_conds(CoinTransaction.created_at, signin_from, signin_to),
|
||||
*_window_conds(CoinTransaction.created_at, date_from, date_to),
|
||||
)
|
||||
.order_by(CoinTransaction.created_at.desc())
|
||||
.limit(fetch)
|
||||
@@ -989,8 +966,7 @@ def user_coin_records(
|
||||
rows.append({
|
||||
"source": "signin",
|
||||
"source_label": "签到",
|
||||
# 北京 wall-clock → UTC,与广告记录统一(前端 apiTime 会 +8 回北京展示,不然签到会多 8 小时)
|
||||
"created_at": _cn_wall_to_utc(rec.created_at),
|
||||
"created_at": rec.created_at,
|
||||
"ecpm": None,
|
||||
"coin": rec.amount,
|
||||
})
|
||||
@@ -1016,7 +992,7 @@ def user_coin_records(
|
||||
+ _count(
|
||||
CoinTransaction, CoinTransaction.user_id == user_id,
|
||||
CoinTransaction.biz_type == "signin",
|
||||
*_window_conds(CoinTransaction.created_at, signin_from, signin_to),
|
||||
*_window_conds(CoinTransaction.created_at, date_from, date_to),
|
||||
)
|
||||
)
|
||||
return rows[offset:offset + limit], (offset + limit if has_more else None), total
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
"""admin「领券数据」看板:发起/完成数 + 领券耗时(均值 + P5/P50/P95/P99)+ 按天趋势 + 逐条明细。
|
||||
|
||||
任意已登录 admin 可看(只读)。聚合逻辑在 app/admin/repositories/coupon_data.py。
|
||||
数据源 coupon_session(客户端 /api/v1/coupon/session 两段上报)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date as _date
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
|
||||
from app.admin.deps import AdminDb, get_current_admin
|
||||
from app.admin.repositories import coupon_data
|
||||
from app.admin.schemas.coupon_data import (
|
||||
CouponDataDaily,
|
||||
CouponDataHourly,
|
||||
CouponDataOut,
|
||||
CouponDataRow,
|
||||
CouponDataSummary,
|
||||
CouponUserRecordsOut,
|
||||
)
|
||||
from app.core.rewards import cn_today
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admin/api/coupon-data",
|
||||
tags=["admin-coupon-data"],
|
||||
dependencies=[Depends(get_current_admin)],
|
||||
)
|
||||
|
||||
# 区间最大跨度(天);超出拒绝,避免一次拉过多天拖垮接口(对齐广告收益报表)。
|
||||
_MAX_RANGE_DAYS = 92
|
||||
|
||||
|
||||
def _parse_day(value: str | None, *, field: str, default: _date) -> _date:
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return _date.fromisoformat(value)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=422, detail=f"{field} 需为 YYYY-MM-DD") from e
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=CouponDataOut,
|
||||
summary="领券数据看板(发起/完成数 + 耗时分位 + 按天趋势 + 逐条明细)",
|
||||
)
|
||||
def get_coupon_data(
|
||||
db: AdminDb,
|
||||
date_from: Annotated[str | None, Query(description="起始日 北京 YYYY-MM-DD,默认今天")] = None,
|
||||
date_to: Annotated[str | None, Query(description="结束日 北京 YYYY-MM-DD,闭区间,默认=date_from")] = None,
|
||||
user: Annotated[str | None, Query(description="用户手机号/昵称模糊搜;不传=全部")] = None,
|
||||
app_env: Annotated[str, Query(description="prod(默认) / dev / all(全部环境)")] = "prod",
|
||||
granularity: Annotated[
|
||||
str, Query(description="day=按天 / hour=按小时(北京);区间>1 天建议 day")
|
||||
] = "day",
|
||||
limit: Annotated[int, Query(ge=1, le=1000, description="每页条数(分页大小)")] = 500,
|
||||
offset: Annotated[int, Query(ge=0, description="分页偏移(已跳过条数)=(页码-1)×每页条数")] = 0,
|
||||
sort: Annotated[
|
||||
str, Query(description="排序:time=发起时间倒序(默认) / elapsed=耗时倒序")
|
||||
] = "time",
|
||||
) -> CouponDataOut:
|
||||
today = cn_today()
|
||||
d_from = _parse_day(date_from, field="date_from", default=today)
|
||||
d_to = _parse_day(date_to, field="date_to", default=d_from)
|
||||
if d_to < d_from:
|
||||
raise HTTPException(status_code=422, detail="date_to 不能早于 date_from")
|
||||
if (d_to - d_from).days + 1 > _MAX_RANGE_DAYS:
|
||||
raise HTTPException(status_code=422, detail=f"区间最长 {_MAX_RANGE_DAYS} 天")
|
||||
|
||||
# 报表默认只看 prod(对齐广告报表防串台口径);app_env=all 时不过滤、看全部环境。
|
||||
env = None if app_env == "all" else app_env
|
||||
result = coupon_data.coupon_data_report(
|
||||
db, date_from=d_from.isoformat(), date_to=d_to.isoformat(),
|
||||
user=user, app_env=env, granularity=granularity,
|
||||
limit=limit, offset=offset, sort=sort,
|
||||
)
|
||||
return CouponDataOut(
|
||||
date_from=d_from.isoformat(),
|
||||
date_to=d_to.isoformat(),
|
||||
summary=CouponDataSummary(**result["summary"]),
|
||||
daily=[CouponDataDaily(**d) for d in result["daily"]],
|
||||
hourly=[CouponDataHourly(**h) for h in result["hourly"]],
|
||||
total=result["total"],
|
||||
items=[CouponDataRow(**r) for r in result["items"]],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/user-records",
|
||||
response_model=CouponUserRecordsOut,
|
||||
summary="某用户全部领券记录(点手机号抽屉:领券次数 + 记录列表)",
|
||||
)
|
||||
def get_user_coupon_records(
|
||||
db: AdminDb,
|
||||
user_id: Annotated[int, Query(description="用户 id")],
|
||||
limit: Annotated[int, Query(ge=1, le=500, description="最多返回条数")] = 100,
|
||||
sort_by: Annotated[str, Query(description="兼容 UserRecordsDrawer 参数;固定按发起时间倒序")] = "created_at",
|
||||
sort_order: Annotated[str, Query(description="兼容参数,忽略")] = "desc",
|
||||
) -> CouponUserRecordsOut:
|
||||
result = coupon_data.coupon_user_records(db, user_id=user_id, limit=limit)
|
||||
return CouponUserRecordsOut(
|
||||
items=[CouponDataRow(**r) for r in result["items"]],
|
||||
total=result["total"],
|
||||
)
|
||||
@@ -36,8 +36,6 @@ def _ensure_pending(fb: Feedback) -> None:
|
||||
def list_feedbacks(
|
||||
db: AdminDb,
|
||||
status: Annotated[str | None, Query()] = None,
|
||||
# 反馈类型筛选:profile(普通反馈)/ comparison(比价反馈);None=全部
|
||||
source: Annotated[str | None, Query(pattern="^(profile|comparison)$")] = None,
|
||||
user_id: Annotated[int | None, Query()] = None,
|
||||
content: Annotated[str | None, Query(max_length=100)] = None,
|
||||
created_from: Annotated[datetime | None, Query()] = None,
|
||||
@@ -50,7 +48,6 @@ def list_feedbacks(
|
||||
items, next_cursor, total = queries.list_feedbacks(
|
||||
db,
|
||||
status=status,
|
||||
source=source,
|
||||
user_id=user_id,
|
||||
content=content,
|
||||
created_from=created_from,
|
||||
@@ -104,7 +101,6 @@ def approve_feedback(
|
||||
status="adopted",
|
||||
reward_coins=payload.reward_coins,
|
||||
review_note=payload.note,
|
||||
admin_reply=payload.reply,
|
||||
reviewed_by_admin_id=admin.id,
|
||||
commit=False,
|
||||
)
|
||||
@@ -127,7 +123,6 @@ def approve_feedback(
|
||||
"after": "adopted",
|
||||
"reward_coins": payload.reward_coins,
|
||||
"note": payload.note,
|
||||
"reply": payload.reply,
|
||||
},
|
||||
ip=get_client_ip(request),
|
||||
commit=False,
|
||||
@@ -157,7 +152,6 @@ def reject_feedback(
|
||||
status="rejected",
|
||||
reject_reason=payload.reason,
|
||||
review_note=payload.note,
|
||||
admin_reply=payload.reply,
|
||||
reviewed_by_admin_id=admin.id,
|
||||
commit=False,
|
||||
)
|
||||
@@ -172,7 +166,6 @@ def reject_feedback(
|
||||
"after": "rejected",
|
||||
"reason": payload.reason,
|
||||
"note": payload.note,
|
||||
"reply": payload.reply,
|
||||
},
|
||||
ip=get_client_ip(request),
|
||||
commit=False,
|
||||
|
||||
@@ -50,8 +50,6 @@ def list_withdraws(
|
||||
db: AdminDb,
|
||||
user_id: Annotated[int | None, Query()] = None,
|
||||
status: Annotated[str | None, Query()] = None,
|
||||
# 提现类型筛选:coin_cash(福利页提现)/ invite_cash(邀请提现);None=全部
|
||||
source: Annotated[str | None, Query(pattern="^(coin_cash|invite_cash)$")] = None,
|
||||
keyword: Annotated[str | None, Query(max_length=100)] = None,
|
||||
date_from: Annotated[datetime | None, Query()] = None,
|
||||
date_to: Annotated[datetime | None, Query()] = None,
|
||||
@@ -71,7 +69,6 @@ def list_withdraws(
|
||||
db,
|
||||
user_id=user_id,
|
||||
status=status,
|
||||
source=source,
|
||||
keyword=keyword,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
|
||||
@@ -94,11 +94,6 @@ class AdRevenueRow(BaseModel):
|
||||
impressions: int = Field(..., description="本行展示条数:有展示=1 / 纯发奖=0(供日汇总、趋势图复用)")
|
||||
ecpm: str | None = Field(None, description="eCPM 原始值(分/千次);展示行取展示值,纯发奖行取发奖采用值")
|
||||
revenue_yuan: float = Field(..., description="本次展示预估收益(元)= eCPM元 ÷ 1000;纯发奖行=0")
|
||||
row_revenue_yuan: float | None = Field(
|
||||
None,
|
||||
description="主表逐行展示用的预估收益(元):一次比价/领券聚合行=该次发奖广告 eCPM 折算之和;"
|
||||
"其它行为空(前端回退取 revenue_yuan)。不进合计/趋势,避免与展示侧重复计",
|
||||
)
|
||||
adn: str | None = Field(None, description="实际填充 ADN 子渠道(pangle/gdt…);纯发奖行为空")
|
||||
slot_id: str | None = Field(None, description="底层 mediation rit(非我们配置的广告位 ID);纯发奖行为空")
|
||||
# ── 发奖侧 ──
|
||||
@@ -111,15 +106,6 @@ class AdRevenueRow(BaseModel):
|
||||
None,
|
||||
description="发奖复算明细(eCPM/因子1/份数/LT/因子2/应发/实发/一致);点行展开下钻用,纯展示为空",
|
||||
)
|
||||
sub_rewards: list[AdRevenueRecord] = Field(
|
||||
default_factory=list,
|
||||
description="一次比价/领券聚合行的组内逐条发奖明细(同一整场 ad_session_id 的多条广告);"
|
||||
"点行展开渲染多行。激励视频/纯展示行为空(单条看 reward_detail)",
|
||||
)
|
||||
sub_count: int = Field(
|
||||
1,
|
||||
description="本行聚合的发奖条数:一次比价/领券=该次广告条数(≥1);激励视频/纯展示=1",
|
||||
)
|
||||
|
||||
|
||||
class AdRevenueReportOut(BaseModel):
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
"""admin「领券数据」看板 schemas:汇总卡 + 按天/小时趋势 + 逐条领券明细。
|
||||
|
||||
数据源 coupon_session(一次领券一行)。耗时单位 ms(前端按需折秒);均值/分位只统计 completed。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CouponDataSummary(BaseModel):
|
||||
"""汇总卡:发起/完成数 + 耗时均值与分位(P5/P50/P95/P99,基于 completed 的 elapsed_ms)。"""
|
||||
|
||||
started_count: int = Field(..., description="发起数(区间内所有领券 session)")
|
||||
completed_count: int = Field(..., description="完成数(status=completed)")
|
||||
avg_elapsed_ms: int | None = Field(None, description="平均耗时(ms,仅 completed;无数据为空)")
|
||||
p5_ms: int | None = Field(None, description="耗时 5 分位(ms)")
|
||||
p50_ms: int | None = Field(None, description="耗时 50 分位(ms,中位数)")
|
||||
p95_ms: int | None = Field(None, description="耗时 95 分位(ms)")
|
||||
p99_ms: int | None = Field(None, description="耗时 99 分位(ms)")
|
||||
|
||||
|
||||
class CouponDataDaily(BaseModel):
|
||||
"""按天趋势(全量,不受分页影响):柱=发起/完成数,线=平均耗时。"""
|
||||
|
||||
date: str = Field(..., description="北京时间 YYYY-MM-DD")
|
||||
started_count: int
|
||||
completed_count: int
|
||||
avg_elapsed_ms: int | None = Field(None, description="当天平均耗时(ms,仅 completed)")
|
||||
|
||||
|
||||
class CouponDataHourly(BaseModel):
|
||||
"""按北京小时(0–23)趋势(单日 granularity=hour 时非空)。"""
|
||||
|
||||
hour: int = Field(..., description="北京时间小时 0–23")
|
||||
started_count: int
|
||||
completed_count: int
|
||||
avg_elapsed_ms: int | None = None
|
||||
|
||||
|
||||
class CouponDataRow(BaseModel):
|
||||
"""一条领券明细(一次领券任务)。"""
|
||||
|
||||
id: int = Field(..., description="coupon_session 主键(抽屉 rowKey 用)")
|
||||
trace_id: str
|
||||
user_id: int | None = None
|
||||
user_phone: str | None = Field(None, description="手机号(admin 展示;匿名领券/查不到为空)")
|
||||
user_nickname: str | None = Field(None, description="昵称")
|
||||
status: str = Field(..., description="started / completed / failed / abandoned")
|
||||
platforms: list[str] | None = Field(None, description="发起勾选平台")
|
||||
origin_package: str | None = Field(None, description="发起来源 App 包名;null=App 内(傻瓜比价首页)发起")
|
||||
elapsed_ms: int | None = Field(None, description="全程耗时(ms)")
|
||||
platform_elapsed: dict[str, int] | None = Field(
|
||||
None, description="各平台耗时 {meituan-waimai/taobao-shanguang/jd-waimai: ms}"
|
||||
)
|
||||
device_model: str | None = None
|
||||
rom: str | None = None
|
||||
app_env: str | None = None
|
||||
started_at: datetime = Field(..., description="发起时刻(明细「时间」列)")
|
||||
claimed_count: int | None = None
|
||||
trace_url: str | None = Field(None, description="pricebot 公网 trace 链接(仅 completed 有);admin 渲染可点链接,无则显示可复制 trace_id")
|
||||
|
||||
|
||||
class CouponDataOut(BaseModel):
|
||||
"""领券数据看板响应:汇总卡 + 趋势 + 明细分页。"""
|
||||
|
||||
date_from: str
|
||||
date_to: str
|
||||
summary: CouponDataSummary
|
||||
daily: list[CouponDataDaily] = Field(default_factory=list, description="按天趋势(全量)")
|
||||
hourly: list[CouponDataHourly] = Field(
|
||||
default_factory=list, description="按小时趋势(单日 hour 粒度时非空)"
|
||||
)
|
||||
total: int = Field(..., description="明细总条数(全量,不受分页)")
|
||||
items: list[CouponDataRow] = Field(..., description="逐条领券明细(当前页)")
|
||||
|
||||
|
||||
class CouponUserRecordsOut(BaseModel):
|
||||
"""某用户全部领券记录(点手机号抽屉用):total=该用户领券总次数,items=记录列表(UserRecordsDrawer 渲染)。"""
|
||||
|
||||
items: list[CouponDataRow]
|
||||
total: int
|
||||
@@ -15,17 +15,11 @@ class FeedbackOut(BaseModel):
|
||||
user_id: int
|
||||
content: str
|
||||
contact: str
|
||||
# 反馈来源入口:profile(「我的」页)/ comparison(比价结果页)——admin 展示/筛选「反馈类型」
|
||||
source: str = "profile"
|
||||
# 比价反馈的问题场景(找错商品/优惠不对…);普通反馈为 None
|
||||
scene: str | None = None
|
||||
images: list[str] | None = None
|
||||
status: str
|
||||
reject_reason: str | None = None
|
||||
reward_coins: int | None = None
|
||||
review_note: str | None = None
|
||||
# 运营给用户的回复留言(用户端可见,采纳/未采纳都可填)
|
||||
admin_reply: str | None = None
|
||||
reviewed_by_admin_id: int | None = None
|
||||
reviewed_at: datetime | None = None
|
||||
created_at: datetime
|
||||
@@ -45,14 +39,12 @@ class FeedbackApproveRequest(BaseModel):
|
||||
le=FEEDBACK_REWARD_MAX_COINS,
|
||||
description="采纳后发放金币数",
|
||||
)
|
||||
note: str | None = Field(default=None, max_length=256, description="采纳要点/审核备注(内部)")
|
||||
reply: str | None = Field(default=None, max_length=256, description="给用户的回复留言,用户端可见")
|
||||
note: str | None = Field(default=None, max_length=256, description="采纳要点/审核备注")
|
||||
|
||||
|
||||
class FeedbackRejectRequest(BaseModel):
|
||||
reason: str = Field(min_length=1, max_length=256, description="未采纳原因,用户端可见")
|
||||
note: str | None = Field(default=None, max_length=256, description="运营内部审核备注")
|
||||
reply: str | None = Field(default=None, max_length=256, description="给用户的回复留言,用户端可见")
|
||||
|
||||
|
||||
class FeedbackSummary(BaseModel):
|
||||
|
||||
@@ -17,7 +17,6 @@ class AdminUserListItem(BaseModel):
|
||||
status: str
|
||||
debug_trace_enabled: bool = False
|
||||
wechat_openid: str | None = None
|
||||
wechat_nickname: str | None = None
|
||||
created_at: datetime
|
||||
last_login_at: datetime
|
||||
|
||||
|
||||
@@ -41,8 +41,6 @@ class WithdrawOrderOut(BaseModel):
|
||||
user_id: int
|
||||
out_bill_no: str
|
||||
amount_cents: int
|
||||
# 提现类型:coin_cash(福利页提现,金币兑换的现金)/ invite_cash(邀请提现,邀请奖励金)
|
||||
source: str = "coin_cash"
|
||||
user_name: str | None = None # 提现实名(审核核对 + 打款用)
|
||||
status: str
|
||||
wechat_state: str | None = None
|
||||
|
||||
@@ -29,7 +29,6 @@ from app.schemas.coupon_state import (
|
||||
CouponPromptDismissIn,
|
||||
CouponPromptShouldShowOut,
|
||||
CouponPromptShownIn,
|
||||
CouponSessionIn,
|
||||
CouponStatsOut,
|
||||
)
|
||||
|
||||
@@ -195,34 +194,6 @@ async def coupon_step(
|
||||
return resp_json
|
||||
|
||||
|
||||
@router.post("/session", summary="领券任务流水上报(admin 领券数据看板数据源)")
|
||||
def coupon_session(payload: CouponSessionIn, db: DbSession) -> dict[str, bool]:
|
||||
"""客户端两段上报一次领券流水(发起 started / 收尾 completed-failed-abandoned),按 trace_id upsert
|
||||
到 coupon_session。不鉴权(同领券循环 MVP,按 device_id/trace_id);供 admin「领券数据」看板算
|
||||
发起/完成数、耗时分位、机型维度。写库失败不应连累客户端(本就 fire-and-forget),吞掉返回 ok。"""
|
||||
try:
|
||||
coupon_repo.upsert_coupon_session(
|
||||
db,
|
||||
trace_id=payload.trace_id,
|
||||
device_id=payload.device_id,
|
||||
status=payload.status,
|
||||
started_at_ms=payload.started_at_ms,
|
||||
user_id=payload.user_id,
|
||||
platforms=payload.platforms,
|
||||
origin_package=payload.origin_package,
|
||||
device_model=payload.device_model,
|
||||
rom=payload.rom,
|
||||
app_env=payload.app_env,
|
||||
elapsed_ms=payload.elapsed_ms,
|
||||
platform_elapsed=payload.platform_elapsed,
|
||||
claimed_count=payload.claimed_count,
|
||||
trace_url=payload.trace_url,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("coupon session write failed: %s", e)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/prompt/shown", summary="领券引导窗弹出即上报(按 App 记 shown)")
|
||||
def coupon_prompt_shown(payload: CouponPromptShownIn, db: DbSession) -> dict[str, bool]:
|
||||
"""客户端弹出引导窗那刻调 → 记一条今日 engagement(shown),今天**这个 App** 不再自动弹。
|
||||
|
||||
+1
-24
@@ -31,18 +31,7 @@ router = APIRouter(prefix="/api/v1/feedback", tags=["feedback"])
|
||||
_MAX_IMAGES = 6
|
||||
_CONTENT_MAX = 200
|
||||
_CONTACT_MAX = 128
|
||||
_SCENE_MAX = 32
|
||||
_VALID_RECORD_STATUS = {"pending", "adopted", "rejected"}
|
||||
_VALID_SOURCES = {"profile", "comparison"}
|
||||
|
||||
|
||||
def _resolve_source(source: str, scene: str) -> str:
|
||||
"""反馈来源:客户端显式传的 source 优先(profile / comparison);未传或非法时,
|
||||
按 scene 有无派生——比价结果页反馈才带 scene,故 scene 非空即视作 comparison。"""
|
||||
src = source.strip().lower()
|
||||
if src in _VALID_SOURCES:
|
||||
return src
|
||||
return "comparison" if scene.strip() else "profile"
|
||||
|
||||
|
||||
def _app_status(db_status: str) -> str:
|
||||
@@ -57,12 +46,10 @@ def _record_out(fb) -> FeedbackRecordOut:
|
||||
return FeedbackRecordOut(
|
||||
id=fb.id,
|
||||
content=fb.content,
|
||||
scene=getattr(fb, "scene", None),
|
||||
images=fb.images or [],
|
||||
status=_app_status(fb.status),
|
||||
reject_reason=getattr(fb, "reject_reason", None),
|
||||
reward_coins=getattr(fb, "reward_coins", None),
|
||||
admin_reply=getattr(fb, "admin_reply", None),
|
||||
created_at=fb.created_at,
|
||||
)
|
||||
|
||||
@@ -74,11 +61,6 @@ async def submit_feedback(
|
||||
content: str = Form(...),
|
||||
# 原型改版后客户端不再采集联系方式;保留字段以兼容旧端 + 后续可能复用,默认空串。
|
||||
contact: str = Form(default=""),
|
||||
# 反馈来源入口:profile(「我的」页)/ comparison(比价结果页)。新端显式传;旧端不带 →
|
||||
# 空串 → 按 scene 有无派生。admin 据此展示/筛选「反馈类型」。
|
||||
source: str = Form(default=""),
|
||||
# 比价结果页反馈的「问题场景」(找错商品/优惠不对…);普通反馈不带 → 空串 → 存 NULL。
|
||||
scene: str = Form(default=""),
|
||||
# 提交端环境快照(admin 反馈页展示「提交版本号」「机型OS版本」);旧端不带 → 空串 → 存 NULL。
|
||||
app_version: str = Form(default=""),
|
||||
device_model: str = Form(default=""),
|
||||
@@ -94,8 +76,6 @@ async def submit_feedback(
|
||||
raise HTTPException(status_code=400, detail="反馈内容过长")
|
||||
if len(contact) > _CONTACT_MAX:
|
||||
raise HTTPException(status_code=400, detail="联系方式过长")
|
||||
scene = scene.strip()[:_SCENE_MAX]
|
||||
resolved_source = _resolve_source(source, scene)
|
||||
|
||||
files = [f for f in (images or []) if f is not None and f.filename]
|
||||
if len(files) > _MAX_IMAGES:
|
||||
@@ -111,13 +91,10 @@ async def submit_feedback(
|
||||
|
||||
fb = feedback_repo.create_feedback(
|
||||
db, user_id=user.id, content=content, contact=contact, images=urls,
|
||||
source=resolved_source, scene=scene or None,
|
||||
app_version=app_version.strip(), device_model=device_model.strip(),
|
||||
rom_name=rom_name.strip(), android_version=android_version.strip(),
|
||||
)
|
||||
logger.info(
|
||||
"feedback id=%d user_id=%d source=%s images=%d", fb.id, user.id, resolved_source, len(urls)
|
||||
)
|
||||
logger.info("feedback id=%d user_id=%d images=%d", fb.id, user.id, len(urls))
|
||||
return FeedbackOut.model_validate(fb)
|
||||
|
||||
|
||||
|
||||
+4
-41
@@ -25,19 +25,9 @@ from app.schemas.meituan import (
|
||||
ReferralLinkResponse,
|
||||
TopSalesRequest,
|
||||
)
|
||||
from app.utils.meituan_city import get_meituan_city
|
||||
|
||||
logger = logging.getLogger("shagua.meituan")
|
||||
|
||||
|
||||
def _resolve_city_id(latitude: float, longitude: float) -> str:
|
||||
"""经纬度 → 美团城市 ID;解析失败返 ""(调用方应降级返空)。"""
|
||||
try:
|
||||
return get_meituan_city(latitude, longitude).get("city_id", "")
|
||||
except Exception:
|
||||
logger.exception("get_meituan_city 失败")
|
||||
return ""
|
||||
|
||||
router = APIRouter(prefix="/api/v1/meituan", tags=["meituan-cps"])
|
||||
|
||||
|
||||
@@ -185,19 +175,13 @@ def feed(req: FeedRequest, db: Session = Depends(get_db)) -> FeedResponse:
|
||||
status = "degraded" if (not cards and wm_fail and dd_fail) else ("ok" if cards else "empty")
|
||||
return FeedResponse(items=cards, has_next=wm_hn or dd_hn, page=req.page, status=status)
|
||||
|
||||
# 智能推荐(rec):走【离线库】筛佣金率 ≥ 3%,按城市过滤,分页返回(SQL 侧去重+排序+分页,秒级、不打美团)。
|
||||
# 智能推荐(rec):走【离线库】筛佣金率 ≥ 3%,分页返回(SQL 侧去重+排序+分页,秒级、不打美团)。
|
||||
# 实测库里佣金≥3% 去重后仅 ~578 条(几乎全是外卖;到店团购佣金普遍 <3%):实时按"同城热销榜单"
|
||||
# 拉既撞限流、又填不满(该榜单中位佣金 ~0.8%,筛完每页剩 0-1 条),故从库出。佣金阈值逻辑不变。
|
||||
if tab == "rec":
|
||||
city_id = _resolve_city_id(lat, lon)
|
||||
if not city_id:
|
||||
return FeedResponse(items=[], has_next=False, page=req.page, status="degraded")
|
||||
PAGE = 20
|
||||
try:
|
||||
base = select(MeituanCoupon).where(
|
||||
MeituanCoupon.commission_percent >= 3.0,
|
||||
MeituanCoupon.city_id == city_id,
|
||||
)
|
||||
base = select(MeituanCoupon).where(MeituanCoupon.commission_percent >= 3.0)
|
||||
deduped = base.distinct(MeituanCoupon.dedup_key).order_by(
|
||||
MeituanCoupon.dedup_key,
|
||||
MeituanCoupon.commission_percent.desc(),
|
||||
@@ -227,9 +211,6 @@ def feed(req: FeedRequest, db: Session = Depends(get_db)) -> FeedResponse:
|
||||
card.distance_text = None
|
||||
card.distance_meters = None
|
||||
cards.append(card)
|
||||
if not cards and req.page == 1:
|
||||
# 命中城市却 0 券:该城确无 ≥3% 券,或 ETL 灌的 city_id 与 city_dict 口径不一致。
|
||||
logger.info("[feed] rec city_id=%s 命中 0 券(该城确无券?或 ETL/city_dict 的 city_id 口径不一致)", city_id)
|
||||
return FeedResponse(items=cards, has_next=has_next, page=req.page,
|
||||
status="ok" if cards else "empty")
|
||||
|
||||
@@ -273,24 +254,14 @@ def referral_link(req: ReferralLinkRequest) -> ReferralLinkResponse:
|
||||
|
||||
|
||||
@router.post("/top-sales", response_model=CouponListResponse,
|
||||
summary="销量最高(从离线库 meituan_coupon 按销量降序 + 跨源去重,按城市过滤,不实时打美团)")
|
||||
summary="销量最高(从离线库 meituan_coupon 按销量降序 + 跨源去重,不实时打美团)")
|
||||
def top_sales(req: TopSalesRequest, db: Session = Depends(get_db)) -> CouponListResponse:
|
||||
# 按设备经纬度定位城市,只查同城券;老客户端不带坐标 → 降级返空(不 422、不误返全城)。
|
||||
if req.latitude is None or req.longitude is None:
|
||||
return CouponListResponse(items=[], has_next=False, search_id=None, status="degraded")
|
||||
city_id = _resolve_city_id(req.latitude, req.longitude)
|
||||
if not city_id:
|
||||
return CouponListResponse(items=[], has_next=False, search_id=None, status="degraded")
|
||||
|
||||
# 去重 + 排序 + 分页全在 SQL 做,每页只取并解析当前页 ~20 条。
|
||||
# (之前实现每翻一页都全表拉取 + 全量 from_raw 解析,翻页慢 → 客户端滑动卡顿/翻不动。)
|
||||
# 库为空(prod 刚部署 / ETL 未跑完)时返空 + status=empty,不崩;库查询异常降级 degraded。
|
||||
try:
|
||||
# 1) DISTINCT ON (dedup_key):每个去重键(品牌|名|价)只留销量最高那条(同销量再按佣金)
|
||||
base = select(MeituanCoupon).where(
|
||||
MeituanCoupon.sale_volume_num.isnot(None),
|
||||
MeituanCoupon.city_id == city_id,
|
||||
)
|
||||
base = select(MeituanCoupon).where(MeituanCoupon.sale_volume_num.isnot(None))
|
||||
if req.platform is not None:
|
||||
base = base.where(MeituanCoupon.platform == req.platform)
|
||||
deduped = base.distinct(MeituanCoupon.dedup_key).order_by(
|
||||
@@ -321,14 +292,6 @@ def top_sales(req: TopSalesRequest, db: Session = Depends(get_db)) -> CouponList
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
if card.product_view_sign:
|
||||
# 不显示距离:库里的距离是相对城市默认点的(对用户无意义、且误导)。
|
||||
# 置空后前端"距离 店名"那行只剩店名、自动顶到最左(店名移到原距离的位置)。
|
||||
# 逻辑与推荐流保持一致
|
||||
card.distance_text = None
|
||||
card.distance_meters = None
|
||||
cards.append(card)
|
||||
if not cards and req.page == 1:
|
||||
# 命中城市却 0 券:可能该城确无券,也可能 ETL 灌的 city_id 与 city_dict 口径不一致(静默降级的隐患)。
|
||||
logger.info("[top-sales] city_id=%s 命中 0 券(该城确无券?或 ETL/city_dict 的 city_id 口径不一致)", city_id)
|
||||
return CouponListResponse(items=cards, has_next=has_next, search_id=None,
|
||||
status="ok" if cards else "empty")
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# 无障碍保护存活监控后台任务(pull 后置检测;本期不接推送)
|
||||
HEARTBEAT_MONITOR_ENABLED: bool = True # 总开关
|
||||
HEARTBEAT_TIMEOUT_MINUTES: int = 60 # 多久没心跳算掉线(1 小时,避免短暂离线误判被杀)
|
||||
HEARTBEAT_TIMEOUT_MINUTES: int = 10 # 多久没心跳算掉线(≈3 个客户端心跳周期)
|
||||
HEARTBEAT_SCAN_INTERVAL_SEC: int = 60 # 扫描周期
|
||||
|
||||
# ===== 短信 =====
|
||||
|
||||
@@ -71,12 +71,6 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
||||
settings.DATABASE_URL.split("://", 1)[0],
|
||||
)
|
||||
get_pricebot_client() # 预热透传 client:把建 SSL 上下文的一次性成本付在启动,首个领券请求即热
|
||||
try:
|
||||
# 预热离线地理库:首次加载 ~2.5M 行 CSV + 建 KDTree,摊到启动、不砸首个按城市过滤的请求
|
||||
from app.utils import geo
|
||||
geo.ensure_loaded()
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("reverse_geocoder 预热失败(城市反查将在首个请求时懒加载)")
|
||||
reconcile_task = start_withdraw_reconcile_worker()
|
||||
heartbeat_task = start_heartbeat_monitor()
|
||||
daily_exchange_task = start_daily_exchange_worker()
|
||||
|
||||
@@ -19,7 +19,6 @@ from app.models.coupon_state import ( # noqa: F401
|
||||
CouponClaimRecord,
|
||||
CouponDailyCompletion,
|
||||
CouponPromptEngagement,
|
||||
CouponSession,
|
||||
)
|
||||
from app.models.feedback import Feedback # noqa: F401
|
||||
from app.models.invite import InviteRelation # noqa: F401
|
||||
|
||||
@@ -181,79 +181,3 @@ class CouponPromptEngagement(Base):
|
||||
f"<CouponPromptEngagement device={self.device_id} "
|
||||
f"date={self.engage_date} type={self.engage_type}>"
|
||||
)
|
||||
|
||||
|
||||
class CouponSession(Base):
|
||||
"""一次领券任务的全程流水(admin「领券数据」看板数据源,2026-06-30)。
|
||||
|
||||
与 coupon_claim_record(按券一天一条去重)、coupon_daily_completion(按设备一天一条)都不同:
|
||||
本表**一次领券一条**(trace_id 唯一),记从发起(started)到收尾(completed/failed/abandoned)的
|
||||
全程耗时 + 各平台耗时 + 机型/ROM。客户端 POST /api/v1/coupon/session 两段上报:发起建行、
|
||||
收尾按 trace_id 更新同一行。发起即落库 → admin 可算「发起数」与中途流失(started 无终态=未完成)。
|
||||
|
||||
口径:elapsed_ms 由客户端全程计时(点发起→收尾)、权威;started_at/finished_at 为时刻留痕。
|
||||
started_date = started_at 的 Asia/Shanghai 自然日,供 admin 按天聚合 / 日期筛选(索引)。
|
||||
"""
|
||||
|
||||
__tablename__ = "coupon_session"
|
||||
__table_args__ = (
|
||||
# 一次领券一行:trace_id 幂等 upsert(发起建、收尾更新同一行)。
|
||||
UniqueConstraint("trace_id", name="uq_coupon_session_trace"),
|
||||
# admin 主聚合/筛选:按上海自然日 + 环境(报表默认只看 prod,避免测试数据串台)。
|
||||
Index("ix_coupon_session_date_env", "started_date", "app_env"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# 一次领券唯一 id(客户端 UUID,全程贯穿),upsert 键。
|
||||
trace_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
device_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
# 登录态才带(admin join 用户表出手机号/昵称);匿名领券为空。
|
||||
user_id: Mapped[int | None] = mapped_column(Integer, index=True, nullable=True)
|
||||
|
||||
# started / completed / failed / abandoned。started 无终态 = 中途流失(未完成)。
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False)
|
||||
# prod / dev(客户端 BuildConfig.DEBUG)。admin 报表默认只看 prod(对齐广告报表防串台口径)。
|
||||
app_env: Mapped[str | None] = mapped_column(String(16), index=True, nullable=True)
|
||||
|
||||
# 发起勾选平台 ["meituan-waimai", ...](空=全领)。
|
||||
platforms: Mapped[list | None] = mapped_column(_JSON, nullable=True)
|
||||
# 发起来源外卖 App 包名;null=App 内(傻瓜比价首页)发起,非空=从美团/淘宝/京东弹券发起。
|
||||
# admin「发起平台」列据此区分(空→傻瓜比价,包名→对应平台)。
|
||||
origin_package: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
# 机型(Build.MANUFACTURER + MODEL)与 ROM(OemDetector,如 "ColorOS 14")。明细「机型/ROM」列 + 维度筛选。
|
||||
device_model: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
rom: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
|
||||
# 发起时刻(客户端墙钟):明细「时间」列、趋势 X 轴。
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
# 发起的 Asia/Shanghai 自然日:按天聚合 / 日期范围筛选(索引)。
|
||||
started_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
# 收尾时刻(服务端 now);未收尾(流失)为空。
|
||||
finished_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
# 全程耗时(ms,客户端点发起→收尾):平均 / 分位都基于它(只统计 completed)。
|
||||
elapsed_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
# 各平台领券耗时 {"meituan-waimai":3200,...}(ms)。明细美团/淘宝/京东耗时列。
|
||||
platform_elapsed: Mapped[dict | None] = mapped_column(_JSON, nullable=True)
|
||||
# 领到总张数(收尾帧带)。
|
||||
claimed_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
# pricebot done 帧回传的公网调试链接(price.shaguabijia.com/traces/{dir});含落盘时分秒、拼不出,只能存
|
||||
# (同 ComparisonRecord.trace_url)。admin「领券数据」明细据此渲染可点 trace 链接;未到 done(failed/abandoned)为空。
|
||||
trace_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return (
|
||||
f"<CouponSession trace={self.trace_id} status={self.status} "
|
||||
f"elapsed_ms={self.elapsed_ms}>"
|
||||
)
|
||||
|
||||
@@ -23,14 +23,6 @@ class Feedback(Base):
|
||||
)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
contact: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
# 反馈来源入口:profile(「我的」页反馈入口)/ comparison(比价结果页反馈入口)。
|
||||
# admin 据此展示/筛选「反馈类型」。旧数据与「我的」页反馈均为 profile(server_default)。
|
||||
# 客户端显式传 source 优先;未传时由 scene 有无派生(见 api/v1/feedback.submit_feedback)。
|
||||
source: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="profile", server_default="profile", index=True
|
||||
)
|
||||
# 比价反馈的「问题场景」(找错商品/优惠不对/比价太慢…),比价结果页反馈才有;普通反馈为 NULL。
|
||||
scene: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
# 截图 URL 列表(相对路径,如 ["/media/feedback/u1_ab12.jpg"]);无图为 None
|
||||
images: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)
|
||||
# 提交时的端环境快照(admin 排查用;客户端改版带上后的新反馈才有,历史数据为 NULL)
|
||||
@@ -44,9 +36,6 @@ class Feedback(Base):
|
||||
reward_coins: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
# 审核批注:采纳时可写采纳要点,未采纳时也可保留运营侧备注
|
||||
review_note: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
# 运营给用户的回复留言(**用户端可见**,采纳/未采纳都可填,随「我的反馈」历史下发)。
|
||||
# 与 review_note(内部备注,用户不可见)、reject_reason(未采纳原因)区分开。
|
||||
admin_reply: Mapped[str | None] = mapped_column(String(256), nullable=True)
|
||||
reviewed_by_admin_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("admin_user.id"), nullable=True
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import date, datetime, timezone
|
||||
from datetime import date, datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
@@ -17,7 +17,6 @@ from app.models.coupon_state import (
|
||||
CouponClaimRecord,
|
||||
CouponDailyCompletion,
|
||||
CouponPromptEngagement,
|
||||
CouponSession,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("shagua.coupon_state")
|
||||
@@ -233,91 +232,3 @@ def sum_claimed_count(db: Session, user_id: int) -> int:
|
||||
)
|
||||
).scalar_one()
|
||||
return int(total or 0)
|
||||
|
||||
|
||||
# ===== 领券任务流水(coupon_session,admin「领券数据」看板数据源)=====
|
||||
|
||||
def upsert_coupon_session(
|
||||
db: Session,
|
||||
*,
|
||||
trace_id: str,
|
||||
device_id: str,
|
||||
status: str,
|
||||
started_at_ms: int,
|
||||
user_id: int | None = None,
|
||||
platforms: list[str] | None = None,
|
||||
origin_package: str | None = None,
|
||||
device_model: str | None = None,
|
||||
rom: str | None = None,
|
||||
app_env: str | None = None,
|
||||
elapsed_ms: int | None = None,
|
||||
platform_elapsed: dict | None = None,
|
||||
claimed_count: int | None = None,
|
||||
trace_url: str | None = None,
|
||||
) -> None:
|
||||
"""一条领券流水按 trace_id 幂等 upsert(发起 started 建行、收尾终态更新同一行)。
|
||||
|
||||
- 乱序/重复兜底:终态(completed/failed/abandoned)先到也建行;started 重复到不覆盖已有终态
|
||||
(状态只前进,不降级)。
|
||||
- started_at 由客户端墙钟毫秒转;started_date 取其 Asia/Shanghai 自然日(admin 按天聚合/筛选)。
|
||||
- 终态帧补 finished_at=服务端 now;各字段非空才写(避免 started 帧的 None 抹掉收尾值,反之亦然)。
|
||||
并发 IntegrityError 回滚忽略(本就幂等)。
|
||||
"""
|
||||
started_at = datetime.fromtimestamp(started_at_ms / 1000, tz=timezone.utc)
|
||||
started_date = started_at.astimezone(_CN_TZ).date()
|
||||
is_terminal = status in ("completed", "failed", "abandoned")
|
||||
|
||||
row = db.execute(
|
||||
select(CouponSession).where(CouponSession.trace_id == trace_id)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if row is None:
|
||||
db.add(CouponSession(
|
||||
trace_id=trace_id,
|
||||
device_id=device_id,
|
||||
user_id=user_id,
|
||||
status=status,
|
||||
app_env=app_env,
|
||||
platforms=platforms,
|
||||
origin_package=origin_package,
|
||||
device_model=device_model,
|
||||
rom=rom,
|
||||
started_at=started_at,
|
||||
started_date=started_date,
|
||||
finished_at=datetime.now(timezone.utc) if is_terminal else None,
|
||||
elapsed_ms=elapsed_ms,
|
||||
platform_elapsed=platform_elapsed,
|
||||
claimed_count=claimed_count,
|
||||
trace_url=trace_url,
|
||||
))
|
||||
else:
|
||||
# 状态只前进:started 帧重复到(如 START_STICKY 重启)不把已有终态降级回 started。
|
||||
if not (status == "started" and row.status in ("completed", "failed", "abandoned")):
|
||||
row.status = status
|
||||
if is_terminal:
|
||||
row.finished_at = datetime.now(timezone.utc)
|
||||
if user_id is not None:
|
||||
row.user_id = user_id
|
||||
if platforms is not None:
|
||||
row.platforms = platforms
|
||||
if origin_package is not None:
|
||||
row.origin_package = origin_package
|
||||
if device_model is not None:
|
||||
row.device_model = device_model
|
||||
if rom is not None:
|
||||
row.rom = rom
|
||||
if app_env is not None:
|
||||
row.app_env = app_env
|
||||
if elapsed_ms is not None:
|
||||
row.elapsed_ms = elapsed_ms
|
||||
if platform_elapsed is not None:
|
||||
row.platform_elapsed = platform_elapsed
|
||||
if claimed_count is not None:
|
||||
row.claimed_count = claimed_count
|
||||
if trace_url is not None:
|
||||
row.trace_url = trace_url
|
||||
try:
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
# 并发下另一请求刚插了同 trace_id → 唯一约束撞,回滚忽略(本就幂等)。
|
||||
db.rollback()
|
||||
|
||||
@@ -17,8 +17,6 @@ def create_feedback(
|
||||
content: str,
|
||||
contact: str,
|
||||
images: list[str] | None,
|
||||
source: str = "profile",
|
||||
scene: str | None = None,
|
||||
app_version: str | None = None,
|
||||
device_model: str | None = None,
|
||||
rom_name: str | None = None,
|
||||
@@ -28,8 +26,6 @@ def create_feedback(
|
||||
user_id=user_id,
|
||||
content=content,
|
||||
contact=contact,
|
||||
source=source,
|
||||
scene=scene or None,
|
||||
images=images or None,
|
||||
app_version=app_version or None,
|
||||
device_model=device_model or None,
|
||||
|
||||
@@ -49,28 +49,3 @@ class CouponStatsOut(BaseModel):
|
||||
"""
|
||||
|
||||
coupon_count: int
|
||||
|
||||
|
||||
class CouponSessionIn(BaseModel):
|
||||
"""客户端领券流水上报体(admin「领券数据」看板数据源,POST /api/v1/coupon/session)。
|
||||
|
||||
一次领券两段上报,按 trace_id upsert 到 coupon_session:
|
||||
- 发起(status=started):带勾选平台 + 机型/ROM/app_env + started_at_ms(发起墙钟毫秒)。
|
||||
- 收尾(completed/failed/abandoned):带 elapsed_ms(全程耗时)+ platform_elapsed(各平台耗时)+ claimed_count。
|
||||
不鉴权(同领券循环 MVP,按 device_id/trace_id),user_id 登录态带上做留痕(可空)。
|
||||
"""
|
||||
|
||||
trace_id: str
|
||||
device_id: str
|
||||
status: str # started / completed / failed / abandoned
|
||||
started_at_ms: int # 发起墙钟毫秒(客户端 System.currentTimeMillis)
|
||||
user_id: int | None = None
|
||||
platforms: list[str] | None = None
|
||||
origin_package: str | None = None
|
||||
device_model: str | None = None
|
||||
rom: str | None = None
|
||||
app_env: str | None = None
|
||||
elapsed_ms: int | None = None
|
||||
platform_elapsed: dict[str, int] | None = None
|
||||
claimed_count: int | None = None
|
||||
trace_url: str | None = None
|
||||
|
||||
@@ -30,14 +30,10 @@ class FeedbackQrConfigOut(BaseModel):
|
||||
class FeedbackRecordOut(BaseModel):
|
||||
id: int
|
||||
content: str
|
||||
# 比价反馈的问题场景(找错商品/优惠不对…);普通反馈为 None
|
||||
scene: str | None = None
|
||||
images: list[str] = Field(default_factory=list)
|
||||
status: str
|
||||
reject_reason: str | None = None
|
||||
reward_coins: int | None = None
|
||||
# 运营给用户的回复留言(用户端可见,采纳/未采纳都可能有)
|
||||
admin_reply: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
|
||||
@@ -174,13 +174,9 @@ class FeedResponse(BaseModel):
|
||||
|
||||
class TopSalesRequest(BaseModel):
|
||||
"""销量最高 tab:从离线库 meituan_coupon 按销量降序取(不实时打美团)。"""
|
||||
# 可选:老客户端(本次改动前发版)不带经纬度。缺省时后端降级返空(status=degraded),
|
||||
# 不做 422 硬拒,也不误返"全城"结果。新客户端会传坐标 → 按城市过滤。
|
||||
longitude: float | None = Field(None, description="经度(用于定位城市;缺省=老客户端,降级返空)")
|
||||
latitude: float | None = Field(None, description="纬度(用于定位城市;缺省=老客户端,降级返空)")
|
||||
page: int = Field(1, ge=1)
|
||||
page_size: int = Field(20, ge=1, le=50)
|
||||
platform: int | None = Field(None, description="可选: 1只外卖 / 2只到店; 不填=全部(同城销量)")
|
||||
platform: int | None = Field(None, description="可选: 1只外卖 / 2只到店; 不填=全部(全城销量)")
|
||||
|
||||
|
||||
# ───────────────── 换链 请求 / 响应 ─────────────────
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
城市ID 城市名称 省份名称
|
||||
3NUYJKKJXPHVNZUHFK3HWUDHNM 宣城市 安徽省
|
||||
LXXSHOY7LNK74ZK2SKVUXFY72Q 阜阳市 安徽省
|
||||
ZEBF2LBJOEHGM4XGFPNW4IHBIA 合肥市 安徽省
|
||||
WADWEY3GR6IARJLSNGQWG2KI4E 滁州市 安徽省
|
||||
Z62QL3X66AOT6MSY7LB6LVO4CI 芜湖市 安徽省
|
||||
M6QCWRCECRT6ZEJ6RERR6IWHFI 淮南市 安徽省
|
||||
UP2NBJACSAO7FUH4XQQDMBOUE4 马鞍山市 安徽省
|
||||
WLL7BSHUBPTLTMITFITJX2GKWY 蚌埠市 安徽省
|
||||
ECTBNJ4KNNHPGXEVI4TBJNU7BY 亳州市 安徽省
|
||||
KNEGDNOSRGNQPAMOMH54ATLIUU 六安市 安徽省
|
||||
YH53FRR55H6VA4KXW36OJ7RNSE 宿州市 安徽省
|
||||
LH6LX5DUVFPOFCAQ6NEGPWCLBI 淮北市 安徽省
|
||||
RIFXWJ46SEJXE7EP6HZXEUIALU 铜陵市 安徽省
|
||||
FK6F7KMO4WNARZKUXMMSBWGATM 安庆市 安徽省
|
||||
M3CHGNNUSFSPS5HRV3W7MCBWFI 黄山市 安徽省
|
||||
XFI6PM7SRO6NHBGYQUZEEA2WT4 池州市 安徽省
|
||||
D2ZILSASTTGEFQWZIDTHA7PWU4 澳门 澳门特别行政区
|
||||
WKV2HMXUEK634WP64CUCUQGM64 北京市 北京市
|
||||
HPMKHLM3QR6EZGMY7GEI4H3QYQ 泉州市 福建省
|
||||
6V523YRU54Y3PEAM2XPADNJM2U 福州市 福建省
|
||||
HH3ZZCERPVQIYUZPW4A2U4JKZI 莆田市 福建省
|
||||
BQ5RWEJS4O7W27SQMLPMRIRJDU 宁德市 福建省
|
||||
TDN7XDQMZEP6ZCK6UO3VVMCSMM 三明市 福建省
|
||||
ZKNTORN6YTZL2BXRYUSRGV3CHU 厦门市 福建省
|
||||
Y2DI2QLZN5NNACMD3KI2DBR4IA 龙岩市 福建省
|
||||
C4RS32I6QAHWLP55UQWI3N5LLY 南平市 福建省
|
||||
5T23WDEOAP7RYNU4JNL2ZG7PDQ 漳州市 福建省
|
||||
UHZBROATFB2KWNMNLS23DPRJBY 定西市 甘肃省
|
||||
WLPAHIUOIVKS644QSN4V5ZY5XQ 金昌市 甘肃省
|
||||
J7TO3UHZ57ABNUKIQUBCIJZQFA 白银市 甘肃省
|
||||
GX277SS75375VEFYVCEHBL6ISA 临夏回族自治州 甘肃省
|
||||
ORA3R7F2LSJHUOCIDTZCP54G7Q 张掖市 甘肃省
|
||||
L6VQYYOJNTHSLW5JCR5SXBITDM 武威市 甘肃省
|
||||
65OXQPQVFXNOYHXTMCE2RNFRL4 兰州市 甘肃省
|
||||
IUIZYQ7E2SPMEAIGOUUNZWBQOA 天水市 甘肃省
|
||||
BYBRGRDRV4NKAWCU6GWBIVLX3Q 酒泉市 甘肃省
|
||||
KDST2VRETG6WK5SMJO2G2FN2RU 嘉峪关市 甘肃省
|
||||
R3Q2XWVFVF4T2ADZZZMPZJRWBA 庆阳市 甘肃省
|
||||
7FWNT2TP66SU4QEP6IBBMZWFNE 陇南市 甘肃省
|
||||
T33S2GYGPVHAL5SR2FLSPTJSBE 平凉市 甘肃省
|
||||
RR6KAWBLOKD4H2UINKYPFCPXO4 甘南藏族自治州 甘肃省
|
||||
QKX4DS3CTJJG7SFW5SBHPLD42I 茂名市 广东省
|
||||
JJZ75A32XCQNZU4IN2ZCEUGN3M 梅州市 广东省
|
||||
SJSOXOSJASLUT6LBH4E32SUKKQ 清远市 广东省
|
||||
SQQWAN5BQOVSX55S7EPF7QHMAU 珠海市 广东省
|
||||
NGRJMW6JMRS6U2KUJEONSORAEY 韶关市 广东省
|
||||
FAUMIGOSOET4E5WR5BL6P3OZHA 佛山市 广东省
|
||||
JSBIH55ICFZQ2D3LEV47YMZ2NI 河源市 广东省
|
||||
KOYAYPD2DBLDCF2ZI5GCW4LF6Y 中山市 广东省
|
||||
647JGFPUYM4VWVLZCPSHT63XKQ 汕头市 广东省
|
||||
AMOIPZW3Q2NMTDSEREFVM4SV74 深圳市 广东省
|
||||
XIHEJY4H2CDZJCLIXDIN36BKXQ 广州市 广东省
|
||||
UZ6OT4CYUR42KCTTED2KJW6EGA 东莞市 广东省
|
||||
RM3HLOIUEYKTQ5O2JSVEKDMFRY 阳江市 广东省
|
||||
C6XNJAZA6N3NNUDBUU3JIZPTDI 潮州市 广东省
|
||||
AUPF3G2ULSV4TDT4L3NMHRTY6Y 揭阳市 广东省
|
||||
7HIITKBPRXTVBA2FBBN443XITQ 云浮市 广东省
|
||||
TO6ILZ7MPJMJN3S7W2SXMIFQQY 江门市 广东省
|
||||
DTTBMGCIOMPCZY5NETUEKWJ6PY 汕尾市 广东省
|
||||
HJY7JYWBA6FQY42RYSKX3RZTRI 湛江市 广东省
|
||||
SLICHB4FBDVDLI53MR74WVUUNI 肇庆市 广东省
|
||||
ZPX4JXJVBBYSSD2KTWHAPXO6NE 惠州市 广东省
|
||||
JH4Q44RQA4EZ3Q6MHQVEVE7KZQ 百色市 广西壮族自治区
|
||||
H2JXFEJFIL4PPFMYOS4MHZ5IBM 崇左市 广西壮族自治区
|
||||
SXIRRISUOEGBU335AWT2ZFL6A4 贵港市 广西壮族自治区
|
||||
HEQHKC4KP7YGGYVBZM5JEUI5AQ 北海市 广西壮族自治区
|
||||
SKGG7KMFKVDIDKRVQEPTS7SIE4 贺州市 广西壮族自治区
|
||||
N4WR7CWCULNA5Z35OTDJSZYDCU 钦州市 广西壮族自治区
|
||||
T4RXX2WY6WPQYZEUXVJNZBXQZU 梧州市 广西壮族自治区
|
||||
3R23AS3EIY7EYE2D5MWWORZODI 河池市 广西壮族自治区
|
||||
57SMWWCV7X44E256P4I23OQ3AA 防城港市 广西壮族自治区
|
||||
YHGHVIQ37UCTNQ4JKPQEAUWIQA 桂林市 广西壮族自治区
|
||||
MQJZTM455OKZLAN5WQYUTA5TDE 柳州市 广西壮族自治区
|
||||
C6FZPLB4NJQ6VUPSKDJH3EWQDM 玉林市 广西壮族自治区
|
||||
BFSU5W6E5XBIDFPQLVPGRDSATY 南宁市 广西壮族自治区
|
||||
CKXOQUZDNOVNME3PEBOY2CULQQ 来宾市 广西壮族自治区
|
||||
LV32FV6IQTKFR7JIBEQMHRUVCA 贵阳市 贵州省
|
||||
F26RCNKMFTZONJCSJ5C6FHVY74 毕节市 贵州省
|
||||
G2LMYRWVRCK7BTD2WM4NWX4SYM 黔南布依族苗族自治州 贵州省
|
||||
MFSOO3NBMB2PVLIVSI5EJK7MWY 黔西南布依族苗族自治州 贵州省
|
||||
KWUL44L7SEMJGIMXCWSSEB3OOA 遵义市 贵州省
|
||||
T2P3OFGQZUGR7D6TGRCMDF22GI 铜仁市 贵州省
|
||||
AGUFUANSZNGC4TMOPZO65IRSPI 六盘水市 贵州省
|
||||
2XOCOSNUAK3J5QDGTKIBWKK7KU 安顺市 贵州省
|
||||
6XRTSAEYJTA2UBKXO4XEPQE5ZY 黔东南苗族侗族自治州 贵州省
|
||||
YRMKRP2GOE2VMRS73N4YRIZUHY 三亚市 海南省
|
||||
CJRGVLBNLJAVBJ4ZKKIZ3FZ2LY 白沙黎族自治县 海南省
|
||||
5XOUAJ5Z4J4K7SVIQGXL2OM2JQ 保亭黎族苗族自治县 海南省
|
||||
2UFQ6A2QRJPXPH3VOYEAQHMVSQ 海口市 海南省
|
||||
TGCVXVS4M7NDQM4ROUDCVI6I3A 承德市 河北省
|
||||
JEUP6QWCOXPSM3SQTINQCJKIGM 衡水市 河北省
|
||||
Z442MNCW6BO2BBHIRUPRBACXPI 唐山市 河北省
|
||||
RFE6R34GD4FY3LUKC2ICSF6AFY 张家口市 河北省
|
||||
CWJN55M73VZDCYJEQ7AHDBWGGY 沧州市 河北省
|
||||
DFL4ES776ECRGBYNOPLWKB247I 雄安新区 河北省
|
||||
ZLSXYY34IHBHIC2NOVPQQBFTBE 保定市 河北省
|
||||
3DO6Z2QRJQFMPLLDS55PG7DSBU 石家庄市 河北省
|
||||
PR57XT25LI3246VGASEPSHP63E 邢台市 河北省
|
||||
SKYLNH737BS56TD452FOKYL36U 邯郸市 河北省
|
||||
5PWPERL7GQKJD6QPLR2TWUUM7E 秦皇岛市 河北省
|
||||
5T2TGV6SJFVL3MO7HIMN2KTQTA 廊坊市 河北省
|
||||
ECSTLZ7GP7IX3MB5EVNKS47MLE 焦作市 河南省
|
||||
VTWW34QB2F5Q4LW7ISNUMWX7GY 开封市 河南省
|
||||
D2NUN47NY4Q55X3UED4JMSI6CM 周口市 河南省
|
||||
TR3XJFQR4EFYRRIX7TUQF3B26Y 郑州市 河南省
|
||||
CKJGF5S6XMHW5ZJEBU7MJC47QA 新乡市 河南省
|
||||
IFASZ625MCFJQKPLJ7EA2SMJUU 商丘市 河南省
|
||||
SKXPYKTTRG4YAUHE2HZXWRWXGM 鹤壁市 河南省
|
||||
G5LXE74CUHO2K6BBRN7Q5DSRJY 漯河市 河南省
|
||||
SLNOAFJV2LTBSH7SJCRXJA36K4 驻马店市 河南省
|
||||
65WO7LH7CFDUGKYMXQLRAYKKWM 安阳市 河南省
|
||||
RIX2X7FAVTZCAQ5RT2C2CWK22Q 南阳市 河南省
|
||||
SZOW5OY3U54SSY4WRC65VJUNTI 平顶山市 河南省
|
||||
7VPIDDUS4P2LSZ6Q5S57MAQDEM 信阳市 河南省
|
||||
4VYCRORUOZ4DC2U6S3CT6H6KWE 洛阳市 河南省
|
||||
M5WNO2BQ3UGLLHBCG4NCEFPP5U 濮阳市 河南省
|
||||
LY3O6PBPIWETMA3ZOL6ETY5UB4 三门峡市 河南省
|
||||
FXXJLIRE72LS2W4OWWQVJMRJHA 许昌市 河南省
|
||||
5XH353QTY3VWF2KYOCZCL3TOXY 牡丹江市 黑龙江省
|
||||
2KGRZKF6IECV2W7K5J64Y2LY4M 齐齐哈尔市 黑龙江省
|
||||
FASGWS5ADVSTFGJG6TGBZPZP6Q 鹤岗市 黑龙江省
|
||||
2O6CDIXSWIKBXILZEEPKCS7MVI 双鸭山市 黑龙江省
|
||||
OZ2PTOBYTBG57XJZMIC23QFJKM 佳木斯市 黑龙江省
|
||||
FO24MQMULT3J5JW64APNXSQEPU 伊春市 黑龙江省
|
||||
ETZ2HYWVU6U7SKU6G4JAO64RUQ 黑河市 黑龙江省
|
||||
PR7EJNBY2VZBEUT36JAWE3TM7I 七台河市 黑龙江省
|
||||
HADAAVLERKIW4SQGCTQYGX4AL4 哈尔滨市 黑龙江省
|
||||
CGTU45YC5C3JYLHMA47USDPA7Y 大庆市 黑龙江省
|
||||
T4W7SQIPOM4EYMEFFRAB5BSTII 鸡西市 黑龙江省
|
||||
TYGZHNQL6YT7CX6EEG5DJQQHMA 绥化市 黑龙江省
|
||||
OOSJTSN2CVUUCKD6XAB7EYYIPY 大兴安岭地区 黑龙江省
|
||||
I3YF3EKZHIZTN6TZOTYTGZ2UXQ 随州市 湖北省
|
||||
ESGVBOSTHW7JWEVCGYJUTEHEBQ 宜昌市 湖北省
|
||||
MTJRWJ53XBW5SBTWHKNNZDLM7U 十堰市 湖北省
|
||||
PXZLF2ISKQL5ACM67ZCBNOGDT4 黄石市 湖北省
|
||||
44RMTOEHPUFXBHZXX4IQ4IRZVQ 荆州市 湖北省
|
||||
OTKZGG743NFC474ADMMRX4ZOOA 鄂州市 湖北省
|
||||
EXOUAZAQ73OEFAK72CHQ32GQHQ 恩施土家族苗族自治州 湖北省
|
||||
SUCY7I72QJDZD7EBFXREIQ67SI 咸宁市 湖北省
|
||||
QENSGB5R7HGYDXCG2LQZQTO3TU 荆门市 湖北省
|
||||
OHIWL6SAE2PR4EJR4BOMLAE6FU 武汉市 湖北省
|
||||
ZXCE4WV2CDVPQTA4HAOVELQMNE 襄阳市 湖北省
|
||||
KEFN5OPSS4ZZF6NU2TTL72S6HE 孝感市 湖北省
|
||||
ROAHLMQ67H6M5NDFVXROJG723E 黄冈市 湖北省
|
||||
YBEBX2YYN4WPBNH6Z6C73DNE7I 张家界市 湖南省
|
||||
45XGRKYGSCPE5VNRYF4FVJGFMM 株洲市 湖南省
|
||||
LK3SEIBRU7GTDT4J2EPTLIO33U 永州市 湖南省
|
||||
R4YXFIK53W5E556BSGSBJWS4DM 郴州市 湖南省
|
||||
PQDO3RNADWXX75OWZW2GSXJ4SE 怀化市 湖南省
|
||||
RRRT6QOJYEJ432L3F76ZN5NHCA 长沙市 湖南省
|
||||
B6WPNMCZ3ENQSV4NFY5MSTPDAM 岳阳市 湖南省
|
||||
KNDZW5EHDPKP2DX7HBLKP4DYLM 益阳市 湖南省
|
||||
PA2GHG3XZ7I47HTKZ4YAFH3OYY 湘西土家族苗族自治州 湖南省
|
||||
I7CNIUA5PYV2EHDEW3RGYT2R4U 邵阳市 湖南省
|
||||
SRI2SU4FN66FMJJCKQOCZD72ZY 常德市 湖南省
|
||||
H7UHHJAMQUL7UA5QEUTGKNSL3A 湘潭市 湖南省
|
||||
EFB255OBTB2BUDZENR5UVIC7ZQ 衡阳市 湖南省
|
||||
RDMANB4KCM3OJSNVGZWVYVME6E 娄底市 湖南省
|
||||
LD37PDU5OB4UAV5QDOBMKG5YTY 吉林市 吉林省
|
||||
EO3GF4XNF5RXWRPUVAT3KTQO4U 四平市 吉林省
|
||||
4GD7OS4CAQABH5YIWVK5SKGHMY 通化市 吉林省
|
||||
6DRI2R5VAWMYJHJPCJKUNMDYEQ 延边朝鲜族自治州 吉林省
|
||||
TVBCNVGUND4MOUOOUXFGX7DIUA 松原市 吉林省
|
||||
JYY62HSKBUVK5OU7KGJDKQ4RTA 白城市 吉林省
|
||||
QEDUHKMZ36CHJTKRD6O2ZPLNBU 长春市 吉林省
|
||||
4EADVCBJMZ5UBH2FVRT6QCLS2U 辽源市 吉林省
|
||||
EUQD5EGS2LR5KJSFNG6PPSIHHI 白山市 吉林省
|
||||
YLTIISPCLBEGTZZX3WUWAD7WDE 淮安市 江苏省
|
||||
L6U5DZP6MESXPMHOHCDMJS55O4 宿迁市 江苏省
|
||||
HQMLYA7TDGMYQAXFCDUXBZPYHI 镇江市 江苏省
|
||||
K6XJ4UN65ZD6XQKYEG5YN7HCRI 盐城市 江苏省
|
||||
36I4X3EZZU4EHOSCLQI5OAKKBE 南通市 江苏省
|
||||
S3GWFQU6QAVRDKLJT77LD6OFLE 泰州市 江苏省
|
||||
IO6F4AFGAVIFRGYTZEC4TXM7W4 无锡市 江苏省
|
||||
NUXNK2VOFSD2JFTEO2AMWX6NSU 扬州市 江苏省
|
||||
TEVZU6CU6SK57HFW7DFNGMQ44A 南京市 江苏省
|
||||
UTYSRBQ4FSB7XLWCF3Z2HTKNUA 常州市 江苏省
|
||||
OCZOBCJDEXKE7KBN3BD7AYQG2Q 徐州市 江苏省
|
||||
6LIBPJGZROLXE3CLZGJRYMYBOU 连云港市 江苏省
|
||||
FS4PIU74F7QKYARDWR5ZMOLICI 苏州市 江苏省
|
||||
R2F4OWUO65HYZW2IQIKINORZ7Y 赣州市 江西省
|
||||
YW346BTN3VFYNRC3744UR5MZXY 抚州市 江西省
|
||||
QR3FDR26U2EJIXOMBHL7IJLQSA 南昌市 江西省
|
||||
OAJHJL7L7VNW2Q5UXRE7F4CUJQ 九江市 江西省
|
||||
OMH7D45R4DX2KNHLV3G2UP56OY 景德镇市 江西省
|
||||
YSB2PAEROB2IZSJZFVFH7KJPEI 鹰潭市 江西省
|
||||
5OYAMNORCXKYA6UF7DW6KFFBIU 上饶市 江西省
|
||||
SMHZOYKE7BXQJ2NT6Q24TFMLEQ 吉安市 江西省
|
||||
2RZV26OUPKUHUJ5ZPB673VDGZU 萍乡市 江西省
|
||||
232VHZEEZ6SXACE4AC5HQ4ZTFQ 新余市 江西省
|
||||
QRLM74YXNDW2QDBWLTFGEMXK2I 宜春市 江西省
|
||||
S6OHUVUKIIWPVMQD44RREUMNT4 葫芦岛市 辽宁省
|
||||
DQQ4OIFUGFYJY3XZRK5VDWMLCA 辽阳市 辽宁省
|
||||
S4YXGFEYXEUG6ISZ6O337OPVSI 阜新市 辽宁省
|
||||
D3JHM7A4CG6RJMBD7YRDS5JOYU 盘锦市 辽宁省
|
||||
VTRWMOSS6PCUYUAIPG6VPBKUUQ 营口市 辽宁省
|
||||
ZPHFGWBIEVLKP5CVZNZUB3CRT4 朝阳市 辽宁省
|
||||
NGYYULZ4UAGD3Q2PG726FFXSHU 抚顺市 辽宁省
|
||||
Q5BRTSW752VSHIAKLLL7KL5TNA 锦州市 辽宁省
|
||||
ZGV3WNPOSS7J4ZWBP6ZQG46BNM 沈阳市 辽宁省
|
||||
XEX676YYMTYIV5QPIUZB4TA7IY 本溪市 辽宁省
|
||||
PRTEQZMLNLQNZXJHRCYYLWZB4E 丹东市 辽宁省
|
||||
4GN4WF6UQRFU64T4FVZPRDXRWQ 鞍山市 辽宁省
|
||||
3QTZDFLJFSLLOOVCZ65PSDAVOU 铁岭市 辽宁省
|
||||
CC4ZTMKKXI73ZEVT5QQTJN5SMM 大连市 辽宁省
|
||||
3MBJEFDLAVOMQZ7L7CM5MNSYKA 鄂尔多斯市 内蒙古自治区
|
||||
5NOS4YC5WO2IZCQPVB6MCBYDJ4 呼和浩特市 内蒙古自治区
|
||||
OQNIP675H7L5R64652BH7KHUOQ 通辽市 内蒙古自治区
|
||||
4WA6I63MGVINV5DNLNWRRHCDDM 阿拉善盟 内蒙古自治区
|
||||
ELI6BDJBAN6RCYTETMK2EX2UKU 乌兰察布市 内蒙古自治区
|
||||
PV5ZAAXFW2DZCVZKCF4I4KK7BQ 巴彦淖尔市 内蒙古自治区
|
||||
YU6UUT6G6T6AMWTJFECDIUQFEQ 乌海市 内蒙古自治区
|
||||
V4MYANW5QFZCXG3FIDPXA3HOTE 呼伦贝尔市 内蒙古自治区
|
||||
S5DCFJWJ7J3MJSLY2PHKWLNPOQ 包头市 内蒙古自治区
|
||||
LY7SAZRFSJJMRU3JEO5SKNKIVM 兴安盟 内蒙古自治区
|
||||
NM2XP54CNQCFOILKACYEQWUSGM 锡林郭勒盟 内蒙古自治区
|
||||
S5G3IO75IDEJPZQA6VFM3OYPDI 赤峰市 内蒙古自治区
|
||||
UUFUUPM5RT6ZU5UKILQC5YQV54 吴忠市 宁夏回族自治区
|
||||
VMSRLIATK44WQXQEWAL63AXJ3M 固原市 宁夏回族自治区
|
||||
4GWWCAAKGNJV2SMQPSWWZNCGYY 中卫市 宁夏回族自治区
|
||||
6IE7GEETBQEF7GUSGU2FLIUEEM 石嘴山市 宁夏回族自治区
|
||||
VI4YIH3URSON4Q4MWOEESXJ56Q 银川市 宁夏回族自治区
|
||||
SIE4ED6QWVRT727GEHWBFH3DAA 海东市 青海省
|
||||
JNJH6OJZIOQKXDXWW5ZGEHG5MA 海西蒙古族藏族自治州 青海省
|
||||
MJADYNCKQNDJU2TXACTDP5I52M 海北藏族自治州 青海省
|
||||
LRGFXIVB6RJWQWYAFH7EIUHCPE 黄南藏族自治州 青海省
|
||||
J4TG3PCK2ZEMNEUMIPZF32UNQY 果洛藏族自治州 青海省
|
||||
2YS5POGG53LKZGFBIUMDWP57SM 玉树藏族自治州 青海省
|
||||
GRZMJEZCA2DNCZK3O6ZSUHMRPM 西宁市 青海省
|
||||
NBFQIACRBBCAH5AZWJ5LVT7AU4 海南藏族自治州 青海省
|
||||
MQUKCLQ76P4FRRECDBA3HBKT7Q 滨州市 山东省
|
||||
633FVSBDDBM5WSMXSKOCX6QC5I 潍坊市 山东省
|
||||
4434FVT3PXLMV6UAWLEW6O3M5A 菏泽市 山东省
|
||||
P7PK4UBVCOHW3PI6IPEIA54DLY 济南市 山东省
|
||||
I5M6JGTGSQEWX6HL7E5I6GRBAY 德州市 山东省
|
||||
V562AOMBVU5NG5GB3EPK6U42XY 烟台市 山东省
|
||||
4OSPHTE5TD24J6DYGR6DXMEDKY 淄博市 山东省
|
||||
227TLAVTUJABWPJD4S4ZECJ3FY 临沂市 山东省
|
||||
DAEZKZU32ZAPJGUTA6LLGO3WTY 聊城市 山东省
|
||||
LBRRK2EOYJN5MLYJWT4R3QBSXM 东营市 山东省
|
||||
AB6PBGCDBTNTG4KUQROY2FJ4GY 枣庄市 山东省
|
||||
EVANGU7WZCVRAAM6NWTDJVP7SU 济宁市 山东省
|
||||
GNUEGWZ3OKRWAKKVJ5THHHX6YY 泰安市 山东省
|
||||
F3VWSF4ART2FYYBBOZYWKRXTUI 青岛市 山东省
|
||||
KD6MNWWLVKB4E655XMV6MMA3KE 日照市 山东省
|
||||
LHYVF4LBCOZ34G3WNZYUVEIQGA 威海市 山东省
|
||||
ENVYDMYGDO3BMDXSAVQYZXLX74 阳泉市 山西省
|
||||
UXOUG4UIF7ZRJYCNMQJ3LDN5FY 临汾市 山西省
|
||||
4NZPT6Z35BMYACJ2HZGHUWRJ6E 吕梁市 山西省
|
||||
KSNXQME2A3VFHCE3DM3SFZKIJQ 晋城市 山西省
|
||||
HDOX7WKYSHJKEHET6TUYMVCTMQ 太原市 山西省
|
||||
DFJIZVXJGBGBIABPSL3DGMIDIE 长治市 山西省
|
||||
5KQYYTJR2EMP653QIALMA6LXXI 忻州市 山西省
|
||||
T76EOJA332RIHML7B6LYS5LF4U 朔州市 山西省
|
||||
HVX67CKT5TS6GPRDFDYOOLK4PE 大同市 山西省
|
||||
S4NGXQJDOH7E4IHDWOH3EK6IIE 晋中市 山西省
|
||||
GWDLZXLAWU54FKQ6G3HRQRR7E4 运城市 山西省
|
||||
3FFTTN5PPV7MBCE5AGY2NGYOOI 安康市 陕西省
|
||||
GACVPL3SWO3ZKH73JMJV6YI4NY 延安市 陕西省
|
||||
6KPS7VRMW57P2DAC6OPR4ISHQQ 商洛市 陕西省
|
||||
OMMF6XLNDNYWG5TBSNTWO2ZJZ4 渭南市 陕西省
|
||||
EALFXGMWYRS6E6TWXQ2K3YHV4M 咸阳市 陕西省
|
||||
WFG7U6JNUWDIS5ZZYM4FSM5C64 榆林市 陕西省
|
||||
R3VBMYTCF5LVHO35X3MYJQFOOE 宝鸡市 陕西省
|
||||
RQOWP7C234IS4RKSHB26IYZ5IU 西安市 陕西省
|
||||
K4YU6B4T5GZLRWVHGVCR3576HI 铜川市 陕西省
|
||||
VVFVAPLKSCN5KN4Q6RK2GPGUUA 汉中市 陕西省
|
||||
2QSF6IG3KMDXWO5VP7FXHMMKXA 上海市 上海市
|
||||
X3JCRNIPTCUU6DGOFFJ4MUK37M 眉山市 四川省
|
||||
GQ24IZNTZJ3PUB5FDAMDA4W7UI 攀枝花市 四川省
|
||||
6B6WT62WHBZRHPQUT7BAD2N6ZI 泸州市 四川省
|
||||
HJ35P4KXL442MLIII7PFFWUNAE 雅安市 四川省
|
||||
K4A6VSJH2AJYT46LMUSVQZPCCU 资阳市 四川省
|
||||
646ZNPATOOM3MHI3LDU6HI4KFI 阿坝藏族羌族自治州 四川省
|
||||
MU735ZDBFPXRQDUZ3I35JK3XEU 内江市 四川省
|
||||
4WPGGJ63USY77GSRN2PPFCYKPQ 广安市 四川省
|
||||
O4FFS4DALDAAKIFAUH4F5V5VS4 宜宾市 四川省
|
||||
IRFJVK2KXBE6BZ7CSN4UFCI624 绵阳市 四川省
|
||||
NELFD7FEKKUNDJ46VLD55SMDCE 甘孜藏族自治州 四川省
|
||||
TKMVEUPZSQCXNRZPBEIK3F45AI 遂宁市 四川省
|
||||
VQW7DPB4KTUI65COJBO3NODU24 巴中市 四川省
|
||||
STP4ELXTVGQSB572LFRFJRIUUY 南充市 四川省
|
||||
6ST5EX2JVXUCLR5GP5VEFSKN5M 成都市 四川省
|
||||
UWNFCMW3HYJRALQI2MJH6EM2O4 德阳市 四川省
|
||||
J5ZYU7XRV6CHSJAGOPQKS5YXNA 达州市 四川省
|
||||
KYJTF5S746T35RFBMR65BLGM6U 凉山彝族自治州 四川省
|
||||
RDUXR23XROLB4NGRVDDLXFXDSE 乐山市 四川省
|
||||
4TUBIBHMVESJUCGMTUSLJPHXWI 广元市 四川省
|
||||
AXQL57AO27NCHYMEOLRHAAKMTA 自贡市 四川省
|
||||
4RXX566RZORCXS6HLEX3DIICSM 花莲县 台湾
|
||||
BD5Y7SISWSSQGP3HTVPU6TXAH4 台东县 台湾
|
||||
I4DNWLECRYOJAZLGYQB7PBBJXQ 台中市 台湾
|
||||
GIZQIESFOMAEQSDQKOEQ5RTTPA 南投县 台湾
|
||||
MPM6M2C634FAW7KYG3KIHERDTU 彰化县 台湾
|
||||
NH2NK6JVOBBYYTK2G53ADWLX4Y 苗栗县 台湾
|
||||
DW2Q2R2UEEDNQA7IHBGYV423K4 新竹市 台湾
|
||||
FX5AOPFRPHGHHYZB4XNIPXLNNM 新北市 台湾
|
||||
UVNZNB6G4M35RV3IGRUN6OXMXE 屏东县 台湾
|
||||
MPB3M2YK24ZORO3EDCJ6UDWIGQ 基隆市 台湾
|
||||
EBHVITJPDHJEEMMZTM4TD4UVRU 台北市 台湾
|
||||
FQS4PZNCTQEX6I5F34Z2AGJUZM 高雄市 台湾
|
||||
WVOJ636Q7MGT6RMN6QYQ4SZWIE 嘉义市 台湾
|
||||
QRLER4EEMMKYGQLER2RWEQA74E 台南市 台湾
|
||||
K2LHF64R2P4OJ7MDHJ6J2NSTPE 桃园市 台湾
|
||||
BILG6LJIWUCTXZ6CDPXYAVM6XI 澎湖县 台湾
|
||||
3FYRA3O2HUMLIQAAJQCPX2TETE 宜兰县 台湾
|
||||
4MW6X22PAPVMHB6SBGF3RYS324 天津市 天津市
|
||||
YEYPP4SQOBXU5UCNDN7ORSR6DI 拉萨市 西藏自治区
|
||||
UNE6UPENGWQDOWGABDJEAQ2FEY 山南市 西藏自治区
|
||||
VDXKN2YCIUPOHBZKKQHPN6HJWE 林芝市 西藏自治区
|
||||
EAWIMNI77H72EYSOAYV3M76CB4 阿里地区 西藏自治区
|
||||
HCF6UHTXOOKIOA43AIJH3ARKXA 昌都市 西藏自治区
|
||||
Y47QI3KJY352QV3VOPXHM2IDWU 日喀则市 西藏自治区
|
||||
R4UWJX44GVAA54NFKHT4Y4ZC5A 那曲市 西藏自治区
|
||||
2D37GB5XUALJDXONWJIGXV3QXU 香港 香港特别行政区
|
||||
PA5W7Z255K3EGYA4LTA7BGLHRA 巴音郭楞蒙古自治州 新疆维吾尔自治区
|
||||
RJZOCU5ECQCOLCOCHJH2UNLQJM 哈密市 新疆维吾尔自治区
|
||||
RYK6AR3VDQJFXZLX3MHYFV5VAI 塔城地区 新疆维吾尔自治区
|
||||
MXUVGU5NPVTNINAKKPNLWUQ54Q 博尔塔拉蒙古自治州 新疆维吾尔自治区
|
||||
T5RKMSH5VIGRTHDZOKV2EIKPIM 伊犁哈萨克自治州 新疆维吾尔自治区
|
||||
NABMKFZPOUCZMS4TUVJSZ24DNA 克孜勒苏柯尔克孜自治州 新疆维吾尔自治区
|
||||
2YQBXWNFYVJX4NU6WXB5II6X34 昌吉回族自治州 新疆维吾尔自治区
|
||||
DBKCQCCQU2URQG2EP5ZNPKBETY 乌鲁木齐市 新疆维吾尔自治区
|
||||
DON6KYBCR2XJQOJQGOZTYV4RMM 阿克苏地区 新疆维吾尔自治区
|
||||
WKG47NEVYII5JISZJN7QSI2BDQ 克拉玛依市 新疆维吾尔自治区
|
||||
DWLK6D3OUOXOVQHSEJVTRIDRAI 喀什地区 新疆维吾尔自治区
|
||||
SAPKF2PQGJD4UMVJZTC3IZKI64 阿勒泰地区 新疆维吾尔自治区
|
||||
ARWSLGG54LGUGN3XMIWW76NW34 吐鲁番市 新疆维吾尔自治区
|
||||
VQCWAKL6ADHYFTSVPBGPDFSB2I 北屯市 新疆维吾尔自治区
|
||||
ES5A6MOROAG6F2XAQ25TYYPWUE 铁门关市 新疆维吾尔自治区
|
||||
36IUY52AESIPF4QEQAR2RTNQYA 和田地区 新疆维吾尔自治区
|
||||
Z26KSUL6ULS65ITZNUCRRWBYJM 文山壮族苗族自治州 云南省
|
||||
XBBUUATPBD2IUJR47FCKVUXB2I 昭通市 云南省
|
||||
TK2LP3JCYYMPTV4OA3WJCH7UQ4 怒江傈僳族自治州 云南省
|
||||
ISJ4FESOYKCQ5LXOQO6NL7TQHA 曲靖市 云南省
|
||||
ELBUBVI5UMUIEQGIOAHPMESXFA 西双版纳傣族自治州 云南省
|
||||
QHOYHEGZM4WSJZPFEU6FCBYIWY 玉溪市 云南省
|
||||
4G4SPJ7MVHMYZAWMQ4642PMLVI 保山市 云南省
|
||||
HCHRV2LGJ2TJ4X6BWNWI2IMID4 普洱市 云南省
|
||||
IS4Q6NASBWHO3RFO7UCIWOXVI4 昆明市 云南省
|
||||
TDJZOAZFQUQPYRR5BHOJTG6RWM 红河哈尼族彝族自治州 云南省
|
||||
EIYC62RNU4SHQW3RLAMDPTQYTI 大理白族自治州 云南省
|
||||
BA3XFPITAYKBUWDKU3QONRHBC4 德宏傣族景颇族自治州 云南省
|
||||
6P6FFFO6C5MLNCVRJAJUICSVQI 临沧市 云南省
|
||||
OXHMWH2TSIDI7BQ43EHAMXJ6N4 丽江市 云南省
|
||||
QQPDT4LBI2K2KMBNXZ6YH7X2FI 楚雄彝族自治州 云南省
|
||||
XBG4EJAWCRJL2TDPNJ23PTFYHQ 迪庆藏族自治州 云南省
|
||||
DINNCH54AP74TJ62MICEYAZP74 宁波市 浙江省
|
||||
XYTSLYGB2ETU6HG7GIXA7X5SOE 嘉兴市 浙江省
|
||||
NNAALJZXGAWALR3LGE2V4UZT6U 丽水市 浙江省
|
||||
H5UOJ5MQYJ737GS3TXYN2OJHUU 杭州市 浙江省
|
||||
HG5VQGOMSCEGNXJXKO6XCNCHMY 湖州市 浙江省
|
||||
LJ2SWEPRINTYDH5A2QHRMI5US4 衢州市 浙江省
|
||||
TW4RRM62TDA7WWU77FDSLSAXGY 台州市 浙江省
|
||||
HBBN247QZ6YUW5ZYSS6D7RVJCA 绍兴市 浙江省
|
||||
GVFB23SJZGRPRXXTIKDCAOCDPI 金华市 浙江省
|
||||
UEW4ENX7N7IFGFM7FD5SZ5GI7Y 舟山市 浙江省
|
||||
HCCXS5DGRJQMYZMRLGVAMUIQEA 温州市 浙江省
|
||||
FDGY55I6IHKY76E3MWDBOT2R6Y 重庆市 重庆市
|
||||
@@ -1,65 +0,0 @@
|
||||
"""通过经纬度反查城市(reverse_geocoder 离线库,零网络调用)。
|
||||
|
||||
reverse_geocoder 内置 ~2.5M 条全球城市/聚居点的经纬度→地名映射表,
|
||||
构建一次 KDTree(~几十MB 内存)后,查询为纯内存搜索,不作任何外部网络调用。
|
||||
|
||||
⚠️ 必须持有单例、且用 mode=1(单进程):
|
||||
- reverse_geocoder 的模块级 rg.search()/rg.get() **每次调用都会 new 一个 RGeocoder**,
|
||||
即每次都重新解析 ~2.5M 行 CSV + 重建 KDTree(数秒/次)。绝不能在服务端按请求调用。
|
||||
- 默认 mode=2 用 multiprocessing 按 CPU 数 spawn 子进程做并行查询;在服务端 / Windows
|
||||
spawn 下会重复 import 主模块(无 __main__ guard 时直接报错),既慢又危险。
|
||||
故本模块持有一个 mode=1 的 RGeocoder 单例,建一次树、复用;查询走单进程内存搜索。
|
||||
|
||||
⚠️ 精度说明:gazetteer 里的中国数据粒度不一致——直辖市/省会通常直接命中城市名,
|
||||
但部分城市会命中到区/街道级(如天津→Erwangzhuang、西安→Zhangjiabao),
|
||||
此时 admin1(省级行政区)可作为回退。业务侧建议优先用 admin1 做城市级判定。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import reverse_geocoder as rg # type: ignore[import-untyped]
|
||||
|
||||
# mode=1 单进程 KDTree 的单例;None 表示尚未构建(见 ensure_loaded)。
|
||||
_geocoder: rg.RGeocoder | None = None
|
||||
|
||||
|
||||
def ensure_loaded() -> None:
|
||||
"""构建(或复用)RGeocoder 单例(幂等)。
|
||||
|
||||
首次调用解析 ~2.5M 行 CSV + 构建 KDTree(~秒级、数十 MB)。生产应在 main.py 的
|
||||
lifespan 启动阶段主动调用一次,把这份一次性成本摊到启动,避免砸在第一个
|
||||
/feed?tab=rec / /top-sales 请求上。mode=1 = 单进程,不 spawn 子进程。
|
||||
"""
|
||||
global _geocoder
|
||||
if _geocoder is None:
|
||||
_geocoder = rg.RGeocoder(mode=1, verbose=False)
|
||||
|
||||
|
||||
def get_city(latitude: float, longitude: float) -> dict[str, str]:
|
||||
"""根据经纬度反查最近聚居点。
|
||||
|
||||
返回 dict:
|
||||
- name: 最近聚居点名称(英文),如 "Beijing" / "Fengsheng";
|
||||
中国境内可能是区/街道级;海洋/无人区返 ""
|
||||
- admin1: 省级行政区(英文),如 "Beijing" / "Hubei" / "Chongqing Shi";
|
||||
直辖市 admin1 即为城市名
|
||||
- country: ISO 3166-1 alpha-2,如 "CN"
|
||||
- latitude: 匹配到的参考点纬度(字符串)
|
||||
- longitude: 匹配到的参考点经度(字符串)
|
||||
|
||||
未匹配到(海洋/远洋)时返回空字符串字段。
|
||||
"""
|
||||
ensure_loaded()
|
||||
assert _geocoder is not None # ensure_loaded 保证已构建
|
||||
results: list[dict[str, Any]] = _geocoder.query([(latitude, longitude)])
|
||||
if not results:
|
||||
return {"name": "", "admin1": "", "country": "", "latitude": "", "longitude": ""}
|
||||
r = results[0]
|
||||
return {
|
||||
"name": str(r.get("name", "")),
|
||||
"admin1": str(r.get("admin1", "")),
|
||||
"country": str(r.get("cc", "")),
|
||||
"latitude": str(r.get("lat", "")),
|
||||
"longitude": str(r.get("lon", "")),
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
"""美团城市词典 + reverse_geocoder 离线反查。
|
||||
|
||||
从 feed 入参的 latitude/longitude 计算出美团城市 ID,
|
||||
用于后续美团 CPS 接口的 cityId 参数。
|
||||
|
||||
⚠️ 跨系统耦合:本模块返回的 city_id 取自 data/city_dict.txt,而离线库
|
||||
`meituan_coupon.city_id` 由 ETL(另一套系统)灌入。二者必须用同一份城市 ID 口径,
|
||||
否则 `WHERE city_id == <本模块结果>` 会静默查到 0 行 → 接口永久降级返空。
|
||||
改动 city_dict.txt 或 ETL 的城市 ID 来源时,务必同步两侧。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from app.utils.geo import get_city as _get_geo_city
|
||||
|
||||
logger = logging.getLogger("shagua.meituan_city")
|
||||
|
||||
# city_dict.txt 作为运行时数据随包分发(见 pyproject [tool.setuptools.package-data])
|
||||
_CITY_DICT_PATH = Path(__file__).resolve().parent / "data" / "city_dict.txt"
|
||||
|
||||
# ─────────── 反向地理编码 admin1 → 中文省份名 ───────────
|
||||
# reverse_geocoder 的 admin1 格式不统一:
|
||||
# 直辖市: "Beijing" / "Shanghai Shi" / "Tianjin Shi" / "Chongqing Shi"
|
||||
# 省份: "Guangdong" / "Jiangsu Sheng" / "Hubei" ...
|
||||
# 自治区: "Xinjiang Uygur Zizhiqu" / "Tibet Autonomous Region" ...
|
||||
# 下面用前缀匹配,去掉了 Sheng/Shi/Zizhiqu/Autonomous Region 等后缀。
|
||||
|
||||
_PROVINCE_EN_PREFIX: list[tuple[str, str]] = [
|
||||
# 直辖市 — admin1 即城市名
|
||||
("Beijing", "北京市"),
|
||||
("Shanghai", "上海市"),
|
||||
("Tianjin", "天津市"),
|
||||
("Chongqing", "重庆市"),
|
||||
# 省
|
||||
("Hebei", "河北省"),
|
||||
("Shanxi", "山西省"), # 注意: 指山西省,不是陕西
|
||||
("Liaoning", "辽宁省"),
|
||||
("Jilin", "吉林省"),
|
||||
("Heilongjiang", "黑龙江省"),
|
||||
("Jiangsu", "江苏省"),
|
||||
("Zhejiang", "浙江省"),
|
||||
("Anhui", "安徽省"),
|
||||
("Fujian", "福建省"),
|
||||
("Jiangxi", "江西省"),
|
||||
("Shandong", "山东省"),
|
||||
("Henan", "河南省"),
|
||||
("Hubei", "湖北省"),
|
||||
("Hunan", "湖南省"),
|
||||
("Guangdong", "广东省"),
|
||||
("Hainan", "海南省"),
|
||||
("Sichuan", "四川省"),
|
||||
("Guizhou", "贵州省"),
|
||||
("Yunnan", "云南省"),
|
||||
("Shaanxi", "陕西省"), # 双写 a 是官方拼音
|
||||
("Gansu", "甘肃省"),
|
||||
("Qinghai", "青海省"),
|
||||
# 自治区 — 注意匹配顺序, Xinjiang 要在 Guangxi 前面(Guangxi 也是 Xi 开头但先匹配 Xin 不会误判)
|
||||
("Guangxi", "广西壮族自治区"),
|
||||
("Inner Mongolia", "内蒙古自治区"),
|
||||
("Nei Mongol", "内蒙古自治区"),
|
||||
("Tibet", "西藏自治区"),
|
||||
("Xizang", "西藏自治区"),
|
||||
("Ningxia", "宁夏回族自治区"),
|
||||
("Xinjiang", "新疆维吾尔自治区"),
|
||||
# 特别行政区
|
||||
("Hong Kong", "香港特别行政区"),
|
||||
("Macau", "澳门特别行政区"),
|
||||
("Macao", "澳门特别行政区"),
|
||||
# 台湾(city_dict 里省份名为 "台湾",没有省/自治区后缀)
|
||||
("Taiwan", "台湾"),
|
||||
]
|
||||
|
||||
# ─────────── 常见城市名 英文→中文 映射 ───────────
|
||||
# 覆盖所有直辖市 + 省会 + 一线城市 + 部分 reverse_geocoder 只能命中到区/县的城市。
|
||||
# key 全小写,匹配时做小写比较。
|
||||
_CITY_EN_TO_CN: dict[str, str] = {
|
||||
# 直辖市
|
||||
"beijing": "北京市",
|
||||
"shanghai": "上海市",
|
||||
"tianjin": "天津市",
|
||||
"chongqing": "重庆市",
|
||||
# 省会 / 副省级
|
||||
"guangzhou": "广州市",
|
||||
"shenzhen": "深圳市",
|
||||
"chengdu": "成都市",
|
||||
"hangzhou": "杭州市",
|
||||
"wuhan": "武汉市",
|
||||
"xi'an": "西安市",
|
||||
"nanjing": "南京市",
|
||||
"changsha": "长沙市",
|
||||
"zhengzhou": "郑州市",
|
||||
"jinan": "济南市",
|
||||
"kunming": "昆明市",
|
||||
"fuzhou": "福州市",
|
||||
"harbin": "哈尔滨市",
|
||||
"lanzhou": "兰州市",
|
||||
"guiyang": "贵阳市",
|
||||
"nanning": "南宁市",
|
||||
"shijiazhuang": "石家庄市",
|
||||
"taiyuan": "太原市",
|
||||
"shenyang": "沈阳市",
|
||||
"changchun": "长春市",
|
||||
"hefei": "合肥市",
|
||||
"nanchang": "南昌市",
|
||||
"haikou": "海口市",
|
||||
"hohhot": "呼和浩特市",
|
||||
"huhehaote": "呼和浩特市",
|
||||
"urumqi": "乌鲁木齐市",
|
||||
"wulumuqi": "乌鲁木齐市",
|
||||
"lhasa": "拉萨市",
|
||||
"yinchuan": "银川市",
|
||||
"xining": "西宁市",
|
||||
# 其他常见城市
|
||||
"xiamen": "厦门市",
|
||||
"suzhou": "苏州市",
|
||||
"qingdao": "青岛市",
|
||||
"dalian": "大连市",
|
||||
"ningbo": "宁波市",
|
||||
"wuxi": "无锡市",
|
||||
"foshan": "佛山市",
|
||||
"dongguan": "东莞市",
|
||||
"zhuhai": "珠海市",
|
||||
"zhongshan": "中山市",
|
||||
"wenzhou": "温州市",
|
||||
"shaoxing": "绍兴市",
|
||||
"jiaxing": "嘉兴市",
|
||||
"jinhua": "金华市",
|
||||
"taizhou": "台州市",
|
||||
"yangzhou": "扬州市",
|
||||
"nantong": "南通市",
|
||||
"changzhou": "常州市",
|
||||
"xuzhou": "徐州市",
|
||||
"zhengjiang": "镇江市",
|
||||
"yantai": "烟台市",
|
||||
"weifang": "潍坊市",
|
||||
"zibo": "淄博市",
|
||||
"linyi": "临沂市",
|
||||
"weihai": "威海市",
|
||||
"rizhao": "日照市",
|
||||
"luoyang": "洛阳市",
|
||||
"kaifeng": "开封市",
|
||||
"xinxiang": "新乡市",
|
||||
"nanyang": "南阳市",
|
||||
"yichang": "宜昌市",
|
||||
"xiangyang": "襄阳市",
|
||||
"huangshi": "黄石市",
|
||||
"zhuzhou": "株洲市",
|
||||
"xiangtan": "湘潭市",
|
||||
"yueyang": "岳阳市",
|
||||
"hengyang": "衡阳市",
|
||||
"mianyang": "绵阳市",
|
||||
"luzhou": "泸州市",
|
||||
"yibin": "宜宾市",
|
||||
"nanchong": "南充市",
|
||||
"zigong": "自贡市",
|
||||
"qujing": "曲靖市",
|
||||
"yuxi": "玉溪市",
|
||||
"zunyi": "遵义市",
|
||||
"guilin": "桂林市",
|
||||
"liuzhou": "柳州市",
|
||||
"sanya": "三亚市",
|
||||
"tangshan": "唐山市",
|
||||
"baoding": "保定市",
|
||||
"handan": "邯郸市",
|
||||
"qinhuangdao": "秦皇岛市",
|
||||
"langfang": "廊坊市",
|
||||
"datong": "大同市",
|
||||
"changzhi": "长治市",
|
||||
"linfen": "临汾市",
|
||||
"baotou": "包头市",
|
||||
"ordos": "鄂尔多斯市",
|
||||
"eerduosi": "鄂尔多斯市",
|
||||
"daqing": "大庆市",
|
||||
"qiqihar": "齐齐哈尔市",
|
||||
"jilin_city": "吉林市",
|
||||
"anshan": "鞍山市",
|
||||
"fushun": "抚顺市",
|
||||
"benxi": "本溪市",
|
||||
"jinzhou": "锦州市",
|
||||
"yingkou": "营口市",
|
||||
"dandong": "丹东市",
|
||||
"huizhou": "惠州市",
|
||||
"jiangmen": "江门市",
|
||||
"zhanjiang": "湛江市",
|
||||
"maoming": "茂名市",
|
||||
"zhaoqing": "肇庆市",
|
||||
"chaozhou": "潮州市",
|
||||
"shantou": "汕头市",
|
||||
"shaoguan": "韶关市",
|
||||
"meizhou": "梅州市",
|
||||
"jieyang": "揭阳市",
|
||||
"qingyuan": "清远市",
|
||||
"heyuan": "河源市",
|
||||
"yangjiang": "阳江市",
|
||||
"shanwei": "汕尾市",
|
||||
"yunfu": "云浮市",
|
||||
}
|
||||
|
||||
|
||||
# ─────────── 省会映射(城市匹配失败时回退) ───────────
|
||||
# city_dict.txt 内省份的第一个城市不一定是省会,故显式维护。
|
||||
_PROVINCE_CAPITAL: dict[str, str] = {
|
||||
"安徽省": "合肥市",
|
||||
"澳门特别行政区": "澳门",
|
||||
"北京市": "北京市",
|
||||
"福建省": "福州市",
|
||||
"甘肃省": "兰州市",
|
||||
"广东省": "广州市",
|
||||
"广西壮族自治区": "南宁市",
|
||||
"贵州省": "贵阳市",
|
||||
"海南省": "海口市",
|
||||
"河北省": "石家庄市",
|
||||
"河南省": "郑州市",
|
||||
"黑龙江省": "哈尔滨市",
|
||||
"湖北省": "武汉市",
|
||||
"湖南省": "长沙市",
|
||||
"吉林省": "长春市",
|
||||
"江苏省": "南京市",
|
||||
"江西省": "南昌市",
|
||||
"辽宁省": "沈阳市",
|
||||
"内蒙古自治区": "呼和浩特市",
|
||||
"宁夏回族自治区": "银川市",
|
||||
"青海省": "西宁市",
|
||||
"山东省": "济南市",
|
||||
"山西省": "太原市",
|
||||
"陕西省": "西安市",
|
||||
"上海市": "上海市",
|
||||
"四川省": "成都市",
|
||||
"台湾": "台北市",
|
||||
"天津市": "天津市",
|
||||
"西藏自治区": "拉萨市",
|
||||
"香港特别行政区": "香港",
|
||||
"新疆维吾尔自治区": "乌鲁木齐市",
|
||||
"云南省": "昆明市",
|
||||
"浙江省": "杭州市",
|
||||
"重庆市": "重庆市",
|
||||
}
|
||||
|
||||
|
||||
# ─────────── 城市字典加载 ───────────
|
||||
|
||||
def _parse_city_dict(path: str | Path) -> list[dict[str, str]]:
|
||||
"""解析 city_dict.txt,返回 [{city_id, city_name, province_name}, ...]。
|
||||
|
||||
city_dict.txt 格式(TSV):
|
||||
城市ID\t城市名称\t省份名称
|
||||
|
||||
示例行:
|
||||
3NUYJKKJXPHVNZUHFK3HWUDHNM\t宣城市\t安徽省
|
||||
"""
|
||||
data: list[dict[str, str]] = []
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
city_id, city_name, province_name = parts[0], parts[1], parts[2]
|
||||
if city_id == "城市ID":
|
||||
continue # 跳过表头
|
||||
if city_id and city_name and province_name:
|
||||
data.append({
|
||||
"city_id": city_id,
|
||||
"city_name": city_name,
|
||||
"province_name": province_name,
|
||||
})
|
||||
return data
|
||||
|
||||
|
||||
# 模块加载时一次解析
|
||||
try:
|
||||
_CITY_DICT: list[dict[str, str]] = _parse_city_dict(_CITY_DICT_PATH)
|
||||
except Exception:
|
||||
logger.exception("加载 city_dict.txt 失败,美团城市反查将不可用")
|
||||
_CITY_DICT = []
|
||||
|
||||
|
||||
def _build_province_index() -> dict[str, list[dict[str, str]]]:
|
||||
"""构建 省份名 → 该省全部城市列表 的索引。"""
|
||||
idx: dict[str, list[dict[str, str]]] = {}
|
||||
for entry in _CITY_DICT:
|
||||
idx.setdefault(entry["province_name"], []).append(entry)
|
||||
return idx
|
||||
|
||||
|
||||
_PROVINCE_INDEX: dict[str, list[dict[str, str]]] | None = None
|
||||
|
||||
|
||||
def _get_province_index() -> dict[str, list[dict[str, str]]]:
|
||||
global _PROVINCE_INDEX
|
||||
if _PROVINCE_INDEX is None:
|
||||
_PROVINCE_INDEX = _build_province_index()
|
||||
return _PROVINCE_INDEX
|
||||
|
||||
|
||||
# ─────────── 查询 ───────────
|
||||
|
||||
def _map_admin1_to_cn_province(admin1: str) -> str:
|
||||
"""将 reverse_geocoder 的 admin1 映射到 city_dict 中的中文省份名。"""
|
||||
if not admin1:
|
||||
return ""
|
||||
normalized = admin1.strip()
|
||||
# 多级匹配:先精确、再前缀
|
||||
for en_prefix, cn_name in _PROVINCE_EN_PREFIX:
|
||||
if normalized == en_prefix or normalized.startswith(en_prefix):
|
||||
return cn_name
|
||||
return ""
|
||||
|
||||
|
||||
def _lookup_city_in_province(city_en_lower: str, province_cn: str) -> str:
|
||||
"""在指定省份内查找匹配的城市名(EN→CN 映射)。"""
|
||||
if not province_cn:
|
||||
return ""
|
||||
index = _get_province_index()
|
||||
candidates = index.get(province_cn, [])
|
||||
if not candidates:
|
||||
return ""
|
||||
|
||||
# 1) 精确映射
|
||||
if city_en_lower in _CITY_EN_TO_CN:
|
||||
cn_city = _CITY_EN_TO_CN[city_en_lower]
|
||||
for c in candidates:
|
||||
if c["city_name"] == cn_city:
|
||||
return cn_city
|
||||
|
||||
# 2) 前缀/包含匹配(处理 admin1 直辖市场景:行政区 → 直辖市本身)
|
||||
for c in candidates:
|
||||
# 去掉"市"后缀比较
|
||||
city_core = c["city_name"].rstrip("市")
|
||||
if city_en_lower.startswith(city_core.lower()) or city_core.lower().startswith(city_en_lower):
|
||||
return c["city_name"]
|
||||
# city_en_lower 可能是拼音,city_core 是中文,尝试从 EN→CN 映射反向匹配
|
||||
for en_k, cn_v in _CITY_EN_TO_CN.items():
|
||||
if cn_v == c["city_name"] and (city_en_lower in en_k or en_k in city_en_lower):
|
||||
return cn_v
|
||||
|
||||
# 3) 匹配不到 → 返回省会
|
||||
capital = _PROVINCE_CAPITAL.get(province_cn, "")
|
||||
if capital:
|
||||
for c in candidates:
|
||||
if c["city_name"] == capital:
|
||||
return capital
|
||||
return candidates[0]["city_name"] # 终极兜底
|
||||
|
||||
|
||||
def _sanitize_city_name(name: str) -> str:
|
||||
"""去除 reverse_geocoder name 中常见的行政后缀使匹配更鲁棒。"""
|
||||
# 去掉 " District" / " Qu" / " Shi" 等英文后缀
|
||||
for suffix in ("District", "Qu", "Shi", "Sheng", "Xian", "Cun", "Zhen", "Xiang",
|
||||
"Zizhiqu", "Autonomous Region", "Special Administrative Region"):
|
||||
name = re.sub(rf"\s+{suffix}$", "", name, flags=re.IGNORECASE)
|
||||
return name.strip()
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def _resolve_meituan_city(latitude: float, longitude: float) -> dict[str, str]:
|
||||
"""反查实现;入参已量化(见 get_meituan_city),故 lru_cache 命中率高。
|
||||
|
||||
返回的 dict 被缓存复用 —— 调用方勿原地修改(get_meituan_city 已返回副本)。
|
||||
"""
|
||||
if not _CITY_DICT:
|
||||
return {"city_id": "", "city_name": "", "province_name": ""}
|
||||
|
||||
logger.debug("resolve_meituan_city: lat=%.2f lon=%.2f", latitude, longitude)
|
||||
geo = _get_geo_city(latitude, longitude)
|
||||
name_en = _sanitize_city_name(geo.get("name", ""))
|
||||
admin1 = geo.get("admin1", "")
|
||||
country = geo.get("country", "")
|
||||
|
||||
if country != "CN":
|
||||
logger.debug("resolve_meituan_city: 坐标(%.2f,%.2f)不在中国境内(country=%s)", latitude, longitude, country)
|
||||
return {"city_id": "", "city_name": "", "province_name": ""}
|
||||
|
||||
# 1) 映射省份
|
||||
province_cn = _map_admin1_to_cn_province(admin1)
|
||||
if not province_cn:
|
||||
logger.warning("get_meituan_city: admin1=%r 无法映射到中文省份", admin1)
|
||||
return {"city_id": "", "city_name": "", "province_name": ""}
|
||||
|
||||
# 2) 查找城市
|
||||
name_lower = name_en.lower()
|
||||
city_cn = _lookup_city_in_province(name_lower, province_cn)
|
||||
|
||||
# 3) 按省份+城市匹配 city_dict 中的城市 ID
|
||||
index = _get_province_index()
|
||||
candidates = index.get(province_cn, [])
|
||||
for c in candidates:
|
||||
if city_cn and c["city_name"] == city_cn:
|
||||
return {
|
||||
"city_id": c["city_id"],
|
||||
"city_name": c["city_name"],
|
||||
"province_name": province_cn,
|
||||
}
|
||||
|
||||
# 4) 最终回退:返回该省省会
|
||||
if candidates:
|
||||
capital = _PROVINCE_CAPITAL.get(province_cn, "")
|
||||
if capital:
|
||||
for c in candidates:
|
||||
if c["city_name"] == capital:
|
||||
logger.info("get_meituan_city: 城市匹配失败 name_en=%r, 回退到省会 %s", name_en, capital)
|
||||
return {
|
||||
"city_id": c["city_id"],
|
||||
"city_name": capital,
|
||||
"province_name": province_cn,
|
||||
}
|
||||
# 终极兜底:第一个城市
|
||||
fallback = candidates[0]
|
||||
logger.info("get_meituan_city: 城市匹配失败 name_en=%r, 回退到 %s", name_en, fallback["city_name"])
|
||||
return {
|
||||
"city_id": fallback["city_id"],
|
||||
"city_name": fallback["city_name"],
|
||||
"province_name": province_cn,
|
||||
}
|
||||
|
||||
return {"city_id": "", "city_name": "", "province_name": ""}
|
||||
|
||||
|
||||
def get_meituan_city(latitude: float, longitude: float) -> dict[str, str]:
|
||||
"""根据经纬度反查美团城市 ID + 城市名 + 省份名(对外入口)。
|
||||
|
||||
返回:
|
||||
- city_id: 美团城市 ID(如 3NUYJKKJXPHVNZUHFK3HWUDHNM);
|
||||
匹配失败时返回 ""
|
||||
- city_name: 中文城市名(如 "北京市")
|
||||
- province_name: 中文省份名(如 "北京市")
|
||||
|
||||
原理:
|
||||
1. reverse_geocoder 根据经纬度查出英文地名 + 省份
|
||||
2. 英文省份→中文省份映射(前缀匹配)
|
||||
3. 英文地名→中文城市名映射(精确映射 + 省内候选回退)
|
||||
4. 在 city_dict.txt 中按省份+城市名匹配城市 ID
|
||||
|
||||
城市名匹配失败的策略:
|
||||
- 直辖市(京沪津渝): admin1 本身即城市名,直接取
|
||||
- 省会: 回退到该省第一个城市(city_dict.txt 中每个省的省会通常排第一位)
|
||||
|
||||
实现说明:先把坐标量化到 ~1km(round 到 2 位小数)再进 lru_cache —— 原始 GPS 坐标
|
||||
每次抖动到小数点后 5~6 位,直接做缓存 key 几乎不命中;城市级解析对 1km 误差不敏感,
|
||||
量化后"同一地点反复请求"可命中缓存。返回缓存 dict 的副本,调用方可安全读写。
|
||||
"""
|
||||
return dict(_resolve_meituan_city(round(latitude, 2), round(longitude, 2)))
|
||||
+22
-73
@@ -1,83 +1,32 @@
|
||||
# 文档索引
|
||||
# 后端文档库(docs/)
|
||||
|
||||
项目文档结构说明。后续大模型增补/更新文档时,按此分类找到对应目录。
|
||||
`shaguabijia-app-server` 的文档都在这里。结构:**根目录放总览,其余按领域/用途分目录**。
|
||||
|
||||
---
|
||||
## 根目录
|
||||
|
||||
## API 接口文档 (`api/`)
|
||||
| 文档 | 作用 |
|
||||
|---|---|
|
||||
| [后端技术实现.md](./后端技术实现.md) | **后端技术方案总览**:业务概览、分层架构与目录、登录链路、美团 CPS、领券/比价透传、数据模型、配置与部署、已知问题。想了解"整个后端怎么回事"先看这份。 |
|
||||
| README.md | 本文件:文档库索引/传送门。 |
|
||||
|
||||
按业务领域分类,每个子目录对应一类接口。
|
||||
## 子目录
|
||||
|
||||
### 分类目录
|
||||
| 目录 | 作用 |
|
||||
|---|---|
|
||||
| [api/](./api/) | **API 接口文档**:"索引 + 一接口一文件"(组织方式见下)。想查"某个接口的协议"看这里。 |
|
||||
| [database/](./database/) | **数据库文档**:每张表一个文件(表结构/字段/索引/约定) + 两份迁移指南——[数据库迁移.md](./database/数据库迁移.md)(Alembic 操作:clone 后建表/日常升级/新增迁移/多 head 排查) + [postgres-migration.md](./database/postgres-migration.md)(SQLite → PostgreSQL 切换步骤,配套 `scripts/init_postgres.py`)。想"把库跑起来 / 改表结构 / 查某张表"看这里。 |
|
||||
| [integrations/](./integrations/) | **集成层实现文档**:穿山甲验签 / 微信支付 / 极光 / 短信 / 美团 CPS 等 SDK 集成的签名、加解密、协议细节与踩坑。想知道"接外部服务那块到底怎么实现"看这里。 |
|
||||
| [guides/](./guides/) | **开发 / 上线 / 功能 指南**:[待办与技术债.md](./guides/待办与技术债.md)(跨前后端 backlog,P1 鉴权/用户绑定、引擎移植待办等,"还欠什么、以后要补什么"看这份) + [看广告赚金币上线清单.md](./guides/看广告赚金币上线清单.md)(看广告发奖上线 checklist) + [邀请功能-实现原理与本地测试.md](./guides/邀请功能-实现原理与本地测试.md)(invite-mvp 实现原理 + 本地内网全链路测试) + [CPS发券分发与微信授权.md](./guides/CPS发券分发与微信授权.md)(**CPS 发券系统交接文档**:设群/建活动/生成落地页短链/美团对账/统计 + 微信网页授权拿 openid 做用户级统计;含数据流/表/平台差异/端点/配置/代码地图/排障/技术债)。 |
|
||||
|
||||
| 目录 | 分类 | URL 前缀 | 说明 |
|
||||
|------|------|----------|------|
|
||||
| [api/auth/](api/auth/) | 认证 | `/api/v1/auth` | 用户登录(极光一键登录/短信验证码)、Token 刷新(access + refresh)、登出、当前用户信息查询 |
|
||||
| [api/ad/](api/ad/) | 广告 | `/api/v1/ad` | 穿山甲 S2S 回调验签发奖、激励视频/信息流广告奖励结算、eCPM 上报、发奖状态查询、测试发奖(仅本地) |
|
||||
| [api/wallet/](api/wallet/) | 钱包 | `/api/v1/wallet` | 账户资产查询(金币/现金余额)、金币与现金流水、兑换规则与执行、绑定/解绑微信、提现申请/状态/记录、免确认收款授权 |
|
||||
| [api/coupon/](api/coupon/) | 领券 | `/api/v1/coupon` | CPS 领券透传(step/session)、引导窗频控(should-show/shown/dismiss/reset)、每日完成状态、累计领券统计 |
|
||||
| [api/compare/](api/compare/) | 比价记录 | `/api/v1/compare` | 比价记录上报/列表/详情/统计、比价战绩里程碑查询及奖励领取(按成功比价数解锁金币) |
|
||||
| [api/savings/](api/savings/) | 省钱 | `/api/v1/savings` + `/api/v1/platform/savings-feed` | 省钱大作战(battle)、省钱明细/汇总、平台省钱动态 Feed |
|
||||
| [api/signin/](api/signin/) | 签到 | `/api/v1/signin` | 每日签到执行、签到加速(看广告多领)、签到状态查询 |
|
||||
| [api/tasks/](api/tasks/) | 任务 | `/api/v1/tasks` | 任务列表查询、任务奖励领取(按 task_key) |
|
||||
| [api/invite/](api/invite/) | 邀请 | `/api/v1/invite` | 邀请码/分享链接生成、已邀请列表、邀请绑定(支持 clipboard/manual/fingerprint 三种归因) |
|
||||
| [api/user/](api/user/) | 用户 | `/api/v1/user` | 个人资料编辑(昵称)、头像上传、新手引导状态/完成标记、注销账号 |
|
||||
| [api/device/](api/device/) | 设备 | `/api/v1/device` | 设备注册/推送 token 更新、无障碍存活心跳、掉线检测(后置 pull)、告警确认 |
|
||||
| [api/platform/](api/platform/) | 平台配置 | `/api/v1/platform` | 平台统计数据、Feature Flag 开关、广告配置(穿山甲 ID)、App 版本更新检查(OTA);全部不鉴权 |
|
||||
| [api/intent/](api/intent/) | 意图/电商 | `/api/v1/intent` + `/api/v1/price` + `/api/v1/ecom` | 比价意图识别(Phase 1 单次/多帧/预券)、电商意图识别、比价步进(Phase 2) |
|
||||
| [api/meituan/](api/meituan/) | 美团 CPS | `/api/v1/meituan` | 美团券列表/信息流/推广链接/销量榜;全部无鉴权 |
|
||||
| [api/other/](api/other/) | 其它 | 分散 | 健康检查、CPS 短链重定向(含微信 OAuth)、反馈提交/配置/记录、埋点上报、订单上报、更低价上报、Trace 收尾 |
|
||||
| [api/admin/](api/admin/) | 管理后台 | `/admin/api` | Admin 独立子应用,二级目录按子资源拆分:`auth/`(登录)、`users/`(用户管理/财务操作)、`wallet/`(流水查询)、`withdraws/`(提现审核/对账)、`feedbacks/`(反馈处理)、`admins/`(管理员管理)、`ad/`(广告对账/收益);单文件留根:审计日志、数据大盘、跑马灯种子、统计概览 |
|
||||
| [api/internal/](api/internal/) | 内部接口 | `/internal` | 服务间调用(pricebot→app-server):价格事实回写、店铺映射、启动确认样本、App 版本写入;X-Internal-Secret 鉴权 |
|
||||
## api/ 目录是怎么组织的(传送门式)
|
||||
|
||||
### 接口索引入口
|
||||
接口多了之后,单个大文件会臃肿、难维护,所以拆成 **一个索引 + 一个接口一个文件**:
|
||||
|
||||
完整接口列表(含路径、方法、鉴权方式)见 [api/README.md](api/README.md)。
|
||||
- **[api/README.md](./api/README.md) — 入口/传送门**
|
||||
一张总览表列出**全部接口**(方法、路径、鉴权),每行链接到该接口的独立文档;另含**通用约定**(错误码、时间格式等)和**复用数据结构**(`TokenPair` / `UserOut` / `CouponCard` 等)。它本身不展开每个接口的细节,只负责"指路"。
|
||||
- **api/<模块>-<接口>.md — 单接口文档**
|
||||
每个接口一个文件,只写自己的入参 / 出参 / 错误码 / 说明。命名按 `<模块>-<接口>`,如 `auth-jverify-login.md`、`coupon-step.md`、`meituan-feed.md`。
|
||||
|
||||
### 文档命名规则
|
||||
**查某个接口的协议**:先打开 [api/README.md](./api/README.md) 的总览表 → 找到接口 → 点链接进对应文档。
|
||||
|
||||
单端点文档按 URL 路径命名:`{prefix}-{resource}.md`(如 `wallet-withdraw.md`、`auth-sms-send.md`)。
|
||||
含子资源的合并文档按族命名(如 `device-liveness.md` 覆盖 register/heartbeat/liveness/liveness-ack 四个端点)。
|
||||
文件名唯一确定文档位置:大类目录 + 文件名前缀 → 直接匹配。
|
||||
|
||||
---
|
||||
|
||||
## 数据库文档 (`database/`)
|
||||
|
||||
数据库表结构说明,每表一个文件,[database/OVERVIEW.md](database/OVERVIEW.md) 为索引入口。
|
||||
|
||||
---
|
||||
|
||||
## 第三方集成 (`integrations/`)
|
||||
|
||||
外部 SDK/API 集成架构与实现说明:[integrations/README.md](integrations/README.md)。
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [jiguang.md](integrations/jiguang.md) | 极光一键登录(REST 验证 + RSA 解密) |
|
||||
| [pangle.md](integrations/pangle.md) | 穿山甲广告 S2S 回调 |
|
||||
| [wxpay.md](integrations/wxpay.md) | 微信支付 V3(提现/授权) |
|
||||
| [meituan.md](integrations/meituan.md) | 美团 CPS 网关 |
|
||||
|
||||
---
|
||||
|
||||
## 开发指南 (`guides/`)
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| [邀请功能-实现原理与本地测试.md](guides/邀请功能-实现原理与本地测试.md) | 邀请系统实现细节 |
|
||||
| [CPS发券分发与微信授权.md](guides/CPS发券分发与微信授权.md) | CPS 发券 + 微信网页授权流程 |
|
||||
| [看广告赚金币上线清单.md](guides/看广告赚金币上线清单.md) | 广告功能上线检查清单 |
|
||||
| [待办与技术债.md](guides/待办与技术债.md) | 待办事项与技术债务 |
|
||||
|
||||
---
|
||||
|
||||
## 架构与设计 (`superpowers/`)
|
||||
|
||||
需求规格与设计文档,按 `YYYY-MM-DD-<topic>-design.md` 命名。
|
||||
|
||||
---
|
||||
|
||||
## 后端技术实现 (`后端技术实现.md`)
|
||||
|
||||
后端整体技术架构说明。
|
||||
> **新增接口时**:在 `api/` 下加一个 `<模块>-<接口>.md`,并到 `api/README.md` 总览表里补一行链接。这样索引始终是唯一的"传送门",细节各自独立、互不干扰。
|
||||
|
||||
+121
-129
@@ -3,7 +3,7 @@
|
||||
> Base URL:生产 `https://app-api.shaguabijia.com`;本地联调 `http://<开发机>:8770`
|
||||
> 协议:HTTP / JSON,请求与响应体均 `application/json`,字段统一 **snake_case**
|
||||
> 鉴权:需鉴权的接口在请求头带 `Authorization: Bearer <access_token>`
|
||||
> 最后更新:2026-07-03(补全缺失文档:ad/watch-report, wallet/transfer-auth 族, coupon/session+stats+completed-today+prompt 族, invite 族, user/onboarding, platform/flags+ad-config+app-version, intent/step+precoupon/step, analytics/events, order/report, report 族, feedback/config+records, trace/finalize。文档移至分类子目录,新增 mock 入参/出参示例)
|
||||
> 最后更新:2026-06-23(补全此前缺整族的端点:device 无障碍存活监控、internal 内部回写、CPS 短链落地、report/invite/wxpay,及 platform/coupon/feedback/user 的零散读端点;device/internal/cps-redirect 三族新建单文件文档。上一次 2026-06-18 补录比价透传端点与 `meituan/top-sales`)
|
||||
> 架构:`app/api/v1/` 只放很轻的接口层;穿山甲/微信支付/极光/短信/美团等 SDK 集成的重逻辑在 `app/integrations/`,实现细节见 [docs/integrations/](../integrations/README.md)。
|
||||
|
||||
---
|
||||
@@ -12,154 +12,146 @@
|
||||
|
||||
| # | 方法 + 路径 | 鉴权 | 详情 |
|
||||
|---|---|---|---|
|
||||
| 1 | `GET /health` | 无 | [详情](./other/health.md) |
|
||||
| 2 | `POST /api/v1/auth/jverify-login` | 无 | [详情](./auth/auth-jverify-login.md) |
|
||||
| 3 | `POST /api/v1/auth/sms/send` | 无 | [详情](./auth/auth-sms-send.md) |
|
||||
| 4 | `POST /api/v1/auth/sms/login` | 无 | [详情](./auth/auth-sms-login.md) |
|
||||
| 5 | `POST /api/v1/auth/refresh` | 无 | [详情](./auth/auth-refresh.md) |
|
||||
| 6 | `GET /api/v1/auth/me` | Bearer | [详情](./auth/auth-me.md) |
|
||||
| 7 | `POST /api/v1/auth/logout` | Bearer | [详情](./auth/auth-logout.md) |
|
||||
| 8 | `POST /api/v1/coupon/step` | 无 | [详情](./coupon/coupon-step.md)(透传 pricebot + best-effort 写 `coupon_*` 三表) |
|
||||
| 8a | `POST /api/v1/coupon/prompt/shown` | 无 | [详情](./coupon/coupon-prompt.md)(引导窗弹出即上报) |
|
||||
| 8b | `POST /api/v1/coupon/prompt/dismiss` | 无 | [详情](./coupon/coupon-prompt.md)(用户拒绝/关闭引导窗) |
|
||||
| 8c | `GET /api/v1/coupon/prompt/should-show` | 无 | [详情](./coupon/coupon-prompt.md)(切到外卖 App 时是否还应弹引导窗) |
|
||||
| 8d | `POST /api/v1/coupon/prompt/reset` | 无 | [详情](./coupon/coupon-prompt.md)(重置今日引导窗 engagement,开发测频控用) |
|
||||
| 8e | `GET /api/v1/coupon/completed-today` | 无 | [详情](./coupon/coupon-completed-today.md)(这台设备今天是否已跑完整轮领券) |
|
||||
| 8f | `POST /api/v1/coupon/completed-today/reset` | 无 | [详情](./coupon/coupon-completed-today.md)(重置今日已完成,开发用) |
|
||||
| 8g | `GET /api/v1/coupon/stats` | Bearer | [详情](./coupon/coupon-stats.md)(累计领券数,「我的」页战绩卡) |
|
||||
| 8h | `POST /api/v1/coupon/session` | 无 | [详情](./coupon/coupon/coupon-session.md)(领券流水上报,admin 看板数据源) |
|
||||
| 9 | `POST /api/v1/meituan/coupons` | 无 | [详情](./meituan/meituan-coupons.md) |
|
||||
| 10 | `POST /api/v1/meituan/feed` | 无 | [详情](./meituan/meituan-feed.md) |
|
||||
| 11 | `POST /api/v1/meituan/referral-link` | 无 | [详情](./meituan/meituan-referral-link.md) |
|
||||
| 11a | `POST /api/v1/meituan/top-sales` | 无 | [详情](./meituan/meituan-top-sales.md)(销量榜:离线库 `meituan_coupon` 按销量降序 + 跨源去重,不实时打美团) |
|
||||
| 1 | `GET /health` | 无 | [详情](./health.md) |
|
||||
| 2 | `POST /api/v1/auth/jverify-login` | 无 | [详情](./auth-jverify-login.md) |
|
||||
| 3 | `POST /api/v1/auth/sms/send` | 无 | [详情](./auth-sms-send.md) |
|
||||
| 4 | `POST /api/v1/auth/sms/login` | 无 | [详情](./auth-sms-login.md) |
|
||||
| 5 | `POST /api/v1/auth/refresh` | 无 | [详情](./auth-refresh.md) |
|
||||
| 6 | `GET /api/v1/auth/me` | Bearer | [详情](./auth-me.md) |
|
||||
| 7 | `POST /api/v1/auth/logout` | Bearer | [详情](./auth-logout.md) |
|
||||
| 8 | `POST /api/v1/coupon/step` | 无 | [详情](./coupon-step.md)(透传 pricebot + best-effort 写 `coupon_*` 三表) |
|
||||
| 8a | `POST /api/v1/coupon/prompt/shown` | 无 | 引导窗弹出即上报(按 device+package+日记 `shown`,今天这个 App 不再自动弹)(无单独文档) |
|
||||
| 8b | `POST /api/v1/coupon/prompt/dismiss` | 无 | 用户拒绝/关闭引导窗(透传链路看不到拒绝,客户端通知)(无单独文档) |
|
||||
| 8c | `GET /api/v1/coupon/prompt/should-show` | 无 | 切到外卖 App 时是否还应弹引导窗(`device_id`+`package`)(无单独文档) |
|
||||
| 8d | `POST /api/v1/coupon/prompt/reset` | 无 | 重置今日引导窗 engagement(开发测频控用)(无单独文档) |
|
||||
| 8e | `GET /api/v1/coupon/completed-today` | 无 | 这台设备今天是否已跑完整轮领券(首页「去领取」卡置灰源)(无单独文档) |
|
||||
| 8f | `POST /api/v1/coupon/completed-today/reset` | 无 | 重置今日已完成(开发用)(无单独文档) |
|
||||
| 8g | `GET /api/v1/coupon/stats` | Bearer | 累计领券数(「我的」页战绩卡;按 user_id 聚合,**鉴权**)(无单独文档) |
|
||||
| 9 | `POST /api/v1/meituan/coupons` | 无 | [详情](./meituan-coupons.md) |
|
||||
| 10 | `POST /api/v1/meituan/feed` | 无 | [详情](./meituan-feed.md) |
|
||||
| 11 | `POST /api/v1/meituan/referral-link` | 无 | [详情](./meituan-referral-link.md) |
|
||||
| 11a | `POST /api/v1/meituan/top-sales` | 无 | [详情](./meituan-top-sales.md)(销量榜:离线库 `meituan_coupon` 按销量降序 + 跨源去重,不实时打美团) |
|
||||
| **比价透传**(前缀 `/api/v1`,外卖 MVP;与 `coupon/step` 同为透传 pricebot-backend;下按 Phase 流程列,均不鉴权) |||
|
||||
| 12 | `POST /api/v1/intent/recognize` | 无 | [详情](./intent/compare-intent-recognize.md)(Phase 1 意图识别,单次,多数源) |
|
||||
| 12a | `POST /api/v1/intent/precoupon/step` | 无 | [详情](./intent/intent-step.md)(Phase 0 意图识别前先用券,仅美团源) |
|
||||
| 12b | `POST /api/v1/intent/step` | 无 | [详情](./intent/intent-step.md)(Phase 1 多帧意图识别,仅淘宝源,循环到 done) |
|
||||
| 13 | `POST /api/v1/price/step` | 无 | [详情](./intent/compare-price-step.md)(Phase 2 步进) |
|
||||
| 13a | `POST /api/v1/trace/finalize` | 无 | [详情](./other/trace-finalize.md)(比价 trace 收尾上云,终止/未识别拿 trace_url) |
|
||||
| 12 | `POST /api/v1/intent/recognize` | 无 | [详情](./compare-intent-recognize.md)(Phase 1 意图识别,单次,多数源) |
|
||||
| 12a | `POST /api/v1/intent/precoupon/step` | 无 | Phase 0 意图识别前先用券,仅美团源(透传,无单独文档) |
|
||||
| 12b | `POST /api/v1/intent/step` | 无 | Phase 1 多帧意图识别,仅淘宝源,循环到 done(透传,无单独文档) |
|
||||
| 13 | `POST /api/v1/price/step` | 无 | [详情](./compare-price-step.md)(Phase 2 步进) |
|
||||
| 13a | `POST /api/v1/trace/finalize` | 无 | 比价 trace 收尾上云,终止/未识别拿 trace_url(透传,无单独文档) |
|
||||
| **比价记录**(前缀 `/api/v1/compare`;按用户落库,**鉴权**,区别于上面不鉴权的透传) |||
|
||||
| 12a | `POST /api/v1/compare/record` | Bearer | [详情](./compare/compare-record-report.md) |
|
||||
| 12b | `GET /api/v1/compare/records` | Bearer | [详情](./compare/compare-records.md) |
|
||||
| 12c | `GET /api/v1/compare/records/{id}` | Bearer | [详情](./compare/compare-record-detail.md) |
|
||||
| 12e | `GET /api/v1/compare/stats` | Bearer | [详情](./compare/compare-stats.md)(「我的」页省钱战绩卡:完成比价数 + 累计发现可省) |
|
||||
| 12a | `POST /api/v1/compare/record` | Bearer | [详情](./compare-record-report.md) |
|
||||
| 12b | `GET /api/v1/compare/records` | Bearer | [详情](./compare-records.md) |
|
||||
| 12c | `GET /api/v1/compare/records/{id}` | Bearer | [详情](./compare-record-detail.md) |
|
||||
| 12e | `GET /api/v1/compare/stats` | Bearer | [详情](./compare-stats.md)(「我的」页省钱战绩卡:完成比价数 + 累计发现可省) |
|
||||
| **比价战绩里程碑**(前缀 `/api/v1/compare`;福利页「记录比价战绩」,按成功比价数解锁逐档发金币) |||
|
||||
| 12d | `GET /api/v1/compare/milestones` | Bearer | [详情](./compare/compare-milestones.md) |
|
||||
| 12e | `POST /api/v1/compare/milestones/{milestone}/claim` | Bearer | [详情](./compare/compare-milestone-claim.md) |
|
||||
| 12d | `GET /api/v1/compare/milestones` | Bearer | [详情](./compare-milestones.md) |
|
||||
| 12e | `POST /api/v1/compare/milestones/{milestone}/claim` | Bearer | [详情](./compare-milestone-claim.md) |
|
||||
| **设备 / 无障碍存活监控**(前缀 `/api/v1/device`;心跳超时检出 + 掉线召回,#65) |||
|
||||
| D1 | `POST /api/v1/device/register` | Bearer | [详情](./device/device-liveness.md)(注册设备/更新极光 push token) |
|
||||
| D2 | `POST /api/v1/device/heartbeat` | Bearer | [详情](./device/device-liveness.md)(无障碍服务存活心跳,心跳也能自注册) |
|
||||
| D3 | `GET /api/v1/device/liveness` | Bearer | [详情](./device/device-liveness.md)(进 App 查本机是否被判掉线过) |
|
||||
| D4 | `POST /api/v1/device/liveness/ack` | Bearer | [详情](./device/device-liveness.md)(确认已弹引导,清掉线告警) |
|
||||
| D1 | `POST /api/v1/device/register` | Bearer | [详情](./device-liveness.md)(注册设备/更新极光 push token) |
|
||||
| D2 | `POST /api/v1/device/heartbeat` | Bearer | [详情](./device-liveness.md)(无障碍服务存活心跳,心跳也能自注册) |
|
||||
| D3 | `GET /api/v1/device/liveness` | Bearer | [详情](./device-liveness.md)(进 App 查本机是否被判掉线过) |
|
||||
| D4 | `POST /api/v1/device/liveness/ack` | Bearer | [详情](./device-liveness.md)(确认已弹引导,清掉线告警) |
|
||||
| **上报更低价**(前缀 `/api/v1/report`;众包纠偏,人工审核发奖) |||
|
||||
| R1 | `POST /api/v1/report` | Bearer | [详情](./other/report-submit.md)(提交上报更低价,multipart:比价记录ID+平台+价格+截图1-4张) |
|
||||
| R2 | `GET /api/v1/report/records` | Bearer | [详情](./other/report-records.md)(上报记录列表,?status=pending/approved/rejected 可选筛选) |
|
||||
| R1 | `POST /api/v1/report` | Bearer | 提交上报(multipart:`comparison_record_id`/`reported_platform_id`/`reported_price`(元) + 1~4 张截图;原最低价反查 `comparison_record.best_*` 校验须更低)(无单独文档) |
|
||||
| R2 | `GET /api/v1/report/records` | Bearer | 上报记录列表(`?status=` pending/approved/rejected 可选筛选)(无单独文档) |
|
||||
| **好友邀请**(前缀 `/api/v1/invite`;注册即生效,双方各发 1 万金币) |||
|
||||
| I1 | `GET /api/v1/invite/me` | Bearer | [详情](./invite/invite-me.md)(我的邀请码+分享链接+已邀人数/已得金币) |
|
||||
| I2 | `GET /api/v1/invite/invitees` | Bearer | [详情](./invite/invite-invitees.md)(我邀请的人列表,limit/offset 分页) |
|
||||
| I3 | `POST /api/v1/invite/landing-track` | 无 | [详情](./invite/invite-bind.md)(落地页 dl.html 访问上报指纹,剪贴板归因兜底;浏览器无 token) |
|
||||
| I4 | `POST /api/v1/invite/bind` | Bearer | [详情](./invite/invite-bind.md)(绑定邀请人;支持 clipboard/manual 邀请码+fingerprint 指纹反查三种归因) |
|
||||
| I1 | `GET /api/v1/invite/me` | Bearer | 我的邀请码 + 分享链接 + 已邀人数/已得金币(无单独文档) |
|
||||
| I2 | `GET /api/v1/invite/invitees` | Bearer | 我邀请的人列表(`limit`/`offset` 分页)(无单独文档) |
|
||||
| I3 | `POST /api/v1/invite/landing-track` | 无 | 落地页 `dl.html` 访问上报指纹(剪贴板归因兜底;浏览器无 token)(无单独文档) |
|
||||
| I4 | `POST /api/v1/invite/bind` | Bearer | 绑定邀请人;支持 clipboard/manual 邀请码 + fingerprint 指纹反查三种归因(无单独文档) |
|
||||
| **钱包 / 我的资产**(前缀 `/api/v1/wallet`) |||
|
||||
| 14 | `GET /api/v1/wallet/account` | Bearer | [详情](./wallet/wallet-account.md) |
|
||||
| 15 | `GET /api/v1/wallet/coin-transactions` | Bearer | [详情](./wallet/wallet-coin-transactions.md) |
|
||||
| 16 | `GET /api/v1/wallet/cash-transactions` | Bearer | [详情](./wallet/wallet-cash-transactions.md) |
|
||||
| 17 | `GET /api/v1/wallet/exchange-info` | 无 | [详情](./wallet/wallet-exchange-info.md) |
|
||||
| 18 | `POST /api/v1/wallet/exchange` | Bearer | [详情](./wallet/wallet-exchange.md) |
|
||||
| 19 | `POST /api/v1/wallet/bind-wechat` | Bearer | [详情](./wallet/wallet-bind-wechat.md) |
|
||||
| 20 | `POST /api/v1/wallet/unbind-wechat` | Bearer | [详情](./wallet/wallet-unbind-wechat.md) |
|
||||
| 21 | `GET /api/v1/wallet/withdraw-info` | Bearer | [详情](./wallet/wallet-withdraw-info.md) |
|
||||
| 22 | `POST /api/v1/wallet/withdraw` | Bearer | [详情](./wallet/wallet-withdraw.md) |
|
||||
| 23 | `GET /api/v1/wallet/withdraw/status` | Bearer | [详情](./wallet/wallet-withdraw-status.md) |
|
||||
| 24 | `GET /api/v1/wallet/withdraw-orders` | Bearer | [详情](./wallet/wallet-withdraw-orders.md) |
|
||||
| 24a | `POST /api/v1/wallet/transfer-auth` | Bearer | [详情](./wallet/wallet-transfer-auth.md)(开启免确认到账,申请授权,返回拉起微信授权页的 package) |
|
||||
| 24b | `GET /api/v1/wallet/transfer-auth/status` | Bearer | [详情](./wallet/wallet-transfer-auth.md)(查免确认授权状态,从微信授权页返回后轮询) |
|
||||
| 24c | `POST /api/v1/wallet/transfer-auth/close` | Bearer | [详情](./wallet/wallet-transfer-auth.md)(关闭免确认到账,解除授权) |
|
||||
| 14 | `GET /api/v1/wallet/account` | Bearer | [详情](./wallet-account.md) |
|
||||
| 15 | `GET /api/v1/wallet/coin-transactions` | Bearer | [详情](./wallet-coin-transactions.md) |
|
||||
| 16 | `GET /api/v1/wallet/cash-transactions` | Bearer | [详情](./wallet-cash-transactions.md) |
|
||||
| 17 | `GET /api/v1/wallet/exchange-info` | 无 | [详情](./wallet-exchange-info.md) |
|
||||
| 18 | `POST /api/v1/wallet/exchange` | Bearer | [详情](./wallet-exchange.md) |
|
||||
| 19 | `POST /api/v1/wallet/bind-wechat` | Bearer | [详情](./wallet-bind-wechat.md) |
|
||||
| 20 | `POST /api/v1/wallet/unbind-wechat` | Bearer | [详情](./wallet-unbind-wechat.md) |
|
||||
| 21 | `GET /api/v1/wallet/withdraw-info` | Bearer | [详情](./wallet-withdraw-info.md) |
|
||||
| 22 | `POST /api/v1/wallet/withdraw` | Bearer | [详情](./wallet-withdraw.md) |
|
||||
| 23 | `GET /api/v1/wallet/withdraw/status` | Bearer | [详情](./wallet-withdraw-status.md) |
|
||||
| 24 | `GET /api/v1/wallet/withdraw-orders` | Bearer | [详情](./wallet-withdraw-orders.md) |
|
||||
| **签到**(前缀 `/api/v1/signin`) |||
|
||||
| 25 | `GET /api/v1/signin/status` | Bearer | [详情](./signin/signin-status.md) |
|
||||
| 26 | `POST /api/v1/signin` | Bearer | [详情](./signin/signin-do.md) |
|
||||
| 26a | `POST /api/v1/signin/boost` | Bearer | [详情](./signin/signin-boost.md) |
|
||||
| 25 | `GET /api/v1/signin/status` | Bearer | [详情](./signin-status.md) |
|
||||
| 26 | `POST /api/v1/signin` | Bearer | [详情](./signin-do.md) |
|
||||
| 26a | `POST /api/v1/signin/boost` | Bearer | [详情](./signin-boost.md) |
|
||||
| **任务**(前缀 `/api/v1/tasks`) |||
|
||||
| 27 | `GET /api/v1/tasks` | Bearer | [详情](./tasks/tasks-list.md) |
|
||||
| 28 | `POST /api/v1/tasks/{task_key}/claim` | Bearer | [详情](./tasks/tasks-claim.md) |
|
||||
| 27 | `GET /api/v1/tasks` | Bearer | [详情](./tasks-list.md) |
|
||||
| 28 | `POST /api/v1/tasks/{task_key}/claim` | Bearer | [详情](./tasks-claim.md) |
|
||||
| **省钱**(前缀 `/api/v1/savings`) |||
|
||||
| 29 | `GET /api/v1/savings/summary` | Bearer | [详情](./savings/savings-summary.md) |
|
||||
| 30 | `GET /api/v1/savings/battle` | Bearer | [详情](./savings/savings-battle.md) |
|
||||
| 31 | `GET /api/v1/savings/records` | Bearer | [详情](./savings/savings-records.md) |
|
||||
| 29 | `GET /api/v1/savings/summary` | Bearer | [详情](./savings-summary.md) |
|
||||
| 30 | `GET /api/v1/savings/battle` | Bearer | [详情](./savings-battle.md) |
|
||||
| 31 | `GET /api/v1/savings/records` | Bearer | [详情](./savings-records.md) |
|
||||
| **看广告发奖**(前缀 `/api/v1/ad`) |||
|
||||
| 32 | `GET /api/v1/ad/pangle-callback` | 验签 | [详情](./ad/ad-pangle-callback.md) |
|
||||
| 33 | `GET /api/v1/ad/reward-status` | Bearer | [详情](./ad/ad-reward-status.md) |
|
||||
| 34 | `POST /api/v1/ad/test-grant` | Bearer | [详情](./ad/ad-test-grant.md) |
|
||||
| 35 | `POST /api/v1/ad/ecpm-report` | Bearer | [详情](./ad/ad-ecpm-report.md) |
|
||||
| 35a | `POST /api/v1/ad/feed-reward` | Bearer | [详情](./ad/ad-feed-reward.md) |
|
||||
| 35b | `POST /api/v1/ad/reward-noshow` | Bearer | [详情](./ad/ad-reward-noshow.md)(激励视频提前关闭/未发奖留痕,只记原因不发币) |
|
||||
| 35c | `GET /api/v1/ad/feed-reward/units` | Bearer | [详情](./ad/ad-feed-reward.md)(信息流广告今日已发份数/上限,配合 feed-reward 看进度) |
|
||||
| 35d | `POST /api/v1/ad/watch-report` | Bearer | [详情](./ad/ad-watch-report.md)(上报激励视频观看时长,旧客户端兼容) |
|
||||
| 32 | `GET /api/v1/ad/pangle-callback` | 验签 | [详情](./ad-pangle-callback.md) |
|
||||
| 33 | `GET /api/v1/ad/reward-status` | Bearer | [详情](./ad-reward-status.md) |
|
||||
| 34 | `POST /api/v1/ad/test-grant` | Bearer | [详情](./ad-test-grant.md) |
|
||||
| 35 | `POST /api/v1/ad/ecpm-report` | Bearer | [详情](./ad-ecpm-report.md) |
|
||||
| 35a | `POST /api/v1/ad/feed-reward` | Bearer | [详情](./ad-feed-reward.md) |
|
||||
| 35b | `POST /api/v1/ad/reward-noshow` | Bearer | [详情](./ad-reward-noshow.md)(激励视频提前关闭/未发奖留痕,只记原因不发币) |
|
||||
| 35c | `GET /api/v1/ad/feed-reward/units` | Bearer | 信息流广告今日已发份数/上限(配合 `feed-reward` 看进度)(无单独文档) |
|
||||
| **用户资料**(前缀 `/api/v1/user`) |||
|
||||
| 35 | `PATCH /api/v1/user/profile` | Bearer | [详情](./user/user-profile.md) |
|
||||
| 36 | `POST /api/v1/user/avatar` | Bearer | [详情](./user/user-avatar.md) |
|
||||
| 36a | `POST /api/v1/user/onboarding/complete` | Bearer | [详情](./user/user-onboarding.md)(标记新手引导完成,按 账号+device_id 幂等,跨卸载重装持久) |
|
||||
| 36b | `GET /api/v1/user/onboarding/status` | Bearer | [详情](./user/user-onboarding.md)(查该 账号+设备 是否走过引导,运营在 admin 删记录即触发重走) |
|
||||
| 37 | `DELETE /api/v1/user` | Bearer | [详情](./user/user-delete.md) |
|
||||
| 35 | `PATCH /api/v1/user/profile` | Bearer | [详情](./user-profile.md) |
|
||||
| 36 | `POST /api/v1/user/avatar` | Bearer | [详情](./user-avatar.md) |
|
||||
| 36a | `POST /api/v1/user/onboarding/complete` | Bearer | 标记新手引导完成(按 账号+device_id 幂等,跨卸载重装持久)(无单独文档) |
|
||||
| 36b | `GET /api/v1/user/onboarding/status` | Bearer | 查该 (账号,设备) 是否走过引导(运营在 admin 删记录即触发重走)(无单独文档) |
|
||||
| 37 | `DELETE /api/v1/user` | Bearer | [详情](./user-delete.md) |
|
||||
| **帮助与反馈**(前缀 `/api/v1/feedback`) |||
|
||||
| 38 | `POST /api/v1/feedback` | Bearer | [详情](./other/feedback.md) |
|
||||
| 38a | `GET /api/v1/feedback/config` | Bearer | [详情](./other/feedback-config.md)(反馈页「加群二维码」卡配置:开关+二维码图+三行文案) |
|
||||
| 38b | `GET /api/v1/feedback/records` | Bearer | [详情](./other/feedback-records.md)(我的反馈历史,pending/adopted/rejected) |
|
||||
| **埋点 & 订单上报**(前缀分散;全部 Bearer 除 analytics/events 不强制登录) |||
|
||||
| E1 | `POST /api/v1/analytics/events` | 无 | [详情](./other/analytics-events.md)(批量上报埋点事件,不强制登录,每批最多200条) |
|
||||
| E2 | `POST /api/v1/order/report` | Bearer | [详情](./other/order-report.md)(上报归因订单,比价后5分钟内点链接+支付金额与比价价相差≤1元) |
|
||||
| 38 | `POST /api/v1/feedback` | Bearer | [详情](./feedback.md) |
|
||||
| 38a | `GET /api/v1/feedback/config` | Bearer | 反馈页「加群二维码」卡配置(开关 + 二维码图 + 三行文案)(无单独文档) |
|
||||
| 38b | `GET /api/v1/feedback/records` | Bearer | 我的反馈历史(pending/adopted/rejected)(无单独文档) |
|
||||
| **首页门面数据 / 客户端配置**(前缀 `/api/v1/platform`;全平台展示数字 + 运营开关,**全部不鉴权**,登录前可读) |||
|
||||
| 39 | `GET /api/v1/platform/stats` | 无 | [详情](./platform/platform-stats.md) |
|
||||
| 40 | `GET /api/v1/platform/savings-feed` | 无 | [详情](./savings/platform-savings-feed.md) |
|
||||
| 40a | `GET /api/v1/platform/flags` | 无 | [详情](./platform/platform-flags.md)(客户端运营 feature flag,比价/领券期广告开关等,拉取后缓存) |
|
||||
| 40b | `GET /api/v1/platform/ad-config` | 无 | [详情](./platform/platform-ad-config.md)(客户端拉广告配置:穿山甲 app_id+各位ID+各场景开关;不含验签密钥) |
|
||||
| 40c | `GET /api/v1/platform/app-version` | 无 | [详情](./platform/platform-app-version.md)(最新 App 版本,OTA 检查更新;与本机 versionCode 比) |
|
||||
| 39 | `GET /api/v1/platform/stats` | 无 | [详情](./platform-stats.md) |
|
||||
| 40 | `GET /api/v1/platform/savings-feed` | 无 | [详情](./platform-savings-feed.md) |
|
||||
| 40a | `GET /api/v1/platform/flags` | 无 | 客户端运营 feature flag(比价/领券期广告开关等),拉取后缓存(无单独文档) |
|
||||
| 40b | `GET /api/v1/platform/ad-config` | 无 | 客户端拉广告配置(穿山甲 app_id + 各位 ID + 各场景开关;不含验签密钥)(无单独文档) |
|
||||
| 40c | `GET /api/v1/platform/app-version` | 无 | 最新 App 版本(OTA 检查更新;与本机 versionCode 比)(无单独文档) |
|
||||
| **微信支付回调**(前缀 `/api/v1/wxpay`) |||
|
||||
| W1 | `POST /api/v1/wxpay/transfer-auth-notify` | 无 | 免确认收款授权结果通知(一期 stub:仅应答 200 不验签不改账,授权状态靠主动查询兜底)(无单独文档) |
|
||||
| **CPS 群发短链落地**(**无前缀**,挂域名根;公网不鉴权) |||
|
||||
| C1 | `GET /c/{code}` | 无 | [详情](./other/cps-redirect.md)(短链落地:微信授权拿 openid + 记点击 + 302 跳/淘宝 H5 落地页) |
|
||||
| C2 | `POST /c/{code}/copy` | 无 | [详情](./other/cps-redirect.md)(淘宝落地页点「复制口令」记 `copy`) |
|
||||
| C3 | `GET /wx/oauth/cb` | 无 | [详情](./other/cps-redirect.md)(微信网页授权回调;upsert `cps_wx_user` + 种 cookie,`include_in_schema=False`) |
|
||||
| C4 | `GET /MP_verify_*.txt` | 无 | [详情](./other/cps-redirect.md)(微信「网页授权域名」归属校验文件,`include_in_schema=False`) |
|
||||
| C1 | `GET /c/{code}` | 无 | [详情](./cps-redirect.md)(短链落地:微信授权拿 openid + 记点击 + 302 跳/淘宝 H5 落地页) |
|
||||
| C2 | `POST /c/{code}/copy` | 无 | [详情](./cps-redirect.md)(淘宝落地页点「复制口令」记 `copy`) |
|
||||
| C3 | `GET /wx/oauth/cb` | 无 | [详情](./cps-redirect.md)(微信网页授权回调;upsert `cps_wx_user` + 种 cookie,`include_in_schema=False`) |
|
||||
| C4 | `GET /MP_verify_*.txt` | 无 | [详情](./cps-redirect.md)(微信「网页授权域名」归属校验文件,`include_in_schema=False`) |
|
||||
| **内部回写端点**(前缀 `/internal`;pricebot/发布流程→app-server,**`X-Internal-Secret` 头**,非客户端接口) |||
|
||||
| N1 | `POST /internal/price-observation` | 内部密钥 | [详情](./internal/internal.md)(比价价格事实批量落 `price_observation`) |
|
||||
| N2 | `GET /internal/store-mapping/lookup` | 内部密钥 | [详情](./internal/internal.md)(按源平台店名反查目标平台已沉淀店铺 id/deeplink) |
|
||||
| N3 | `POST /internal/store-mapping` | 内部密钥 | [详情](./internal/internal.md)(跨平台店铺身份映射落 `store_mapping`) |
|
||||
| N4 | `POST /internal/store-mapping/invalidate` | 内部密钥 | [详情](./internal/internal.md)(标记某平台 shopId 缓存 deeplink 失效) |
|
||||
| N5 | `POST /internal/launch-confirm-sample` | 内部密钥 | [详情](./internal/internal.md)(启动确认窗兜底样本落 `launch_confirm_sample`) |
|
||||
| N6 | `POST /internal/app-version` | 内部密钥 | [详情](./internal/internal.md)(发布流程写最新 App 版本,落 `app_config`) |
|
||||
| N1 | `POST /internal/price-observation` | 内部密钥 | [详情](./internal.md)(比价价格事实批量落 `price_observation`) |
|
||||
| N2 | `GET /internal/store-mapping/lookup` | 内部密钥 | [详情](./internal.md)(按源平台店名反查目标平台已沉淀店铺 id/deeplink) |
|
||||
| N3 | `POST /internal/store-mapping` | 内部密钥 | [详情](./internal.md)(跨平台店铺身份映射落 `store_mapping`) |
|
||||
| N4 | `POST /internal/store-mapping/invalidate` | 内部密钥 | [详情](./internal.md)(标记某平台 shopId 缓存 deeplink 失效) |
|
||||
| N5 | `POST /internal/launch-confirm-sample` | 内部密钥 | [详情](./internal.md)(启动确认窗兜底样本落 `launch_confirm_sample`) |
|
||||
| N6 | `POST /internal/app-version` | 内部密钥 | [详情](./internal.md)(发布流程写最新 App 版本,落 `app_config`) |
|
||||
| **静态资源**(StaticFiles 挂载,见下方 `/media` 静态服务) |||
|
||||
| - | `GET /media/avatars/<file>` | 无 | 用户头像;返回二进制图片 |
|
||||
| - | `GET /media/feedback/<file>` | 无 | 反馈截图;返回二进制图片 |
|
||||
| **运营后台 Admin**(独立子应用 `app/admin/`,前缀 `/admin/api`,独立进程 + 独立 admin JWT。鉴权列:`admin`=任意已登录管理员,`operator`/`finance`/`super_admin`=需对应角色(`super_admin` 恒通过)) |||
|
||||
| A1 | `POST /admin/api/auth/login` | 无 | [详情](./admin/auth/admin-auth-login.md) |
|
||||
| A2 | `GET /admin/api/auth/me` | admin | [详情](./admin/auth/admin-auth-me.md) |
|
||||
| A3 | `GET /admin/api/stats/overview` | admin | [详情](./admin/admin-stats-overview.md) |
|
||||
| A4 | `GET /admin/api/users` | admin | [详情](./admin/users/admin-users-list.md) |
|
||||
| A5 | `GET /admin/api/users/{user_id}` | admin | [详情](./admin/users/admin-user-detail.md) |
|
||||
| A6 | `POST /admin/api/users/{user_id}/status` | operator | [详情](./admin/users/admin-user-status.md) |
|
||||
| A7 | `POST /admin/api/users/{user_id}/coins` | finance | [详情](./admin/users/admin-user-coins.md) |
|
||||
| A8 | `POST /admin/api/users/{user_id}/cash` | finance | [详情](./admin/users/admin-user-cash.md) |
|
||||
| A9 | `GET /admin/api/wallet/coin-transactions` | admin | [详情](./admin/wallet/admin-wallet-coin-transactions.md) |
|
||||
| A10 | `GET /admin/api/wallet/cash-transactions` | admin | [详情](./admin/wallet/admin-wallet-cash-transactions.md) |
|
||||
| A11 | `GET /admin/api/withdraws` | admin | [详情](./admin/withdraws/admin-withdraws-list.md) |
|
||||
| A12 | `POST /admin/api/withdraws/reconcile` | finance | [详情](./admin/withdraws/admin-withdraw-reconcile.md) |
|
||||
| A13 | `POST /admin/api/withdraws/{out_bill_no}/refresh` | finance | [详情](./admin/withdraws/admin-withdraw-refresh.md) |
|
||||
| A14 | `GET /admin/api/feedbacks` | admin | [详情](./admin/feedbacks/admin-feedbacks-list.md) |
|
||||
| A15 | `POST /admin/api/feedbacks/{feedback_id}/handle` | operator | [详情](./admin/feedbacks/admin-feedback-handle.md) |
|
||||
| A16 | `GET /admin/api/admins` | super_admin | [详情](./admin/admins/admin-admins-list.md) |
|
||||
| A17 | `POST /admin/api/admins` | super_admin | [详情](./admin/admins/admin-admin-create.md) |
|
||||
| A18 | `PATCH /admin/api/admins/{admin_id}` | super_admin | [详情](./admin/admins/admin-admin-update.md) |
|
||||
| A19 | `GET /admin/api/audit-logs` | admin | [详情](./admin/admin-audit-logs.md) |
|
||||
| A20 | `GET /admin/api/dashboard-display` | admin | [详情](./admin/admin-dashboard-display.md) |
|
||||
| A21 | `PATCH /admin/api/dashboard-display/{metric}` | operator | [详情](./admin/admin-dashboard-display.md) |
|
||||
| A22 | `GET /admin/api/marquee-seeds` | admin | [详情](./admin/admin-marquee-seeds.md) |
|
||||
| A23 | `POST /admin/api/marquee-seeds` | operator | [详情](./admin/admin-marquee-seeds.md) |
|
||||
| A24 | `PATCH /admin/api/marquee-seeds/{seed_id}` | operator | [详情](./admin/admin-marquee-seeds.md) |
|
||||
| A25 | `DELETE /admin/api/marquee-seeds/{seed_id}` | operator | [详情](./admin/admin-marquee-seeds.md) |
|
||||
| A26 | `POST /admin/api/marquee-seeds/bulk` | operator | [详情](./admin/admin-marquee-seeds.md) |
|
||||
| A27 | `GET /admin/api/marquee-seeds/preview` | admin | [详情](./admin/admin-marquee-seeds.md) |
|
||||
| A28 | `GET /admin/api/ad-coin-audit` | admin | [详情](./admin/ad/admin-ad-coin-audit.md)(看广告金币公式复算对账,只读) |
|
||||
| A29 | `GET /admin/api/ad-revenue-report` | admin | [详情](./admin/ad/admin-ad-revenue-report.md)(广告收益报表:按用户/日期/类型/应用/代码位 聚合 条数/收益/金币,只读) |
|
||||
| A1 | `POST /admin/api/auth/login` | 无 | [详情](./admin-auth-login.md) |
|
||||
| A2 | `GET /admin/api/auth/me` | admin | [详情](./admin-auth-me.md) |
|
||||
| A3 | `GET /admin/api/stats/overview` | admin | [详情](./admin-stats-overview.md) |
|
||||
| A4 | `GET /admin/api/users` | admin | [详情](./admin-users-list.md) |
|
||||
| A5 | `GET /admin/api/users/{user_id}` | admin | [详情](./admin-user-detail.md) |
|
||||
| A6 | `POST /admin/api/users/{user_id}/status` | operator | [详情](./admin-user-status.md) |
|
||||
| A7 | `POST /admin/api/users/{user_id}/coins` | finance | [详情](./admin-user-coins.md) |
|
||||
| A8 | `POST /admin/api/users/{user_id}/cash` | finance | [详情](./admin-user-cash.md) |
|
||||
| A9 | `GET /admin/api/wallet/coin-transactions` | admin | [详情](./admin-wallet-coin-transactions.md) |
|
||||
| A10 | `GET /admin/api/wallet/cash-transactions` | admin | [详情](./admin-wallet-cash-transactions.md) |
|
||||
| A11 | `GET /admin/api/withdraws` | admin | [详情](./admin-withdraws-list.md) |
|
||||
| A12 | `POST /admin/api/withdraws/reconcile` | finance | [详情](./admin-withdraw-reconcile.md) |
|
||||
| A13 | `POST /admin/api/withdraws/{out_bill_no}/refresh` | finance | [详情](./admin-withdraw-refresh.md) |
|
||||
| A14 | `GET /admin/api/feedbacks` | admin | [详情](./admin-feedbacks-list.md) |
|
||||
| A15 | `POST /admin/api/feedbacks/{feedback_id}/handle` | operator | [详情](./admin-feedback-handle.md) |
|
||||
| A16 | `GET /admin/api/admins` | super_admin | [详情](./admin-admins-list.md) |
|
||||
| A17 | `POST /admin/api/admins` | super_admin | [详情](./admin-admin-create.md) |
|
||||
| A18 | `PATCH /admin/api/admins/{admin_id}` | super_admin | [详情](./admin-admin-update.md) |
|
||||
| A19 | `GET /admin/api/audit-logs` | admin | [详情](./admin-audit-logs.md) |
|
||||
| A20 | `GET /admin/api/dashboard-display` | admin | [详情](./admin-dashboard-display.md) |
|
||||
| A21 | `PATCH /admin/api/dashboard-display/{metric}` | operator | [详情](./admin-dashboard-display.md) |
|
||||
| A22 | `GET /admin/api/marquee-seeds` | admin | [详情](./admin-marquee-seeds.md) |
|
||||
| A23 | `POST /admin/api/marquee-seeds` | operator | [详情](./admin-marquee-seeds.md) |
|
||||
| A24 | `PATCH /admin/api/marquee-seeds/{seed_id}` | operator | [详情](./admin-marquee-seeds.md) |
|
||||
| A25 | `DELETE /admin/api/marquee-seeds/{seed_id}` | operator | [详情](./admin-marquee-seeds.md) |
|
||||
| A26 | `POST /admin/api/marquee-seeds/bulk` | operator | [详情](./admin-marquee-seeds.md) |
|
||||
| A27 | `GET /admin/api/marquee-seeds/preview` | admin | [详情](./admin-marquee-seeds.md) |
|
||||
| A28 | `GET /admin/api/ad-coin-audit` | admin | [详情](./admin-ad-coin-audit.md)(看广告金币公式复算对账,只读) |
|
||||
| A29 | `GET /admin/api/ad-revenue-report` | admin | [详情](./admin-ad-revenue-report.md)(广告收益报表:按用户/日期/类型/应用/代码位 聚合 条数/收益/金币,只读) |
|
||||
| - | `GET /admin/api/health` | 无 | admin 健康检查(无单独文档) |
|
||||
|
||||
> ⚠️ 美团三个接口当前**无鉴权**,且 `referral-link` 的 `sid` 允许客户端传值覆盖默认渠道——见各接口"备注"。
|
||||
@@ -209,8 +201,8 @@
|
||||
|---|---|---|
|
||||
| `id` | int | 用户主键 |
|
||||
| `phone` | string | 手机号(注销账号后变 `deleted_<id>` 占位释放唯一约束) |
|
||||
| `nickname` | string \| null | 昵称,经 [`PATCH /api/v1/user/profile`](./user/user-profile.md) 修改 |
|
||||
| `avatar_url` | string \| null | 头像相对 URL(`/media/avatars/...`),经 [`POST /api/v1/user/avatar`](./user/user-avatar.md) 上传 |
|
||||
| `nickname` | string \| null | 昵称,经 [`PATCH /api/v1/user/profile`](./user-profile.md) 修改 |
|
||||
| `avatar_url` | string \| null | 头像相对 URL(`/media/avatars/...`),经 [`POST /api/v1/user/avatar`](./user-avatar.md) 上传 |
|
||||
| `register_channel` | string | 注册渠道:`jverify` / `sms` |
|
||||
| `status` | string | `active` / `disabled` / `deleted` |
|
||||
| `created_at` | datetime | 注册时间 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/ad/ecpm-report — 上报本次广告展示的 eCPM(内部收益统计)
|
||||
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
请求体:`EcpmReportIn`
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /api/v1/ad/pangle-callback — 穿山甲 GroMore 激励视频发奖回调(S2S)
|
||||
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:**无 JWT,靠验签**(穿山甲 GroMore 服务器调用) | 限流:同 IP ≤300 次/分 | [← 返回 API 索引](../README.md)
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:**无 JWT,靠验签**(穿山甲 GroMore 服务器调用) | 限流:同 IP ≤300 次/分 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> ⚠️ 我们客户端用 `useMediation(true)`(GroMore 融合),回调走 **GroMore 广告位层级**(规范见 supportcenter/26240),**不是**联盟代码位层级(5416)。两者密钥、响应格式都不同,别混。后台配置入口:**GroMore 聚合管理 → 搜广告位ID → 编辑 → 勾选「服务端激励回调」**(广告位层级配了就别再在代码位层级重复配,会冲突)。
|
||||
>
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /api/v1/ad/reward-status — 今日看广告发奖进度
|
||||
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无(用户由 token 确定)。
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/ad/test-grant — [仅本地联调]模拟穿山甲回调发奖
|
||||
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:Bearer | 限流:同 IP ≤60 次/分 | [← 返回 API 索引](../README.md)
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:Bearer | 限流:同 IP ≤60 次/分 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> ⚠️ **仅本地联调**,受 `AD_REWARD_TEST_GRANT_ENABLED` 开关控制,**生产必须关闭**(默认 False → 一律 404)。
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# POST /api/v1/ad/watch-report — 上报激励视频观看时长
|
||||
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:Bearer | 限流:同 IP ≤120 次/分 | [← 返回 API 索引](../README.md)
|
||||
|
||||
客户端在激励视频关闭(onAdClose)后上报本次实际观看秒数,服务端累计到当日总时长。当前产品只保留每日 500 次上限,DAILY_AD_WATCH_SECONDS_LIMIT=0 表示时长闸不启用;该接口仍保留用于旧客户端兼容和排查观看时长。
|
||||
|
||||
## 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `seconds` | int | ✅ (≥0) | 本次观看秒数,服务端夹 [0, MAX_SINGLE_WATCH_SECONDS] |
|
||||
|
||||
Mock 入参:
|
||||
```json
|
||||
{
|
||||
"seconds": 28
|
||||
}
|
||||
```
|
||||
|
||||
## 出参
|
||||
|
||||
响应 `200`:`WatchReportOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `watched_seconds_today` | int | 今日累计观看秒数 |
|
||||
| `watch_seconds_limit` | int | 每日上限(秒);0 表示当前未启用时长闸 |
|
||||
| `watch_seconds_remaining` | int | 今日剩余可观看秒数 |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{
|
||||
"watched_seconds_today": 128,
|
||||
"watch_seconds_limit": 0,
|
||||
"watch_seconds_remaining": 0
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
- `401` 未鉴权 / token 失效
|
||||
- `422` `seconds` 缺或为负
|
||||
|
||||
## 说明
|
||||
- 鉴权靠 Bearer,`user_id` 取自 JWT(不信 body)
|
||||
- 后端只做累计 + 上限裁切,不据此发奖(发奖靠 S2S 回调)
|
||||
- `watch_seconds_limit=0` 时客户端不据此拦截,按每日 500 次上限走
|
||||
@@ -1,6 +1,6 @@
|
||||
# Admin 看广告金币审计
|
||||
|
||||
> 所属:Admin 组(前缀 `/admin/api/ad-coin-audit`) | 鉴权:Admin Bearer(任意已登录 admin,只读) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin 组(前缀 `/admin/api/ad-coin-audit`) | 鉴权:Admin Bearer(任意已登录 admin,只读) | [← 返回 API 索引](./README.md)
|
||||
|
||||
把「看视频赚金币」(`ad_reward_record`)和「比价信息流广告」(`ad_feed_reward_record`)两类发奖记录,用与**正式发奖完全相同**的公式 [`app/core/rewards.py` `calculate_ad_reward_coin`](../../app/core/rewards.py) 复算一遍 `expected_coin`,与实际入账的 `actual_coin` 对比,核对金币公式是否生效。**纯只读对账**,不发币、不改任何数据。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Admin 广告收益报表
|
||||
|
||||
> 所属:Admin 组(前缀 `/admin/api/ad-revenue-report`) | 鉴权:Admin Bearer(任意已登录 admin,只读) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin 组(前缀 `/admin/api/ad-revenue-report`) | 鉴权:Admin Bearer(任意已登录 admin,只读) | [← 返回 API 索引](./README.md)
|
||||
|
||||
按 **用户 × 日期 × 广告类型 × 我们的应用 × 我们的代码位** 聚合,回答「每个用户某天、每类广告(激励视频 / 信息流 / 历史 Draw)分别**看了多少条**、**收益多少**、按现算法**发了多少金币**、广告来自**哪个应用的哪个代码位**」。**纯只读**,不发币、不改数据,也**不改发奖逻辑**。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/admins — 创建管理员
|
||||
|
||||
> 所属:Admin·Accounts 组(前缀 `/admin/api/admins`) | 鉴权:Bearer admin_token(角色:super_admin) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·Accounts 组(前缀 `/admin/api/admins`) | 鉴权:Bearer admin_token(角色:super_admin) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
**application/json**:
|
||||
@@ -1,6 +1,6 @@
|
||||
# PATCH /admin/api/admins/{admin_id} — 改角色/启停/重置密码
|
||||
|
||||
> 所属:Admin·Accounts 组(前缀 `/admin/api/admins`) | 鉴权:Bearer admin_token(角色:super_admin) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·Accounts 组(前缀 `/admin/api/admins`) | 鉴权:Bearer admin_token(角色:super_admin) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
**路径参数**:
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/admins — 管理员列表
|
||||
|
||||
> 所属:Admin·Accounts 组(前缀 `/admin/api/admins`) | 鉴权:Bearer admin_token(角色:super_admin) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·Accounts 组(前缀 `/admin/api/admins`) | 鉴权:Bearer admin_token(角色:super_admin) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无(按 `id` 升序返回全部,无分页)
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/audit-logs — 审计日志(谁改了什么,游标分页)
|
||||
|
||||
> 所属:Admin·Audit 组(前缀 `/admin/api/audit-logs`) | 鉴权:Bearer admin_token(角色:任意已登录 admin) | [← 返回 API 索引](../README.md)
|
||||
> 所属:Admin·Audit 组(前缀 `/admin/api/audit-logs`) | 鉴权:Bearer admin_token(角色:任意已登录 admin) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参(query)
|
||||
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/auth/login — 管理员登录
|
||||
|
||||
> 所属:Admin·Auth 组(前缀 `/admin/api/auth`) | 鉴权:无 | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·Auth 组(前缀 `/admin/api/auth`) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
**application/json**:
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/auth/me — 当前管理员
|
||||
|
||||
> 所属:Admin·Auth 组(前缀 `/admin/api/auth`) | 鉴权:Bearer admin_token(角色:任意已登录 admin) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·Auth 组(前缀 `/admin/api/auth`) | 鉴权:Bearer admin_token(角色:任意已登录 admin) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无(身份取自 Header token)
|
||||
@@ -1,6 +1,6 @@
|
||||
# Admin 首页数据配置 — 三统计展示模式
|
||||
|
||||
> 所属:Admin 组(前缀 `/admin/api/dashboard-display`) | 鉴权:Admin Bearer(改需 operator/super) | [← 返回 API 索引](../README.md)
|
||||
> 所属:Admin 组(前缀 `/admin/api/dashboard-display`) | 鉴权:Admin Bearer(改需 operator/super) | [← 返回 API 索引](./README.md)
|
||||
|
||||
配置客户端首页三个门面数字(帮助用户 / 完成比价 / 累计节省)的展示模式。每个指标可独立选 real/manual/random。用户侧读取见 [platform-stats](./platform-stats.md);表见 [ops_stat_config](../database/ops_stat_config.md)。
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/feedbacks/{feedback_id}/handle — 标记反馈已处理
|
||||
|
||||
> 所属:Admin·反馈 组(前缀 `/admin/api/feedbacks`) | 鉴权:Bearer admin_token(角色:`operator`,`super_admin` 恒通过,`require_role("operator")`) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·反馈 组(前缀 `/admin/api/feedbacks`) | 鉴权:Bearer admin_token(角色:`operator`,`super_admin` 恒通过,`require_role("operator")`) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
- 路径:`feedback_id`(int)
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/feedbacks — 反馈工单列表(offset 分页 + 筛选/排序)
|
||||
|
||||
> 所属:Admin·反馈 组(前缀 `/admin/api/feedbacks`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 `require_role`,仅 `get_current_admin`) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·反馈 组(前缀 `/admin/api/feedbacks`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 `require_role`,仅 `get_current_admin`) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参(query)
|
||||
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
||||
@@ -1,6 +1,6 @@
|
||||
# Admin 首页轮播种子管理
|
||||
|
||||
> 所属:Admin 组(前缀 `/admin/api/marquee-seeds`) | 鉴权:Admin Bearer(改需 operator/super) | [← 返回 API 索引](../README.md)
|
||||
> 所属:Admin 组(前缀 `/admin/api/marquee-seeds`) | 鉴权:Admin Bearer(改需 operator/super) | [← 返回 API 索引](./README.md)
|
||||
|
||||
管理首页轮播「真实+种子混播」的兜底种子。种子是「生成规则」:`masked_user` 可空(空→feed 随机合成名)、金额是 `[min_cents, max_cents]` 区间(feed 每次随机取值)。用户侧 feed 见 [platform-savings-feed](./platform-savings-feed.md);表见 [ops_marquee_seed](../database/ops_marquee_seed.md)。金额单位:分(前端 ÷100 显示元)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/stats/overview — 大盘核心指标
|
||||
|
||||
> 所属:Admin·数据大盘 组(前缀 `/admin/api/stats`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 require_role) | [← 返回 API 索引](../README.md)
|
||||
> 所属:Admin·数据大盘 组(前缀 `/admin/api/stats`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 require_role) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/users/{user_id}/cash — 手动增减/设值现金(带审计)
|
||||
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](./README.md)
|
||||
|
||||
主要用于给无现金用户直接发钱、好让其测试提现链路。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/users/{user_id}/coins — 手动增减/设值金币(带审计)
|
||||
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
- 路径:`user_id`(int)
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/users/{user_id} — 用户 360 详情
|
||||
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 require_role) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 require_role) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
- 路径:`user_id`(int)
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/users/{user_id}/status — 封禁/解封用户
|
||||
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:`operator`,`super_admin` 恒通过) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:`operator`,`super_admin` 恒通过) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
- 路径:`user_id`(int)
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/users — 用户列表(筛选+排序+分页)
|
||||
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 require_role) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·用户 组(前缀 `/admin/api/users`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,无 require_role) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参(query)
|
||||
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/wallet/cash-transactions — 现金流水(游标分页)
|
||||
|
||||
> 所属:Admin·钱包 组(前缀 `/admin/api/wallet`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,仅需 `get_current_admin`,无 `require_role`) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·钱包 组(前缀 `/admin/api/wallet`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,仅需 `get_current_admin`,无 `require_role`) | [← 返回 API 索引](./README.md)
|
||||
|
||||
跨用户查询全量现金流水,可按 `user_id` / `biz_type` 过滤。游标分页(`id` 倒序)。金额单位一律为**分**。
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/wallet/coin-transactions — 金币流水(游标分页)
|
||||
|
||||
> 所属:Admin·钱包 组(前缀 `/admin/api/wallet`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,仅需 `get_current_admin`,无 `require_role`) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·钱包 组(前缀 `/admin/api/wallet`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,仅需 `get_current_admin`,无 `require_role`) | [← 返回 API 索引](./README.md)
|
||||
|
||||
跨用户查询全量金币流水,可按 `user_id` / `biz_type` 过滤。游标分页(`id` 倒序)。
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/withdraws/reconcile — 批量对账(扫超时 pending 单)
|
||||
|
||||
> 所属:Admin·提现 组(前缀 `/admin/api/withdraws`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·提现 组(前缀 `/admin/api/withdraws`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](./README.md)
|
||||
|
||||
扫描创建时间超过 `older_than_minutes` 分钟、仍为 `pending` 的提现单,逐单调微信查单并归一化(成功落 `success`;失败/已撤销则退款落 `failed`;查到 `WAIT_USER_CONFIRM` 视为用户放弃,撤单+退款)。用于解开"扣了款但转账没发起/没确认"的孤儿单。单笔失败不影响其余(内部 rollback 后继续,下轮再试)。
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# POST /admin/api/withdraws/{out_bill_no}/refresh — 单笔提现重试查单
|
||||
|
||||
> 所属:Admin·提现 组(前缀 `/admin/api/withdraws`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·提现 组(前缀 `/admin/api/withdraws`) | 鉴权:Bearer admin_token(角色:`finance`,`super_admin` 恒通过) | [← 返回 API 索引](./README.md)
|
||||
|
||||
对单笔提现单调微信查单并归一化:`SUCCESS`→`success`;`FAIL`/`CANCELLED`/`CLOSED`→退款+`failed`;查到 `WAIT_USER_CONFIRM` 视为用户放弃(`cancel_if_unconfirmed=True`),撤单+退款;`ACCEPTED`/`PROCESSING` 等仍在途则保持 `pending`。已是终态的单直接返回、不再查。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /admin/api/withdraws — 提现单列表(游标分页)
|
||||
|
||||
> 所属:Admin·提现 组(前缀 `/admin/api/withdraws`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,列表为只读,仅需 `get_current_admin`,无 `require_role`) | [← 返回 API 索引](../../README.md)
|
||||
> 所属:Admin·提现 组(前缀 `/admin/api/withdraws`) | 鉴权:Bearer admin_token(角色:任意已登录管理员,列表为只读,仅需 `get_current_admin`,无 `require_role`) | [← 返回 API 索引](./README.md)
|
||||
|
||||
跨用户查询全量提现单,可按 `user_id` / `status` 过滤。游标分页(`id` 倒序)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/auth/jverify-login — 极光一键登录
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](../README.md)
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/jiguang](../integrations/jiguang.md)(极光核验链路、RSA 解密策略、私钥配对踩坑)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/auth/logout — 登出
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:Bearer access_token | [← 返回 API 索引](../README.md)
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:Bearer access_token | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /api/v1/auth/me — 获取当前登录用户
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:Bearer access_token | [← 返回 API 索引](../README.md)
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:Bearer access_token | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无(身份取自 Header token)
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/auth/refresh — 刷新 token
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无(凭 body 里的 refresh_token) | [← 返回 API 索引](../README.md)
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无(凭 body 里的 refresh_token) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/auth/sms/login — 手机号 + 验证码登录
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](../README.md)
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/sms](../integrations/sms.md)(验证码校验逻辑)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/auth/sms/send — 发送短信验证码
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](../README.md)
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/sms](../integrations/sms.md)(mock 模式、频控、接真供应商 TODO)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/intent/recognize — 外卖比价 Phase 1 意图识别(透传到 pricebot)
|
||||
|
||||
> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**无(MVP 阶段不鉴权)** | [← 返回 API 索引](../README.md)
|
||||
> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**无(MVP 阶段不鉴权)** | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
任意 JSON body,**不做 schema 校验**,原样透传给上游。后端仅从中读 `device_id`、`trace_id`、`step` 用于日志。
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/compare/milestones/{milestone}/claim — 领取比价战绩里程碑奖励
|
||||
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
领取某一档(第 `milestone` 次)。⚠️ **当前不真发金币**(产品定,后续整体删除该功能):仍写
|
||||
`comparison_milestone_claim`((user_id, milestone) 唯一)标记该档已领、**每档只能领一次**,但不调
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /api/v1/compare/milestones — 比价战绩里程碑进度
|
||||
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
福利页「记录比价战绩」的数据源。返回各档(第 1~6 次)解锁/领取状态。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/price/step — 外卖比价 Phase 2 步进(透传到 pricebot)
|
||||
|
||||
> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**无(MVP 阶段不鉴权)** | [← 返回 API 索引](../README.md)
|
||||
> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**无(MVP 阶段不鉴权)** | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
任意 JSON body,**不做 schema 校验**,原样透传给上游。后端仅从中读 `device_id`、`trace_id`、`step` 用于日志。
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /api/v1/compare/records/{record_id} — 比价记录详情
|
||||
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
单条比价记录详情,在列表项基础上额外带 `raw_payload`(客户端上报的原始全量),供未来 UI 展示任意细节。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/compare/record — 上报一次比价结果(幂等)
|
||||
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
比价 `done` 帧后,客户端用**带 JWT 的通道**上报一条比价结果,落 `comparison_record` 表,作为「我的比价记录」数据源 + 用户级行为画像。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /api/v1/compare/records — 比价记录列表(游标分页)
|
||||
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
「我的比价记录」列表页数据源。按 `id` 倒序(最新在前)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /api/v1/compare/stats — 比价口径战绩(「我的」页省钱战绩卡)
|
||||
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
> 所属:比价记录组(前缀 `/api/v1/compare`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无(用户由 token 确定)。
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/coupon/step — 一键领券任务步进(透传到 pricebot)
|
||||
|
||||
> 所属:Coupon 组(前缀 `/api/v1/coupon`) | 鉴权:Bearer access_token(客户端契约;Server MVP 阶段不强校验) | [← 返回 API 索引](../README.md)
|
||||
> 所属:Coupon 组(前缀 `/api/v1/coupon`) | 鉴权:Bearer access_token(客户端契约;Server MVP 阶段不强校验) | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
任意 JSON body,**不做 schema 校验**,原样透传给上游。后端仅从中读 `device_id`、`trace_id`、`step` 用于日志。
|
||||
@@ -1,67 +0,0 @@
|
||||
# 今日领券完成状态(completed-today 族)
|
||||
|
||||
> 所属:Coupon 组(前缀 `/api/v1/coupon`) | 鉴权:无(按 device_id 判断) | [← 返回 API 索引](../README.md)
|
||||
|
||||
---
|
||||
|
||||
## GET /completed-today — 今天是否已完成整轮领券
|
||||
|
||||
判断这台设备今天是否跑到 done 帧。已完成 → 首页「去领取」卡置灰。
|
||||
|
||||
### 入参(query)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `device_id` | string | ✅ | 设备 ID(需与领券循环上报的一致) |
|
||||
|
||||
Mock 请求:
|
||||
```
|
||||
GET /api/v1/coupon/completed-today?device_id=android_abc123def456
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
响应 `200`:`CouponCompletedTodayOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `completed` | bool | 今天是否已跑完整轮 |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{"completed": true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /completed-today/reset — 重置今日已完成
|
||||
|
||||
删这台设备今天的 completion → `has_completed_today` 变 false,首页「去领取」卡恢复可点。开发设置用。
|
||||
|
||||
### 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `device_id` | string | ✅ | 设备 ID |
|
||||
| `package` | string | ❌ | App 包名(默认 "") |
|
||||
| `user_id` | int \| null | ❌ | 登录用户 ID |
|
||||
|
||||
Mock 入参:
|
||||
```json
|
||||
{
|
||||
"device_id": "android_abc123def456"
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
```json
|
||||
{"ok": true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
- 判断维度 `device_id`(客户端两端都用 ANDROID_ID)
|
||||
- 用户决策 A 方案:到 done 即算完成,不管单券成败
|
||||
- MVP 不鉴权
|
||||
@@ -1,128 +0,0 @@
|
||||
# 领券引导窗频控(prompt 族)
|
||||
|
||||
> 所属:Coupon 组(前缀 `/api/v1/coupon`) | 鉴权:无(按 device_id 判断,MVP 阶段) | [← 返回 API 索引](../README.md)
|
||||
|
||||
领券引导窗频控:今天这台设备**这个 App** 已弹过/领过/拒过 → 不再弹。各 App 独立(美团弹过不压淘宝/京东)。
|
||||
|
||||
---
|
||||
|
||||
## GET /prompt/should-show — 是否还应弹引导窗
|
||||
|
||||
客户端切到外卖 App 时查。
|
||||
|
||||
### 入参(query)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `device_id` | string | ✅ | 设备 ID |
|
||||
| `package` | string | ❌ | App 包名(默认 "",老客户端兼容全局态) |
|
||||
|
||||
Mock 请求:
|
||||
```
|
||||
GET /api/v1/coupon/prompt/should-show?device_id=android_abc&package=com.sankuai.meituan
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
响应 `200`:`CouponPromptShouldShowOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `should_show` | bool | 今天是否还应弹引导窗 |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{"should_show": true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /prompt/shown — 引导窗弹出即上报
|
||||
|
||||
客户端弹出引导窗那刻调 → 记一条今日 engagement(shown),今天这个 App 不再自动弹。频控主判据管跨重装。
|
||||
|
||||
### 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `device_id` | string | ✅ | 设备 ID |
|
||||
| `package` | string | ✅ | App 包名 |
|
||||
| `user_id` | int \| null | ❌ | 登录用户 ID |
|
||||
|
||||
Mock 入参:
|
||||
```json
|
||||
{
|
||||
"device_id": "android_abc123def456",
|
||||
"package": "com.sankuai.meituan",
|
||||
"user_id": 42
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
```json
|
||||
{"ok": true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /prompt/dismiss — 用户拒绝/关闭引导窗
|
||||
|
||||
客户端点关闭时调 → 记 dismissed,今天这个 App 不再弹。
|
||||
|
||||
### 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `device_id` | string | ✅ | 设备 ID |
|
||||
| `package` | string | ❌ | App 包名(默认 "") |
|
||||
| `user_id` | int \| null | ❌ | 登录用户 ID |
|
||||
|
||||
Mock 入参:
|
||||
```json
|
||||
{
|
||||
"device_id": "android_abc123def456",
|
||||
"package": "com.sankuai.meituan"
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
```json
|
||||
{"ok": true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /prompt/reset — 重置今日引导窗状态
|
||||
|
||||
删这台设备今天的 engagement → 今天又能弹。开发测频控用。
|
||||
|
||||
### 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `device_id` | string | ✅ | 设备 ID |
|
||||
| `package` | string | ❌ | App 包名(默认 "") |
|
||||
| `user_id` | int \| null | ❌ | 登录用户 ID |
|
||||
|
||||
Mock 入参:
|
||||
```json
|
||||
{
|
||||
"device_id": "android_abc123def456"
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
```json
|
||||
{"ok": true}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
- 频控按 `(device, package, 日)`,各 App 独立
|
||||
- 弹出即占用今天一次(管跨重装),后续领取/拒绝再升级 type
|
||||
- user_id 可选,登录态带上就一并记(资产留痕)
|
||||
- MVP 不鉴权
|
||||
@@ -1,75 +0,0 @@
|
||||
# POST /api/v1/coupon/session — 领券任务流水上报
|
||||
|
||||
> 所属:Coupon 组(前缀 `/api/v1/coupon`) | 鉴权:无(按 device_id/trace_id 区分) | [← 返回 API 索引](../README.md)
|
||||
|
||||
客户端两段上报一次领券流水(发起 / 收尾),按 `trace_id` upsert 到 `coupon_session`。供 admin「领券数据」看板算发起/完成数、耗时分位、机型维度。
|
||||
|
||||
## 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `trace_id` | string | ✅ | 领券 trace 标识(同领券循环 step 的 trace_id) |
|
||||
| `device_id` | string | ✅ | 设备 ID(与领券循环一致) |
|
||||
| `status` | string | ✅ | `started` / `completed` / `failed` / `abandoned` |
|
||||
| `started_at_ms` | int | ✅ | 发起墙钟毫秒(客户端 `System.currentTimeMillis`) |
|
||||
| `user_id` | int \| null | ❌ | 登录用户 ID(未登录可空) |
|
||||
| `platforms` | list[string] \| null | ❌ | 勾选的平台列表 |
|
||||
| `origin_package` | string \| null | ❌ | 发起领券的 App 包名 |
|
||||
| `device_model` | string \| null | ❌ | 机型(如 `24115RA8EC`) |
|
||||
| `rom` | string \| null | ❌ | ROM 信息(如 `MIUI 14.0.8`) |
|
||||
| `app_env` | string \| null | ❌ | 应用环境(`prod` / `test`) |
|
||||
| `elapsed_ms` | int \| null | ❌ | 全程耗时(ms),收尾帧必带 |
|
||||
| `platform_elapsed` | dict \| null | ❌ | 各平台耗时(如 `{"meituan": 3200, "jd": 2800}`),收尾帧带 |
|
||||
| `claimed_count` | int \| null | ❌ | 本场实际领到的券张数 |
|
||||
| `trace_url` | string \| null | ❌ | trace 云端 URL(done 帧带) |
|
||||
|
||||
Mock 入参(started):
|
||||
```json
|
||||
{
|
||||
"trace_id": "tr_20260703_a1b2c3d4",
|
||||
"device_id": "android_abc123def456",
|
||||
"status": "started",
|
||||
"started_at_ms": 1719993600000,
|
||||
"user_id": 42,
|
||||
"platforms": ["meituan", "jd"],
|
||||
"origin_package": "com.sankuai.meituan",
|
||||
"device_model": "24115RA8EC",
|
||||
"rom": "MIUI 14.0.8",
|
||||
"app_env": "prod"
|
||||
}
|
||||
```
|
||||
|
||||
Mock 入参(completed):
|
||||
```json
|
||||
{
|
||||
"trace_id": "tr_20260703_a1b2c3d4",
|
||||
"device_id": "android_abc123def456",
|
||||
"status": "completed",
|
||||
"started_at_ms": 1719993600000,
|
||||
"user_id": 42,
|
||||
"platforms": ["meituan", "jd"],
|
||||
"origin_package": "com.sankuai.meituan",
|
||||
"device_model": "24115RA8EC",
|
||||
"rom": "MIUI 14.0.8",
|
||||
"app_env": "prod",
|
||||
"elapsed_ms": 12500,
|
||||
"platform_elapsed": {"meituan": 5200, "jd": 4800},
|
||||
"claimed_count": 3,
|
||||
"trace_url": "https://trace.shaguabijia.com/tr_20260703_a1b2c3d4"
|
||||
}
|
||||
```
|
||||
|
||||
## 出参
|
||||
|
||||
响应 `200`:
|
||||
```json
|
||||
{"ok": true}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
- `422` 必填字段缺失或类型不符
|
||||
|
||||
## 说明
|
||||
- 不鉴权(同领券循环 MVP,按 `device_id`/`trace_id`)
|
||||
- 写库失败不连累客户端(fire-and-forget,吞掉返回 ok)
|
||||
- 一次领券两段上报:发起(started)→ 收尾(completed/failed/abandoned)
|
||||
@@ -1,33 +0,0 @@
|
||||
# GET /api/v1/coupon/stats — 累计领券数
|
||||
|
||||
> 所属:Coupon 组(前缀 `/api/v1/coupon`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
|
||||
「我的」页战绩卡「领取优惠券 X 张」数据源。该登录用户累计领到的券数(`SUM(claimed_count)`,与领券完成时给用户看的「本次领了 N 张」同源)。
|
||||
|
||||
**鉴权(CurrentUser)** — 区别于同文件不鉴权的 `/step` 透传与 `/prompt` 频控:个人战绩按 `user_id` 聚合,必须有登录态。
|
||||
|
||||
## 入参
|
||||
|
||||
无(`user_id` 从 JWT 取)。
|
||||
|
||||
## 出参
|
||||
|
||||
响应 `200`:`CouponStatsOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `coupon_count` | int | 累计领到的券张数 |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{
|
||||
"coupon_count": 42
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
- `401` 未鉴权 / token 失效
|
||||
|
||||
## 说明
|
||||
- 口径:`SUM(claimed_count)` — 各成功领券记录 pricebot 展示张数之和
|
||||
- 只算登录用户,按 `user_id` 聚合(非 device_id)
|
||||
@@ -1,6 +1,6 @@
|
||||
# CPS 群发短链落地(cps-redirect 族)
|
||||
|
||||
> 所属:cps-redirect 组(**无前缀**,挂域名根;源 `app/api/v1/cps_redirect.py`) | 鉴权:**公网无鉴权**(群里任何人点都要能跳/能领) | [← 返回 API 索引](../README.md)
|
||||
> 所属:cps-redirect 组(**无前缀**,挂域名根;源 `app/api/v1/cps_redirect.py`) | 鉴权:**公网无鉴权**(群里任何人点都要能跳/能领) | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 落库:点击落 [`cps_click`](../database/cps_click.md)、微信落地页用户落 [`cps_wx_user`](../database/cps_wx_user.md);短链由 [`cps_link`](../database/cps_link.md) 解析。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 设备 / 无障碍存活监控(device 族)
|
||||
|
||||
> 所属:device 组(前缀 `/api/v1/device`,源 `app/api/v1/device.py`) | 鉴权:**全部 Bearer**(设备绑登录用户) | [← 返回 API 索引](../README.md)
|
||||
> 所属:device 组(前缀 `/api/v1/device`,源 `app/api/v1/device.py`) | 鉴权:**全部 Bearer**(设备绑登录用户) | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 落库:[`device_liveness`](../database/device_liveness.md) 表;后台 `heartbeat_monitor_worker` 据此检出心跳超时的设备并召回。设计见 spec `accessibility-liveness-push.md`(推送版)+ `accessibility-liveness-pull-prompt.md`(后置 pull 提醒版)。#65 新增。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/feedback — 提交反馈
|
||||
|
||||
> 所属:Feedback 组(前缀 `/api/v1/feedback`) | 鉴权:Bearer access_token | [← 返回 API 索引](../README.md)
|
||||
> 所属:Feedback 组(前缀 `/api/v1/feedback`) | 鉴权:Bearer access_token | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
**multipart/form-data**:
|
||||
@@ -1,6 +1,6 @@
|
||||
# GET /health — 健康检查
|
||||
|
||||
> 所属:Meta | 鉴权:无 | [← 返回 API 索引](../README.md)
|
||||
> 所属:Meta | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
|
||||
## 入参
|
||||
无
|
||||
@@ -1,113 +0,0 @@
|
||||
# 比价意图多帧步进(intent/step + intent/precoupon/step)
|
||||
|
||||
> 所属:Intent 组(前缀 `/api/v1`,外卖比价透传) | 鉴权:无(MVP 阶段不鉴权) | [← 返回 API 索引](../README.md)
|
||||
|
||||
透传到 pricebot-backend。请求体原样转发、不做 schema 校验。
|
||||
|
||||
---
|
||||
|
||||
## POST /intent/step — Phase 1 多帧意图识别(淘宝源)
|
||||
|
||||
淘宝源走多帧版意图识别(展开+滚动采集→提取):循环调用直到 done(done 帧顶层带 `result` + `calibration`)。其它源走单次 `/intent/recognize`。
|
||||
|
||||
### 入参
|
||||
|
||||
透传 pricebot,客户端按 pricebot 协议组装。关键字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `device_id` | string | 设备 ID |
|
||||
| `trace_id` | string | 比价 trace 标识 |
|
||||
| `frame` | int | 帧序号(0 起始) |
|
||||
| `continue` | bool | 是否继续(pricebot 回传,客户端 next 调用时带上帧) |
|
||||
| `image` | string | 当前帧截图 base64 |
|
||||
| `platform` | string | 源平台 |
|
||||
| `package` | string | 目标平台包名 |
|
||||
| *(透传)* | | 其余字段由 pricebot 定义,本端点不做校验 |
|
||||
|
||||
Mock 入参(首帧):
|
||||
```json
|
||||
{
|
||||
"device_id": "android_abc123def456",
|
||||
"trace_id": "tr_20260703_e5f6g7h8",
|
||||
"frame": 0,
|
||||
"continue": true,
|
||||
"image": "/9j/4AAQSkZJRgABAQEAYABgAAD/...(base64)",
|
||||
"platform": "taobao",
|
||||
"package": "com.sankuai.meituan"
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
透传 pricebot 原始响应。通常包含 `action.command`(`continue` / `done`)、`action.params` 等。
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{
|
||||
"trace_id": "tr_20260703_e5f6g7h8",
|
||||
"frame": 0,
|
||||
"continue": true,
|
||||
"action": {
|
||||
"command": "continue",
|
||||
"params": {
|
||||
"next_frame": 1,
|
||||
"scroll_distance": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /intent/precoupon/step — Phase 0 意图识别前先用券(美团源)
|
||||
|
||||
美团源平台「意图识别前先用券」多帧循环:客户端在调 `/intent/recognize` 之前先循环调本端点到 done(`continue=false`)。订单页底部有「点击使用X红包」就自动选最大免费券用上,已用券/无券则首帧秒过。与 `/intent/step` 同属 intent 域,复用同一透传壳。
|
||||
|
||||
### 入参
|
||||
|
||||
同 `/intent/step`,透传 pricebot。
|
||||
|
||||
Mock 入参(首帧):
|
||||
```json
|
||||
{
|
||||
"device_id": "android_abc123def456",
|
||||
"trace_id": "tr_20260703_i9j0k1l2",
|
||||
"frame": 0,
|
||||
"continue": true,
|
||||
"image": "/9j/4AAQSkZJRgABAQEAYABgAAD/...(base64)",
|
||||
"platform": "meituan",
|
||||
"package": "com.sankuai.meituan"
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
透传 pricebot 原始响应。
|
||||
|
||||
Mock 出参(无券首帧秒过):
|
||||
```json
|
||||
{
|
||||
"trace_id": "tr_20260703_i9j0k1l2",
|
||||
"frame": 0,
|
||||
"continue": false,
|
||||
"action": {
|
||||
"command": "done",
|
||||
"params": {
|
||||
"coupon_used": false,
|
||||
"reason": "no_coupon_available"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码
|
||||
- `502` pricebot 不可达或返回 5xx
|
||||
- `400` 请求体不是合法 JSON
|
||||
|
||||
## 说明
|
||||
- 两个端点是同域透传,区别是 pricebot 后端路由不同(`/api/intent/step` vs `/api/intent/precoupon/step`)
|
||||
- 一致性 hash 按 `trace_id` 路由到同一 pricebot 实例
|
||||
- MVP 阶段不鉴权(待补 JWT)
|
||||
@@ -1,6 +1,6 @@
|
||||
# 内部回写端点(internal 族,pricebot / 发布流程 → app-server)
|
||||
|
||||
> 所属:internal 组(前缀 `/internal`,源 `app/api/internal/`) | 鉴权:**`X-Internal-Secret` 头**(== `settings.INTERNAL_API_SECRET`) | [← 返回 API 索引](../README.md)
|
||||
> 所属:internal 组(前缀 `/internal`,源 `app/api/internal/`) | 鉴权:**`X-Internal-Secret` 头**(== `settings.INTERNAL_API_SECRET`) | [← 返回 API 索引](./README.md)
|
||||
|
||||
**不是给客户端的接口**:不走用户 JWT。`§6` 是 app-server 透传给 pricebot,这里反过来——pricebot(及发布流程)把少量数据 server→server 回写 app-server 落库。
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# 邀请绑定(bind + landing-track)
|
||||
|
||||
> 所属:Invite 组(前缀 `/api/v1/invite`) | 鉴权:bind 需 Bearer / landing-track 无需鉴权 | [← 返回 API 索引](../README.md)
|
||||
|
||||
---
|
||||
|
||||
## POST /bind — 绑定邀请人
|
||||
|
||||
把当前登录用户(被邀请人)绑定到某邀请码。支持三种归因路径:clipboard(首启读剪贴板)、manual(手动输入邀请码)、fingerprint(指纹兜底反查)。绑定成功双方各发 1 万金币。
|
||||
|
||||
### 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `invite_code` | string \| null | ❌ | 邀请码;走指纹兜底时为空 |
|
||||
| `channel` | string | ✅ | 归因来源:`clipboard` / `manual` / `fingerprint` |
|
||||
| `fingerprint` | object \| null | ❌ | 指纹兜底时必传 |
|
||||
| `fingerprint.screen` | string | - | 屏幕分辨率(如 `1080x2400`) |
|
||||
| `fingerprint.device_model` | string | - | 设备型号(如 `24115RA8EC`) |
|
||||
|
||||
Mock 入参(clipboard 归因):
|
||||
```json
|
||||
{
|
||||
"invite_code": "A3F8K2",
|
||||
"channel": "clipboard",
|
||||
"fingerprint": null
|
||||
}
|
||||
```
|
||||
|
||||
Mock 入参(指纹兜底):
|
||||
```json
|
||||
{
|
||||
"invite_code": null,
|
||||
"channel": "fingerprint",
|
||||
"fingerprint": {
|
||||
"screen": "1080x2400",
|
||||
"device_model": "24115RA8EC"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
响应 `200`:`BindInviteOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `status` | string | `success` / `already_bound` / `invalid_code` / `self_invite` / `not_eligible` / `fp_not_found` |
|
||||
| `coins_awarded` | int | 本次给当前用户(被邀请人)发的金币 |
|
||||
| `message` | string | 给前端直接展示的文案 |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"coins_awarded": 10000,
|
||||
"message": "邀请绑定成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /landing-track — 落地页指纹采集
|
||||
|
||||
B 浏览器打开 `dl.html?ref=xxx` 时上报指纹(剪贴板归因失败时兜底)。**无需鉴权**(浏览器没 token)。
|
||||
|
||||
### 入参
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `ref` | string | ✅(4-16 位) | 邀请码(落地页 `?ref=`) |
|
||||
| `screen` | string | ❌ | 屏幕分辨率(如 `1080x2400`) |
|
||||
|
||||
Mock 入参:
|
||||
```json
|
||||
{
|
||||
"ref": "A3F8K2",
|
||||
"screen": "1080x2400"
|
||||
}
|
||||
```
|
||||
|
||||
### 出参
|
||||
|
||||
响应 `200`:`LandingTrackOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `status` | string | `ok` / `invalid_code` / `no_ip` |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{"status": "ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 说明
|
||||
- `bind`: 绑定只在注册后首次有效(`not_eligible` = 已过新人期),自邀屏蔽
|
||||
- `landing-track`: IP/UA 服务端从 HTTP 头自动拿,JS 无需上报
|
||||
- 指纹反查窗口期由 `INVITE_FP_WINDOW_DAYS` 控制
|
||||
@@ -1,60 +0,0 @@
|
||||
# GET /api/v1/invite/invitees — 我邀请的人列表
|
||||
|
||||
> 所属:Invite 组(前缀 `/api/v1/invite`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
|
||||
分页查询当前用户成功邀请的人列表。邀请页小窗(取前几条)+ 完整列表页(分页加载)共用。
|
||||
|
||||
## 入参(query)
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `limit` | int | ❌ | 每页条数(1–50,默认 20) |
|
||||
| `offset` | int | ❌ | 偏移量(≥0,默认 0) |
|
||||
|
||||
Mock 请求:
|
||||
```
|
||||
GET /api/v1/invite/invitees?limit=5&offset=0
|
||||
```
|
||||
|
||||
## 出参
|
||||
|
||||
响应 `200`:`InviteeListOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `items` | list[InviteeItem] | 被邀请人列表 |
|
||||
| `items[].display_name` | string | 显示名(昵称 → 微信昵称 → 脱敏手机号,后端已兜底) |
|
||||
| `items[].avatar_url` | string \| null | 头像 URL;null = 前端画默认色块 |
|
||||
| `items[].coins` | int | 这次邀请给邀请人发的金币 |
|
||||
| `items[].invited_at` | datetime | 邀请绑定时间(ISO 8601 UTC) |
|
||||
| `total` | int | 我邀请的总人数 |
|
||||
| `has_more` | bool | 还有下一页吗 |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"display_name": "省钱小王",
|
||||
"avatar_url": "/media/avatars/u2_f1e2d3c4b5a60708.jpg",
|
||||
"coins": 10000,
|
||||
"invited_at": "2026-06-28T14:30:00Z"
|
||||
},
|
||||
{
|
||||
"display_name": "138****1234",
|
||||
"avatar_url": null,
|
||||
"coins": 10000,
|
||||
"invited_at": "2026-07-01T09:15:00Z"
|
||||
}
|
||||
],
|
||||
"total": 5,
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
- `401` 未鉴权 / token 失效
|
||||
|
||||
## 说明
|
||||
- 名字/头像降级兜底在后端算好:昵称 → 微信昵称 → 脱敏手机号
|
||||
- `limit` 钳到 [1, 50],`offset` 钳到 ≥0
|
||||
@@ -1,48 +0,0 @@
|
||||
# GET /api/v1/invite/me — 我的邀请信息
|
||||
|
||||
> 所属:Invite 组(前缀 `/api/v1/invite`) | 鉴权:Bearer | [← 返回 API 索引](../README.md)
|
||||
|
||||
获取当前用户的邀请码、分享链接、已邀人数、累计获得金币/奖励金,以及 7 天一轮的倒计时信息。
|
||||
|
||||
## 入参
|
||||
|
||||
无(`user_id` 从 JWT 取)。
|
||||
|
||||
## 出参
|
||||
|
||||
响应 `200`:`InviteInfoOut`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `invite_code` | string | 我的邀请码(6-8 位) |
|
||||
| `share_url` | string | 落地页链接(含 `?ref=`),前端据此生成二维码 + 复制分享 |
|
||||
| `invited_count` | int | 已成功邀请人数 |
|
||||
| `coins_earned` | int | 累计从邀请获得的金币(v1 口径) |
|
||||
| `reward_balance_cents` | int | v2 可提现邀请奖励金(分) |
|
||||
| `reward_withdrawn_cents` | int | v2 累计提现成功的邀请奖励金(分) |
|
||||
| `countdown_days_left` | int | v2 本轮剩余天数(7 天 1 轮) |
|
||||
| `countdown_is_fresh_round` | bool | 是否刚进入新一轮(非首轮第 1 天) |
|
||||
| `countdown_text` | string | 倒计时展示文案(前端直接显示,新轮含换行) |
|
||||
|
||||
Mock 出参:
|
||||
```json
|
||||
{
|
||||
"invite_code": "A3F8K2",
|
||||
"share_url": "https://app.shaguabijia.com/dl.html?ref=A3F8K2",
|
||||
"invited_count": 5,
|
||||
"coins_earned": 50000,
|
||||
"reward_balance_cents": 3200,
|
||||
"reward_withdrawn_cents": 1800,
|
||||
"countdown_days_left": 4,
|
||||
"countdown_is_fresh_round": false,
|
||||
"countdown_text": "还剩 4 天"
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
- `401` 未鉴权 / token 失效
|
||||
|
||||
## 说明
|
||||
- 首次调用自动生成邀请码(幂等)
|
||||
- v2 奖励金与现金隔离(`reward_balance_cents` 独立于 `cash_balance_cents`)
|
||||
- 7 天 1 轮倒计时:新用户从注册日起算
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/meituan/coupons — 券列表 / 搜索
|
||||
|
||||
> 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](../README.md)
|
||||
> 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/meituan](../integrations/meituan.md)(CPS S-Ca 签名、入参换算坑)。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POST /api/v1/meituan/feed — 首页推荐流(多 tab)
|
||||
|
||||
> 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](../README.md)
|
||||
> 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/meituan](../integrations/meituan.md)(CPS S-Ca 签名、入参换算坑);离线库见 [database/meituan_coupon](../database/meituan_coupon.md)。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user