Forráskód Böngészése

docs: 将数据库修复设计文档改为中文

Sherlock 2 napja
szülő
commit
1d3091821a
1 módosított fájl, 41 hozzáadás és 41 törlés
  1. 41 41
      docs/superpowers/specs/2026-03-15-database-bugfix-design.md

+ 41 - 41
docs/superpowers/specs/2026-03-15-database-bugfix-design.md

@@ -1,51 +1,51 @@
-# Database Bug Fix Design
+# 数据库操作 Bug 修复设计
 
-Date: 2026-03-15
-Scope: `database/db/mysql.py`, `database/dao/mysql_dao.py`
-Approach: Surgical bug fixes only (no architectural changes)
+日期:2026-03-15
+范围:`database/db/mysql.py`、`database/dao/mysql_dao.py`
+方案:外科手术式 bug 修复,不改架构
 
-## Problems
+## 问题清单
 
 ### mysql.py
 
-1. `connect_database`: try/except only wraps connection string construction, not `create_engine`. Real connection failures are uncaught.
-2. `load_data_with_page`: Uses `query.replace("SELECT *", "SELECT COUNT(*)")` to build count query. Breaks if query doesn't start with `SELECT *`. Also mutates the caller's `params` dict in-place by adding `limit` and `offset` keys.
-3. `fetch_all` / `fetch_one`: Catch `SQLAlchemyError`, print, and return `None` silently. Callers cannot detect failures.
+1. `connect_database`:try/except 只包了连接字符串拼接,`create_engine` 在 try 块外,真正的连接失败不会被捕获。
+2. `load_data_with_page`:用 `query.replace("SELECT *", "SELECT COUNT(*)")` 构建 count 查询,查询不以 `SELECT *` 开头时会出错。另外会直接修改调用方传入的 `params` 字典(添加 `limit`、`offset` 键),产生副作用。
+3. `fetch_all` / `fetch_one`:捕获 `SQLAlchemyError` 后只打印错误、静默返回 `None`,调用方无法感知失败。
 
 ### mysql_dao.py
 
-4. `get_cust_by_ids`, `get_shop_by_ids`, `get_product_by_ids`, `get_order_by_product_ids`: Build `IN (...)` clause via string concatenation — SQL injection risk.
-5. `get_product_by_id`: Queries a single record using `load_data_with_page` (pagination overhead for one row).
-6. `get_cust_list`, `get_product_from_order`: Call `fetch_all(text(query))` directly while all other methods use `load_data_with_page` — inconsistent interface. Depends on Fix 2 being applied first.
+4. `get_cust_by_ids`、`get_shop_by_ids`、`get_product_by_ids`、`get_order_by_product_ids`:`IN (...)` 子句通过字符串拼接构建,存在 SQL 注入风险。
+5. `get_product_by_id`:查询单条记录却走了分页逻辑,有不必要的开销。
+6. `get_cust_list`、`get_product_from_order`:直接调用 `fetch_all(text(query))`,与其他方法统一使用 `load_data_with_page` 的风格不一致。依赖修复 2 先完成。
 
-## Fixes
+## 修复方案
 
 ### mysql.py
 
-**Fix 1 — `connect_database`**
-Move `create_engine(...)` inside the try block so connection failures are caught and re-raised as `ConnectionAbortedError`.
+**修复 1 — `connect_database`**
+将 `create_engine(...)` 移入 try 块,确保连接失败时被捕获并抛出 `ConnectionAbortedError`。
 
-**Fix 2 — `load_data_with_page`**
-Replace string substitution with subquery wrapping:
+**修复 2 — `load_data_with_page`**
+用子查询包裹原始查询来构建 count 查询,不再依赖字符串替换:
 ```sql
-SELECT COUNT(*) FROM (<original_query>) AS _count_subq
+SELECT COUNT(*) FROM (<原始查询>) AS _count_subq
 ```
-Works regardless of the original SELECT clause. Also copy `params` before mutating to avoid side-effects on the caller's dict:
+无论原始查询的 SELECT 子句是什么都能正确计数。同时在修改 params 前先复制,避免副作用:
 ```python
-params = dict(params)  # avoid mutating caller's dict
+params = dict(params)  # 避免修改调用方的字典
 ```
 
-**Fix 3 — `fetch_all` / `fetch_one`**
-Add `raise` in the except block after printing the error.
+**修复 3 — `fetch_all` / `fetch_one`**
+在 except 块打印错误后加 `raise`,让异常向上传播。
 
-Note: This is a behavior-breaking change. Currently callers receive `None` on failure; after this fix they receive an unhandled exception. This is intentional — silent failures are harder to debug than explicit ones. Callers that currently rely on `None` returns (e.g. `pd.DataFrame(self.db_helper.fetch_all(...))`) will propagate the exception upward, which is the correct behavior.
+注意:这是一个行为破坏性变更。当前调用方在失败时收到 `None`,修复后会收到未处理的异常。这是有意为之——显式异常比静默失败更容易排查问题。
 
 ### mysql_dao.py
 
-**Fix 4 — SQL injection in IN clauses**
-`load_data_with_page` processes its `query` argument as a plain string (string concatenation, then wraps in `text()`), so pre-built `text()` objects with `bindparam(expanding=True)` are incompatible with it.
+**修复 4 — IN 子句 SQL 注入**
+`load_data_with_page` 将 query 参数作为普通字符串处理(字符串拼接后再包装成 `text()`),与预构建的 `text()` 对象不兼容。
 
-Instead, switch the four affected methods to use `fetch_all` directly with `bindparam(expanding=True)`:
+因此,受影响的四个方法改为直接调用 `fetch_all`,配合 `bindparam(expanding=True)` 参数化:
 ```python
 from sqlalchemy import bindparam, text
 
@@ -57,29 +57,29 @@ query = text("""
 params = {"city_uuid": city_uuid, "ids": list(id_list)}
 data = pd.DataFrame(self.db_helper.fetch_all(query, params))
 ```
-These methods are bounded by the input list size, so skipping pagination is safe.
+这些方法的结果集大小由输入列表决定,跳过分页是安全的。
 
-**Fix 5 — `get_product_by_id`**
-Replace `load_data_with_page` with `fetch_one`. Wrap the result to return a single-row DataFrame consistent with other methods:
+**修复 5 — `get_product_by_id`**
+改用 `fetch_one` 查单条,返回单行 DataFrame 与其他方法保持一致:
 ```python
 result = self.db_helper.fetch_one(text(query), params)
 return pd.DataFrame([dict(result._mapping)] if result else [])
 ```
 
-**Fix 6 — `get_cust_list` / `get_product_from_order`**
-Replace direct `fetch_all(text(query))` calls with `load_data_with_page(query, params)`. Requires Fix 2 to be applied first (subquery count approach handles `SELECT DISTINCT` correctly).
+**修复 6 — `get_cust_list` / `get_product_from_order`**
+将直接调用 `fetch_all(text(query))` 改为 `load_data_with_page(query, params)`,统一接口风格。需要修复 2 先完成(子查询方式能正确处理 `SELECT DISTINCT`)。
 
-## Fix Application Order
+## 修复顺序
 
-Fixes must be applied in this order due to dependencies:
-1. Fix 2 (load_data_with_page subquery count)
-2. Fix 1, Fix 3 (independent, can be done together)
-3. Fix 6 (depends on Fix 2)
-4. Fix 4, Fix 5 (independent of above)
+由于存在依赖关系,按以下顺序执行:
+1. 修复 2(load_data_with_page 子查询 count)
+2. 修复 1、修复 3(相互独立,可同时进行)
+3. 修复 6(依赖修复 2)
+4. 修复 4、修复 5(相互独立)
 
-## Out of Scope
+## 不在范围内
 
-- No architectural changes (no read/write separation, no ORM models)
-- No changes to Redis layer
-- No changes to DAO method signatures
-- No additional error handling in DAO callers
+- 不改架构(不做读写分离,不引入 ORM 模型)
+- 不改 Redis 层
+- 不改 DAO 方法签名
+- 不在 DAO 调用方添加额外错误处理