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:
Misaka_Company
2026-07-24 16:00:21 +08:00
parent 622f348cf1
commit ee566e3fbd
5 changed files with 236 additions and 121 deletions

117
db.py
View File

@@ -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)
# ---------------------------------------------------------------------------