diff --git a/app/api/v1/compare.py b/app/api/v1/compare.py index fa8268a..69c656f 100644 --- a/app/api/v1/compare.py +++ b/app/api/v1/compare.py @@ -21,7 +21,6 @@ from __future__ import annotations import json import logging import time -import uuid from typing import Any import httpx @@ -32,6 +31,7 @@ from app.api.deps import DbSession, OptionalUser from app.core.config import settings from app.core.logging import trace_id_ctx from app.core.pricebot_client import get_pricebot_client +from app.core.trace_ids import new_trace_id from app.core.pricebot_router import pick_pricebot from app.db.session import SessionLocal from app.repositories import comparison as crud_compare @@ -142,7 +142,7 @@ async def _forward( trace_id = meta.get("trace_id") minted = False if not trace_id: - trace_id = str(uuid.uuid4()) + trace_id = new_trace_id() meta["trace_id"] = trace_id raw = json.dumps(meta).encode() # 仅首帧重新序列化(注入 trace_id);后续帧走原始 bytes minted = True diff --git a/app/api/v1/compare_record.py b/app/api/v1/compare_record.py index af1f88d..b0d5428 100644 --- a/app/api/v1/compare_record.py +++ b/app/api/v1/compare_record.py @@ -12,12 +12,12 @@ from __future__ import annotations import logging -import uuid from fastapi import APIRouter, BackgroundTasks, HTTPException, Query, status from app.api.deps import CurrentUser, DbSession from app.core import limit_policy +from app.core.trace_ids import new_trace_id from app.repositories import comparison as crud_compare from app.repositories import risk as risk_repo from app.schemas.compare_record import ( @@ -57,7 +57,7 @@ def reserve_compare_start( # trace_id 统一由服务端签发(客户端不带时):预占额度本就是任务的第一个请求, # 签发与建 running 行合一,此后 Phase1/Phase2/记录/前端日志全链用同一个 id。 # 客户端带了则沿用——老客户端兼容 + 同 trace 重试幂等(reserve_daily_start 按 trace_id 去重)。 - trace_id = payload.trace_id or str(uuid.uuid4()) + trace_id = payload.trace_id or new_trace_id() try: policy = limit_policy.resolve( db, diff --git a/app/api/v1/coupon.py b/app/api/v1/coupon.py index a3179d8..5038761 100644 --- a/app/api/v1/coupon.py +++ b/app/api/v1/coupon.py @@ -12,7 +12,6 @@ from __future__ import annotations import json import logging -import uuid from typing import Any import httpx @@ -22,6 +21,7 @@ from fastapi.concurrency import run_in_threadpool from app.api.deps import CurrentUser, DbSession from app.core.config import settings from app.core.pricebot_client import get_pricebot_client +from app.core.trace_ids import new_trace_id from app.core.pricebot_router import pick_pricebot from app.db.session import SessionLocal from app.repositories import coupon_state as coupon_repo @@ -228,7 +228,7 @@ def coupon_session(payload: CouponSessionIn, db: DbSession) -> CouponSessionOut: 签发新 id 只会造出一行查不到发起信息的孤儿),不写库、trace_id=null 返回。 """ trace_id = payload.trace_id or ( - str(uuid.uuid4()) if payload.status == "started" else None + new_trace_id() if payload.status == "started" else None ) if trace_id is None: logger.warning( diff --git a/app/core/trace_ids.py b/app/core/trace_ids.py new file mode 100644 index 0000000..480d70e --- /dev/null +++ b/app/core/trace_ids.py @@ -0,0 +1,29 @@ +"""trace_id 签发(全后端唯一签发口径, 2026-07 起替代裸 uuid4)。 + +格式: "YYYYMMDD_HHMMSS_" + 12 位小写 hex 随机, 共 28 字符, 如 + 20260731_162254_a1b2c3d4e5f6 + +Why 带时间前缀: pricebot 落盘目录名/trace_url 尾段**直接用 trace_id 本身** +(见 pricebot app/utils/trace_ids.py), id/目录/URL 三者合一——此前 uuid trace_id +与 {首帧时刻}_{uuid[:16]} 目录名是两套标识, URL 只有 pricebot 能拼、按前缀反查 +还有同秒歧义。时间用北京时间(CN_TZ)——不依赖各机器 TZ 配置, 与业务时区一致。 + +唯一性: 秒级前缀 + 48bit 随机(hex12), 同一秒内碰撞概率可忽略(比价/领券发起 QPS +远低于产生生日碰撞的量级); pricebot 侧同秒多 trace 靠随机段区分(目录精确匹配、 +llm jsonl 按尾 12 分文件, 不做前缀模糊匹配)。 + +兼容: 三个签发点(compare/start、coupon/session started、compare.py _forward mint) +统一走这里; 客户端自带 trace_id(老客户端/重试幂等)仍原样沿用——pricebot 对老 +uuid 格式保持既有目录/短标识行为, 两代 id 并行不冲突。 +""" +from __future__ import annotations + +import uuid +from datetime import datetime + +from app.core.rewards import CN_TZ + + +def new_trace_id() -> str: + """签发一个自描述 trace_id: 北京时间前缀 + 12 位 hex 随机。""" + return f"{datetime.now(CN_TZ):%Y%m%d_%H%M%S}_{uuid.uuid4().hex[:12]}"