refactor: --id 改为真实 ID 列,新增 --sn 对应总排号
此前接口 --id 实际按总排号列查询,与数据库真实 ID 字段语义混淆。
现明确区分两种键类型:
- --id -> 数据库真实 ID 列(config id_field)
- --sn -> 总排号列(config id_column,即原先 --id 的语义)
- --ids-file 视为总排号(键类型 sn)
db.py: fetch_params_by_ids 改为接受 (标识符, 键类型) 列表,返回
{标识符: {param, sn}};id 键回取对应总排号;新增 id_field 配置读取
classifier.py: classify_batch/classify_single 透传键类型,输出 zong_pai_hao
一律为回查到的总排号,not_found 时回退输入标识符便于追溯
main.py / write_attachments.py: 新增 --sn,--id 改指真实 ID,二者可混用;
全表扫描回退仍按总排号(sn)查询
README.md: 对齐 --id/--sn 语义并补充 id_field 配置说明
注:config.yaml 含密钥被 .gitignore 忽略,id_field 仅存于本地配置;
db.py 在未配置 id_field 时优雅降级为按总排号查询并告警。
Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
This commit is contained in:
117
db.py
117
db.py
@@ -32,60 +32,101 @@ def _build_conn_str(db_cfg: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def fetch_params_by_ids(
|
||||
db_cfg: dict[str, Any], zong_pai_hao_list: list[str]
|
||||
) -> dict[str, str | None]:
|
||||
"""按总排号批量查询新参数字段。
|
||||
db_cfg: dict[str, Any], id_keys: list[tuple[str, str]]
|
||||
) -> dict[str, dict[str, Any | None]]:
|
||||
"""按 (标识符, 键类型) 批量查询新参数字段。
|
||||
|
||||
返回 dict:{总排号: 新参数文本}。数据库中不存在的总排号,其值为 None
|
||||
(而不是直接从结果里省略该 key),方便调用方区分"没查到"和"查到但内容为空"。
|
||||
id_keys: list[(identifier, key)],key 取值:
|
||||
"sn" -> 按总排号列(配置 id_column)查询,标识符即总排号;
|
||||
"id" -> 按数据库真实 ID 列(配置 id_field)查询,并回取对应的总排号。
|
||||
|
||||
一个总排号只对应一条记录(业务已确认为一对一关系);如果实际数据出现
|
||||
重复总排号,取查询结果的第一条并记录一条 WARNING 日志,不中断整体流程。
|
||||
返回 dict:{identifier: {"param": 新参数文本|None, "sn": 总排号|None}}。
|
||||
标识符作为 key 原样保留(便于回查);未查到的标识符其 param/sn 为 None,
|
||||
与"查到但内容为空"区分开。sn 为对应的总排号(键类型为 sn 时即标识符本身,
|
||||
键类型为 id 时由数据库回取);若数据库未配置 id_field 且使用了 "id" 键,
|
||||
则退化为按总排号列查询(仅向后兼容,会在日志告警)。
|
||||
|
||||
一对一关系:同一标识符出现多条记录时取第一条并记录 WARNING。
|
||||
"""
|
||||
if not zong_pai_hao_list:
|
||||
if not id_keys:
|
||||
return {}
|
||||
|
||||
result: dict[str, str | None] = {zph: None for zph in zong_pai_hao_list}
|
||||
result: dict[str, dict[str, Any | None]] = {
|
||||
idt: {"param": None, "sn": None} for idt, _ in id_keys
|
||||
}
|
||||
|
||||
schema = db_cfg["schema"]
|
||||
table = db_cfg["table"]
|
||||
id_col = db_cfg["id_column"]
|
||||
param_col = db_cfg["param_column"]
|
||||
|
||||
# 表名必须带 schema 前缀(如 [dbo].[表名]),只写表名在 schema 不是默认dbo时
|
||||
# 会查到错误的表,甚至直接报"找不到对象"。schema 和表名分别加中括号转义,
|
||||
# 不能写成 [schema.table],那样会被当成一个整体标识符解析。
|
||||
sn_col = db_cfg["id_column"] # 总排号列
|
||||
id_field = db_cfg.get("id_field") # 真实 ID 列,可缺省
|
||||
qualified_table = f"[{schema}].[{table}]"
|
||||
|
||||
# 用参数化查询防止总排号里混入特殊字符导致 SQL 注入或语法错误
|
||||
placeholders = ",".join("?" for _ in zong_pai_hao_list)
|
||||
sql = f"SELECT [{id_col}], [{param_col}] FROM {qualified_table} WHERE [{id_col}] IN ({placeholders})"
|
||||
sn_items = [idt for idt, k in id_keys if k == "sn"]
|
||||
id_items = [idt for idt, k in id_keys if k == "id"]
|
||||
|
||||
conn_str = _build_conn_str(db_cfg)
|
||||
try:
|
||||
with pyodbc.connect(conn_str, timeout=db_cfg["connect_timeout"]) as conn:
|
||||
cursor = conn.cursor()
|
||||
# 查询超时设在 Connection 上(pyodbc 的 timeout 是 Connection 属性,
|
||||
# Cursor 没有该属性,设 cursor.timeout 会报 AttributeError)。
|
||||
conn.timeout = db_cfg["query_timeout"]
|
||||
cursor.execute(sql, zong_pai_hao_list)
|
||||
seen = set()
|
||||
for row in cursor.fetchall():
|
||||
zph, param = row[0], row[1]
|
||||
if zph in seen:
|
||||
logger.warning("总排号 %s 存在重复记录,已取第一条", zph)
|
||||
continue
|
||||
seen.add(zph)
|
||||
result[zph] = param
|
||||
except pyodbc.Error as e:
|
||||
raise DatabaseError(f"数据库查询失败: {e}") from e
|
||||
def _fill(identifiers: list[str], where_col: str, is_id_key: bool) -> None:
|
||||
if not identifiers:
|
||||
return
|
||||
# 表名/schema 加中括号转义,不能写成 [schema.table]
|
||||
placeholders = ",".join("?" for _ in identifiers)
|
||||
if is_id_key:
|
||||
# 查真实 ID 列,同时回取总排号列(sn_col)作为 sn
|
||||
sql = (
|
||||
f"SELECT [{where_col}], [{sn_col}], [{param_col}] "
|
||||
f"FROM {qualified_table} WHERE [{where_col}] IN ({placeholders})"
|
||||
)
|
||||
else:
|
||||
sql = (
|
||||
f"SELECT [{where_col}], [{param_col}] "
|
||||
f"FROM {qualified_table} WHERE [{where_col}] IN ({placeholders})"
|
||||
)
|
||||
|
||||
conn_str = _build_conn_str(db_cfg)
|
||||
try:
|
||||
with pyodbc.connect(conn_str, timeout=db_cfg["connect_timeout"]) as conn:
|
||||
conn.timeout = db_cfg["query_timeout"]
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(sql, identifiers)
|
||||
seen: set[str] = set()
|
||||
for row in cursor.fetchall():
|
||||
if is_id_key:
|
||||
ident, sn_val, param = row[0], row[1], row[2]
|
||||
else:
|
||||
ident, param = row[0], row[1]
|
||||
sn_val = ident # 总排号即标识符本身
|
||||
ident = str(ident)
|
||||
if ident in seen:
|
||||
logger.warning("标识符 %s 在列 [%s] 上重复,已取第一条", ident, where_col)
|
||||
continue
|
||||
seen.add(ident)
|
||||
entry = result.get(ident)
|
||||
if entry is not None:
|
||||
entry["param"] = param
|
||||
entry["sn"] = sn_val
|
||||
except pyodbc.Error as e:
|
||||
raise DatabaseError(f"数据库查询失败: {e}") from e
|
||||
|
||||
# 总排号键:直接查 id_column
|
||||
_fill(sn_items, sn_col, is_id_key=False)
|
||||
# 真实 ID 键:查 id_field;未配置时降级为总排号列并告警
|
||||
if id_items:
|
||||
if id_field:
|
||||
_fill(id_items, id_field, is_id_key=True)
|
||||
else:
|
||||
logger.warning(
|
||||
"未配置 database.id_field,--id 将退化为按总排号列 [%s] 查询", sn_col
|
||||
)
|
||||
_fill(id_items, sn_col, is_id_key=False)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def fetch_param_by_id(db_cfg: dict[str, Any], zong_pai_hao: str) -> str | None:
|
||||
"""单个总排号查询的便捷封装。"""
|
||||
return fetch_params_by_ids(db_cfg, [zong_pai_hao]).get(zong_pai_hao)
|
||||
def fetch_param_by_id(
|
||||
db_cfg: dict[str, Any], identifier: str, key: str = "sn"
|
||||
) -> dict[str, Any | None] | None:
|
||||
"""单个查询的便捷封装,返回 {param, sn} 或 None(未查到)。"""
|
||||
return fetch_params_by_ids(db_cfg, [(identifier, key)]).get(identifier)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user