diff --git a/app/service/financial_nl2sql_service.py b/app/service/financial_nl2sql_service.py index 9c20e12..55e0f77 100644 --- a/app/service/financial_nl2sql_service.py +++ b/app/service/financial_nl2sql_service.py @@ -46,10 +46,32 @@ def _range(question: str) -> tuple[str | None, str | None]: return start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S") +#: 产品代码形态:本平台现库**5 位与 6 位并存**(6 位 25 个,5 位 1 个 —— 演示产品 `15911`)。 +#: +#: 原先只认 `\d{6}`,于是 5 位代码被**静默丢弃**:查询退化成"不带产品过滤", +#: SQL 里没有 `fin_product.product_code = ?`,却仍返回 `status=success` —— 实测 +#: 「查询15911的净值」返回的是 511810 等其它产品的净值,**看起来成功、数据却是错的**。 +_PRODUCT_CODE = re.compile(r"(? list[str]: + codes: list[str] = [] + for match in _PRODUCT_CODE.finditer(question): + code = match.group(0) + if len(code) == 5 and _AMOUNT_UNIT_AFTER.match(question[match.end():]): + continue + codes.append(code) + return sorted(set(codes)) + + def _filters(question: str) -> tuple[dict[str, Any], ...]: return tuple( {"field": "fin_product.product_code", "operator": "=", "value": code} - for code in sorted(set(re.findall(r"(? None: assert result["status"] == "ready" assert "fin_cash_ledger" in result["sql"] assert result["query_plan"]["intent"] == "cash_ledger_query" + + +# --- 产品代码提取:5 位与 6 位并存(2026-09-14 修) -------------------------------- +# +# 原先只认 `\d{6}`,5 位代码(演示产品 `15911`)被静默丢弃 ⇒ 查询**不带产品过滤**, +# 却仍返回 `status=success`:实测问 15911 的净值拿到的是 511810 的数据。 +# 「看起来成功、数据却是错的」比报错更危险,所以这几条要钉住。 + + +@pytest.mark.asyncio +async def test_five_digit_product_code_is_used_as_filter() -> None: + result = await FinancialNL2SQLService().query( + FinancialNL2SQLInput(question="查询15911的净值", dry_run=True), + context(), + ) + + assert result["status"] == "ready" + assert result["parameters"]["filter_0"] == "15911" + # 关键:SQL 里必须**真的带上产品过滤**(列名走别名 `p.`),否则就是"成功但查错数据"。 + assert "p.product_code = :filter_0" in result["sql"] + + +@pytest.mark.asyncio +async def test_six_digit_product_code_still_works() -> None: + result = await FinancialNL2SQLService().query( + FinancialNL2SQLInput(question="查询510300的净值", dry_run=True), + context(), + ) + + assert result["parameters"]["filter_0"] == "510300" + + +@pytest.mark.asyncio +async def test_amount_like_five_digit_number_is_not_treated_as_product_code() -> None: + """「申购金额50000元」里的 50000 是金额,不是产品代码 —— 它后面紧跟单位。""" + result = await FinancialNL2SQLService().query( + FinancialNL2SQLInput(question="查询申购金额50000元的记录", dry_run=True), + context(), + ) + + assert "filter_0" not in result.get("parameters", {}) + + +def test_product_codes_ignores_years_and_short_numbers() -> None: + from app.service.financial_nl2sql_service import _product_codes + + assert _product_codes("查询2026年的净值") == [] # 4 位年份 + assert _product_codes("查询近30天净值") == [] # 天数 + assert _product_codes("查询15911的净值") == ["15911"] + assert _product_codes("对比15911和510300") == ["15911", "510300"]