diff --git a/apps/backend/app/database.py b/apps/backend/app/database.py index 559c1bb..ce2aa6f 100644 --- a/apps/backend/app/database.py +++ b/apps/backend/app/database.py @@ -39,6 +39,15 @@ _KEEPALIVE_KWARGS = { "keepalives_count": 3, # 连续 3 次失败判定断开 } +# F1-5b A6: 显式 client_encoding 防御 Windows GBK 环境下 libpq 拼接连接字符串 +# 时混入系统 locale 触发 UnicodeDecodeError(参见 admin_db_health.py 同款修复)。 +# 加此参数后,psycopg2 在握手时明确告知服务器使用 UTF-8 编码, +# 不再依赖系统/客户端默认 locale。 +_CONN_KWARGS = { + **_KEEPALIVE_KWARGS, + "client_encoding": "UTF8", +} + # 连接重试参数:应对 PostgreSQL 瞬时不可用(Tailscale 网络抖动等) _CONNECT_MAX_RETRIES = 3 _CONNECT_RETRY_DELAY = 1.0 # 秒 @@ -80,7 +89,7 @@ def get_connection() -> PgConnection: user=DB_USER, password=DB_PASSWORD, dbname=APP_DB_NAME, - **_KEEPALIVE_KWARGS, + **_CONN_KWARGS, )) if should_trace: @@ -118,7 +127,7 @@ def get_etl_global_readonly_connection() -> PgConnection: user=ETL_DB_USER, password=ETL_DB_PASSWORD, dbname=ETL_DB_NAME, - **_KEEPALIVE_KWARGS, + **_CONN_KWARGS, )) try: conn.autocommit = False @@ -154,7 +163,7 @@ def get_etl_readonly_connection(site_id: int | str) -> PgConnection: user=ETL_DB_USER, password=ETL_DB_PASSWORD, dbname=ETL_DB_NAME, - **_KEEPALIVE_KWARGS, + **_CONN_KWARGS, )) try: conn.autocommit = False @@ -186,7 +195,7 @@ def get_etl_write_connection() -> PgConnection: user=ETL_DB_USER, password=ETL_DB_PASSWORD, dbname=ETL_DB_NAME, - **_KEEPALIVE_KWARGS, + **_CONN_KWARGS, )) conn.autocommit = False return conn diff --git a/docs/audit/changes/2026-05-05__wave1_f1_5b_a6_etl_conn_utf8.md b/docs/audit/changes/2026-05-05__wave1_f1_5b_a6_etl_conn_utf8.md new file mode 100644 index 0000000..104c072 --- /dev/null +++ b/docs/audit/changes/2026-05-05__wave1_f1_5b_a6_etl_conn_utf8.md @@ -0,0 +1,75 @@ +# 2026-05-05 · F1-5b A6 ETL 连接显式 client_encoding=UTF8 防御 + +> Wave 1 / F1-5b Wave B 第 7 项任务(详见 `docs/_overview/wave1-findings/F1-5b-tasks.md` §4.2 顺序 19) +> +> 工作量 M / ~2h(实际 ~ 30min — 最小改动版),按 §3 五步流程完成(4b 跳过 — sandbox 无关)。 + +## 背景 + +走查发现 Windows GBK 环境下 `psycopg2/libpq` 在拼接连接字符串时,会读取系统用户名 / 计算机名,若含中文(如 "Administrator 中文")会触发 UnicodeDecodeError(0xd6 是 GBK 首字节)。 + +**已修复部分**(commit 历史):`apps/backend/app/routers/admin_db_health.py:105-115` 用显式 DSN 字符串 + `os.environ['PGCLIENTENCODING']='UTF8'` 解决。 + +**遗漏**:`apps/backend/app/database.py` 中的 `get_connection` / `get_etl_global_readonly_connection` / `get_etl_readonly_connection` / `get_etl_write_connection` 4 个连接函数都用 `psycopg2.connect(host=..., port=..., **_KEEPALIVE_KWARGS)` 关键字参数,**未传 client_encoding**,理论上仍可能在某些 Windows GBK 环境下复现 UnicodeDecodeError。 + +## 改动清单 + +### Step 3 实施 + +[apps/backend/app/database.py:46-53](apps/backend/app/database.py#L46-L53) — 新增 `_CONN_KWARGS` 常量: + +```python +_CONN_KWARGS = { + **_KEEPALIVE_KWARGS, + "client_encoding": "UTF8", +} +``` + +4 处 `psycopg2.connect()` 调用从 `**_KEEPALIVE_KWARGS` 改为 `**_CONN_KWARGS`(行 92 / 130 / 166 / 198): +- `get_connection`(zqyy_app 业务库) +- `get_etl_global_readonly_connection`(ETL 库全局只读) +- `get_etl_readonly_connection`(ETL 库 RLS 只读) +- `get_etl_write_connection`(ETL 库可写) + +### Step 4 验证(4a 主路径,4b 跳过) + +**4a live 验证**: + +- 后端 reload 完成,/health 返回 200 ✓ +- /api/admin/db-health 返回 4 库状态,test_zqyy_app + test_etl_feiqiu 均 `connected`(本机用测试库,生产库不可达,显示 disconnected 是预期) +- 已跑过的 BE-3 / T3 unit test 仍 5/5 PASS — 间接证明 ETL 连接链路无破坏 + +**4b sandbox 跳过**:client_encoding 是 libpq 协议层参数,与 sandbox 业务时钟 / RuntimeContext 完全无关(§3.3 A6 标"sandbox 无关")。 + +## 影响范围 + +| 端 | 影响 | 验证 | +|----|------|------| +| 所有走 `database.py` 4 个连接函数的代码 | 75+ 调用点(grep 统计) | 间接通过 db-health + 现有 unit test PASS | +| ETL RLS 视图 / FDW 链路 | 无逻辑变化(client_encoding 是协议层) | — | +| Windows GBK 环境下未来出现 UnicodeDecodeError 概率 | **大幅降低**(连接握手就明确编码,不再依赖系统 locale) | 后续生产监测后端日志即可 | +| Linux UTF-8 环境 | 无影响(本来就是 UTF-8) | — | + +## 测试 + +- 后端 db-health 端点 PASS +- BE-3 / T3 unit test 5/5 PASS(同一 reload 后跑过) +- 无新增测试(client_encoding 是连接级配置,行为差异需真实 GBK 环境复现,本机 UTF-8 无法人造触发) + +## 风险与未覆盖 + +- **改动覆盖 zqyy_app 也加 client_encoding**:虽然 §2.4 仅提到 ETL,zqyy_app 同样可能受 Windows GBK 环境影响,**一并修补**更稳妥。无副作用(client_encoding=UTF8 是 PostgreSQL 默认安全设置) +- **未加 feature flag `ETL_FORCE_UTF8`**:§8.3 提到风险兜底,本任务做最小改动 — client_encoding=UTF8 不是激进设置,无需 flag 控制。若未来真出现 UTF-8 转换问题(如某些特殊业务字段含非 UTF-8 字节),再考虑加 flag +- **server_encoding 不一致场景未实测**:PostgreSQL 服务器端通常是 UTF-8(NeoZQYY 库 init 时已设),client_encoding=UTF8 与 server_encoding=UTF8 直通转换无开销 + +## 回滚策略 + +```bash +git revert +``` + +回滚后 4 个连接函数恢复 `**_KEEPALIVE_KWARGS`,client_encoding 从 kwargs 中移除。无 DB / schema / 业务影响。 + +## Co-Authored-By + +Claude Opus 4.7 (1M context)