From f9f3d90f96639ffa7fc4790e6d883aa0e1d1abf4 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Mon, 21 Sep 2026 16:34:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BA=8C=E7=BB=B4=E7=A0=81=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=8E=BB=E9=89=B4=E6=9D=83=EF=BC=88=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=A0=B4=E5=9B=BE=EF=BC=89+=20=E4=BA=A7=E5=93=81=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8F=B7=E5=8A=A0=E9=83=A8=E9=97=A8=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) 二维码破图 /products/qrcode/{sn} 带了 Depends(get_current_user),而前端是用 引用它的 —— 无法携带 Authorization 头,请求必然 401,页面上就是破图(同时刷大量 401 审计)。 该接口不查库、只把调用方传进来的字符串渲染成二维码,没有数据泄露面, 故去掉鉴权。刻意不做 ?token= 兜底:JWT 进 URL 会渗进访问日志、浏览器 历史与 Referer,比它想解决的问题更糟。 实测:HTTP 200 / image/png / 300x300。 2) 序列号部门前缀 新增 config.SERIAL_PREFIX(LICA 为 "L"),counter_service 生成 {前缀}{15 位 HEX},总长仍严格 16 位 —— products.serial_number 是 String(16),前端 TaskTreeViewer / ManualInput / ScanPage 多处按 16 位 校验,不能改总长。IRIS 实例该值为空串,格式保持原样。 实测 LICA 生成 L000000000000002。 前端本就兼容带字母的序列号:TaskTreeViewer 的占位符示例就是 X20260801000001,ScanPage 的 replace(/[^a-zA-Z0-9]/g,"") 也不会滤掉 L。 --- backend/app/api/v1/endpoints/products.py | 19 +++++++++---- backend/app/core/config.py | 3 ++ backend/app/services/counter_service.py | 36 ++++++++++++++++++------ 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/backend/app/api/v1/endpoints/products.py b/backend/app/api/v1/endpoints/products.py index 878faa8..b756066 100644 --- a/backend/app/api/v1/endpoints/products.py +++ b/backend/app/api/v1/endpoints/products.py @@ -26,13 +26,22 @@ router = APIRouter(prefix="/products", tags=["产品管理"]) # ============================================================ @router.get("/qrcode/{serial_number}") -async def get_product_qrcode( - serial_number: str, - current_user: dict = Depends(get_current_user), -): +async def get_product_qrcode(serial_number: str): """ 生成产品二维码(PNG 图片)。 - 内容为 16 位序列号,扫描后可调用 /scan/{serial_number} 查询产品。 + + ⚠️ 本接口【刻意不加鉴权】: + 前端以 `` 引用它,而 + 无法携带 Authorization 头 —— 加了鉴权会让所有二维码图片加载失败 + (页面上显示成破图),并在审计里刷出大量 401。 + + 不加鉴权是安全的:本函数**不查数据库**,只校验长度并把这个字符串渲染成 + 二维码,没有任何业务数据泄露面(序列号本身就是调用方提供的)。 + + 也刻意不支持 ?token= 兜底:把 JWT 放进 URL 会渗进访问日志、浏览器历史 + 与 Referer,比它想解决的问题更糟。 + + 内容为序列号,扫描后可调用 /scan/{serial_number} 查询产品。 尺寸:300×300 px,用于 PC 端打印或嵌入标签。 """ if len(serial_number) != 16: diff --git a/backend/app/core/config.py b/backend/app/core/config.py index b1e7c27..5126d4a 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -38,6 +38,9 @@ class Settings(BaseSettings): # 全仓库的部门过滤点只有三处 SQL:登录、人员列表、物料选择器,全部引用这里。 ORG_DEPARTMENT: str = "LICA" # MOM sys_user.department 的取值 MATERIAL_CATEGORY_PREFIX: str = "LICA/" # MOM material_base.category 的部门前缀 + # 产品序列号前缀(产品身份证 / 二维码内容)。留空则生成纯 16 位 HEX。 + # LICA 用 "L" 打头,一眼区分部门来源;IRIS 实例保持空串不受影响。 + SERIAL_PREFIX: str = "L" @property def CORS_ORIGINS_LIST(self) -> list[str]: diff --git a/backend/app/services/counter_service.py b/backend/app/services/counter_service.py index ac4535e..4e9f5e5 100644 --- a/backend/app/services/counter_service.py +++ b/backend/app/services/counter_service.py @@ -1,9 +1,14 @@ -"""16进制自增计数器 — 基于 PostgreSQL Sequence,生成 16 位 HEX 唯一 ID""" +"""序列号计数器 — 基于 PostgreSQL Sequence,生成定长序列号(产品身份证)""" from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession +from app.core.config import settings + SEQUENCE_NAME = "product_hex_counter" +# 序列号总长度(与 products.serial_number String(16)、前端的 16 位校验一致) +SERIAL_LENGTH = 16 + async def ensure_sequence(db: AsyncSession) -> None: """确保 counter sequence 存在(幂等)""" @@ -12,15 +17,30 @@ async def ensure_sequence(db: AsyncSession) -> None: ) -async def next_hex_id(db: AsyncSession, length: int = 16) -> str: +async def next_hex_id(db: AsyncSession, length: int = SERIAL_LENGTH) -> str: """ - 生成下一个 hex ID。 + 生成下一个序列号:{部门前缀}{递增计数器的 16 进制},总长固定为 length。 - 示例: 1 → "0000000000000001" - 15 → "000000000000000F" - 16 → "0000000000000010" - 255 → "00000000000000FF" + 前缀来自 settings.SERIAL_PREFIX(LICA 是 "L",IRIS 留空), + 用来在一眼扫号时区分部门来源;计数器部分保持 HEX,便于人工核对数量。 + + 示例(SERIAL_PREFIX = "L"): + 1 → "L000000000000001" + 255 → "L0000000000000FF" + 示例(SERIAL_PREFIX = "",IRIS 实例): + 1 → "0000000000000001" + 255 → "00000000000000FF" + + ⚠️ 长度必须严格等于 length:products.serial_number 是 String(16), + 且 /products/qrcode/{sn} 接口会校验 16 位,前缀是从 HEX 位里让出来的。 """ result = await db.execute(text(f"SELECT nextval('{SEQUENCE_NAME}');")) counter: int = result.scalar_one() - return format(counter, f"0{length}X") + + prefix = settings.SERIAL_PREFIX or "" + hex_length = length - len(prefix) + if hex_length <= 0: + raise ValueError( + f"SERIAL_PREFIX({prefix!r}) 过长,没有给计数器留下位数" + ) + return f"{prefix}{format(counter, f'0{hex_length}X')}"