fix: 二维码接口去鉴权(修复破图)+ 产品序列号加部门前缀

1) 二维码破图
   /products/qrcode/{sn} 带了 Depends(get_current_user),而前端是用
   <img src="/api/v1/products/qrcode/{sn}"> 引用它的 —— <img> 无法携带
   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。
This commit is contained in:
2026-09-21 16:34:56 +08:00
parent f04c7e0b99
commit f9f3d90f96
3 changed files with 45 additions and 13 deletions

View File

@ -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_PREFIXLICA 是 "L"IRIS 留空),
用来在一眼扫号时区分部门来源;计数器部分保持 HEX便于人工核对数量。
示例SERIAL_PREFIX = "L":
1 → "L000000000000001"
255 → "L0000000000000FF"
示例SERIAL_PREFIX = ""IRIS 实例):
1 → "0000000000000001"
255 → "00000000000000FF"
⚠️ 长度必须严格等于 lengthproducts.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')}"