Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f0c80e51b9 |
@@ -0,0 +1,54 @@
|
|||||||
|
"""add user.debug_trace_enabled + comparison_record.trace_url(并合并既存双 head)
|
||||||
|
|
||||||
|
调试链接权限功能:
|
||||||
|
- user.debug_trace_enabled:运营后台给指定用户开「复制调试链接」权限
|
||||||
|
- comparison_record.trace_url:比价记录页「复制调试链接」的数据(pricebot done 帧给,
|
||||||
|
dir 名含落盘时分秒、前端/server 拼不出,必须落库)
|
||||||
|
|
||||||
|
⚠️ 本迁移 down_revision 是 **tuple**,同时合并了 clone 时 main 上既存的双 head:
|
||||||
|
- invite_fingerprint_table (#31 被邀请人列表 + 指纹归因 那条线)
|
||||||
|
- 044dce6e9b1f (merge_ad_feed_reward_session + withdraw_safety_indexes)
|
||||||
|
这两个头平行未合并,`alembic upgrade head` 会报 Multiple heads。本迁移顺手收口成单 head。
|
||||||
|
若想把「合并双 head」与「加列」拆开,可改成:先一个纯 merge 迁移、再一个加列迁移。
|
||||||
|
|
||||||
|
Revision ID: f8d3b1e60a27
|
||||||
|
Revises: invite_fingerprint_table, 044dce6e9b1f
|
||||||
|
Create Date: 2026-06-10 02:40:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "f8d3b1e60a27"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = (
|
||||||
|
"invite_fingerprint_table",
|
||||||
|
"044dce6e9b1f",
|
||||||
|
)
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# sa.false() 渲染成 PG 的 false / SQLite 的 0,两端兼容(避免字符串 "false" 在 SQLite 上存歪)
|
||||||
|
op.add_column(
|
||||||
|
"user",
|
||||||
|
sa.Column(
|
||||||
|
"debug_trace_enabled",
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.false(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"comparison_record",
|
||||||
|
sa.Column("trace_url", sa.String(length=512), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("comparison_record", "trace_url")
|
||||||
|
op.drop_column("user", "debug_trace_enabled")
|
||||||
@@ -24,6 +24,20 @@ def set_user_status(db: Session, user: User, *, status: str, commit: bool = True
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_debug_trace(
|
||||||
|
db: Session, user: User, *, enabled: bool, commit: bool = True
|
||||||
|
) -> User:
|
||||||
|
"""开关用户「复制调试链接」权限。同 set_user_status:支持 commit=False 让 router 把
|
||||||
|
业务写 + 审计写放进同一事务。"""
|
||||||
|
user.debug_trace_enabled = enabled
|
||||||
|
if commit:
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
else:
|
||||||
|
db.flush()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
def update_feedback_status(
|
def update_feedback_status(
|
||||||
db: Session, feedback: Feedback, *, status: str, commit: bool = True
|
db: Session, feedback: Feedback, *, status: str, commit: bool = True
|
||||||
) -> Feedback:
|
) -> Feedback:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from app.admin.schemas.user import (
|
|||||||
AdminUserListItem,
|
AdminUserListItem,
|
||||||
AdminUserOverview,
|
AdminUserOverview,
|
||||||
GrantCoinsRequest,
|
GrantCoinsRequest,
|
||||||
|
SetDebugTraceRequest,
|
||||||
SetUserStatusRequest,
|
SetUserStatusRequest,
|
||||||
)
|
)
|
||||||
from app.models.admin import AdminUser
|
from app.models.admin import AdminUser
|
||||||
@@ -77,6 +78,28 @@ def set_user_status(
|
|||||||
return OkResponse()
|
return OkResponse()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{user_id}/debug-trace", response_model=OkResponse, summary="开关调试链接权限")
|
||||||
|
def set_user_debug_trace(
|
||||||
|
user_id: int,
|
||||||
|
body: SetDebugTraceRequest,
|
||||||
|
request: Request,
|
||||||
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||||
|
db: AdminDb,
|
||||||
|
) -> OkResponse:
|
||||||
|
user = user_repo.get_user_by_id(db, user_id)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(status_code=404, detail="用户不存在")
|
||||||
|
before = user.debug_trace_enabled
|
||||||
|
# 业务写 + 审计写同一事务(commit=False),最后一起 commit(同 set_user_status)
|
||||||
|
mutations.set_user_debug_trace(db, user, enabled=body.enabled, commit=False)
|
||||||
|
write_audit(
|
||||||
|
db, admin, action="user.debug_trace.set", target_type="user", target_id=user_id,
|
||||||
|
detail={"before": before, "after": body.enabled}, ip=get_client_ip(request), commit=False,
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return OkResponse()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{user_id}/coins", response_model=OkResponse, summary="手动增减金币(带审计)")
|
@router.post("/{user_id}/coins", response_model=OkResponse, summary="手动增减金币(带审计)")
|
||||||
def grant_user_coins(
|
def grant_user_coins(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class AdminUserListItem(BaseModel):
|
|||||||
nickname: str | None = None
|
nickname: str | None = None
|
||||||
register_channel: str
|
register_channel: str
|
||||||
status: str
|
status: str
|
||||||
|
debug_trace_enabled: bool = False
|
||||||
wechat_openid: str | None = None
|
wechat_openid: str | None = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
last_login_at: datetime
|
last_login_at: datetime
|
||||||
@@ -45,3 +46,7 @@ class SetUserStatusRequest(BaseModel):
|
|||||||
status: Literal["active", "disabled"] = Field(
|
status: Literal["active", "disabled"] = Field(
|
||||||
..., description="active=解封 / disabled=封禁(注销 deleted 不走此接口)"
|
..., description="active=解封 / disabled=封禁(注销 deleted 不走此接口)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SetDebugTraceRequest(BaseModel):
|
||||||
|
enabled: bool = Field(..., description="是否给该用户开「复制调试链接」权限")
|
||||||
|
|||||||
@@ -77,10 +77,12 @@ def list_records(
|
|||||||
items, next_cursor = crud_compare.list_records(
|
items, next_cursor = crud_compare.list_records(
|
||||||
db, user.id, limit=limit, cursor=cursor
|
db, user.id, limit=limit, cursor=cursor
|
||||||
)
|
)
|
||||||
return ComparisonRecordPage(
|
outs = [ComparisonRecordOut.model_validate(it) for it in items]
|
||||||
items=[ComparisonRecordOut.model_validate(it) for it in items],
|
# 权限闸:未开 debug_trace_enabled 的用户不下发 trace_url(列表页「复制调试链接」靠它)
|
||||||
next_cursor=next_cursor,
|
if not user.debug_trace_enabled:
|
||||||
)
|
for o in outs:
|
||||||
|
o.trace_url = None
|
||||||
|
return ComparisonRecordPage(items=outs, next_cursor=next_cursor)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -94,4 +96,12 @@ def get_record(
|
|||||||
rec = crud_compare.get_record(db, user.id, record_id)
|
rec = crud_compare.get_record(db, user.id, record_id)
|
||||||
if rec is None:
|
if rec is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="record not found")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="record not found")
|
||||||
return ComparisonRecordDetailOut.model_validate(rec)
|
out = ComparisonRecordDetailOut.model_validate(rec)
|
||||||
|
# 权限闸:未开 debug_trace_enabled 的用户不下发 trace_url。
|
||||||
|
# ⚠️ raw_payload 是上报体全量(model_dump),里面也藏着一份 trace_url,必须一并抹掉——
|
||||||
|
# 否则无权限用户从详情接口的 raw_payload 绕过权限闸拿到 trace_url。
|
||||||
|
if not user.debug_trace_enabled:
|
||||||
|
out.trace_url = None
|
||||||
|
if isinstance(out.raw_payload, dict):
|
||||||
|
out.raw_payload.pop("trace_url", None)
|
||||||
|
return out
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ class ComparisonRecord(Base):
|
|||||||
)
|
)
|
||||||
# pricebot 侧 trace_id:关联调试落盘 + 幂等去重键
|
# pricebot 侧 trace_id:关联调试落盘 + 幂等去重键
|
||||||
trace_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
trace_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||||
|
# 本次比价的公网调试链接(price.shaguabijia.com/traces/{dir}/)。pricebot done 帧给、
|
||||||
|
# 客户端上报带上——dir 名含 pricebot 落盘的时分秒,前端/server 都拼不出,必须存。
|
||||||
|
# 查看接口按 user.debug_trace_enabled 决定返不返回。旧记录 / 未开上云为 None。
|
||||||
|
trace_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||||
|
|
||||||
# ===== 源平台(发起比价的那家)=====
|
# ===== 源平台(发起比价的那家)=====
|
||||||
source_platform_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
source_platform_id: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||||
|
|||||||
+8
-1
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from sqlalchemy import DateTime, Integer, String, func
|
from sqlalchemy import Boolean, DateTime, Integer, String, false, func
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
@@ -51,6 +51,13 @@ class User(Base):
|
|||||||
# 账号状态:active / disabled / deleted
|
# 账号状态:active / disabled / deleted
|
||||||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active")
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active")
|
||||||
|
|
||||||
|
# 调试链接权限:开了的用户在比价完成弹窗 + 比价记录页能看到「复制调试链接」按钮
|
||||||
|
# (复制 price.shaguabijia.com 的 trace 链接发给开发排障)。运营后台按用户配置;
|
||||||
|
# /me 与登录响应里带出给前端做条件渲染。默认 false。
|
||||||
|
debug_trace_enabled: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, default=False, server_default=false()
|
||||||
|
)
|
||||||
|
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), server_default=func.now(), nullable=False
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ def upsert_record(
|
|||||||
source_package=payload.source_package,
|
source_package=payload.source_package,
|
||||||
information=payload.information,
|
information=payload.information,
|
||||||
best_deeplink=payload.best_deeplink,
|
best_deeplink=payload.best_deeplink,
|
||||||
|
trace_url=payload.trace_url,
|
||||||
total_dish_count=payload.total_dish_count,
|
total_dish_count=payload.total_dish_count,
|
||||||
skipped_dish_count=payload.skipped_dish_count,
|
skipped_dish_count=payload.skipped_dish_count,
|
||||||
items=[it.model_dump(exclude_none=True) for it in payload.items],
|
items=[it.model_dump(exclude_none=True) for it in payload.items],
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class UserOut(BaseModel):
|
|||||||
status: str
|
status: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
last_login_at: datetime
|
last_login_at: datetime
|
||||||
|
# 调试链接权限:前端据此在比价结果弹窗/记录页显示「复制调试链接」按钮。默认 false。
|
||||||
|
debug_trace_enabled: bool = False
|
||||||
|
|
||||||
|
|
||||||
# ===== Token 通用结构 =====
|
# ===== Token 通用结构 =====
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ class ComparisonRecordIn(BaseModel):
|
|||||||
status: str | None = Field(None, description="success / failed,可不传由服务端派生")
|
status: str | None = Field(None, description="success / failed,可不传由服务端派生")
|
||||||
# 最优平台商家/商品深链(客户端从 collectedLinks[best_index] 取);「再次比价」直达用
|
# 最优平台商家/商品深链(客户端从 collectedLinks[best_index] 取);「再次比价」直达用
|
||||||
best_deeplink: str | None = Field(None, description="最优平台深链,再次比价直达")
|
best_deeplink: str | None = Field(None, description="最优平台深链,再次比价直达")
|
||||||
|
# pricebot done.params.trace_url 原样上报,落库供记录页「复制调试链接」(dir 名含落盘
|
||||||
|
# 时分秒前端拼不出,必须由后端透传)。
|
||||||
|
trace_url: str | None = Field(None, description="本次比价公网调试链接")
|
||||||
|
|
||||||
|
|
||||||
# ===== 读取出参 =====
|
# ===== 读取出参 =====
|
||||||
@@ -79,6 +82,8 @@ class ComparisonRecordOut(BaseModel):
|
|||||||
id: int
|
id: int
|
||||||
business_type: str
|
business_type: str
|
||||||
trace_id: str
|
trace_id: str
|
||||||
|
# 公网调试链接;仅当 user.debug_trace_enabled 时由端点填充,否则端点层置 None(权限闸)。
|
||||||
|
trace_url: str | None = None
|
||||||
source_platform_id: str | None = None
|
source_platform_id: str | None = None
|
||||||
source_platform_name: str | None = None
|
source_platform_name: str | None = None
|
||||||
source_package: str | None = None
|
source_package: str | None = None
|
||||||
|
|||||||
Reference in New Issue
Block a user