Merge branch 'dev' into codex/bank-tag-real-rule-phase2-backend
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
# 新增模型打标完整验证 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 对 2026-03-20 新加入的模型打标改动执行一轮完整验证,覆盖 Mock 随机命中、第一期真实规则、数据库事实和接口端到端结果。
|
||||
|
||||
**Architecture:** 这次工作不是改代码,而是按“环境与基线 -> 自动化回归 -> 数据库核验 -> 接口链路 -> 文档沉淀”五层顺序执行。只要某一层失败,就停在该层记录证据,不继续给出“验证通过”的结论,也不进入修复。
|
||||
|
||||
**Tech Stack:** Bash, Python 3, FastAPI, Java 21, Spring Boot 3, Maven, pytest, MySQL, curl, jq
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- `docs/tests/plans/2026-03-20-bank-tag-new-model-validation-test-plan.md`: 已确认的验证计划,执行时必须严格对齐范围与停点。
|
||||
- `docs/tests/plans/2026-03-20-bank-tag-new-model-validation-execution-plan.md`: 本执行计划,负责把验证计划拆成可执行步骤。
|
||||
- `docs/reports/implementation/2026-03-20-bank-tag-new-model-validation-record.md`: 本次实施记录,记录执行内容与范围。
|
||||
- `docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md`: 本次验证记录,记录命令、SQL、接口结果和结论。
|
||||
- `docs/reports/implementation/2026-03-20-lsfx-mock-random-hit-rule-backend-record.md`: Mock 随机命中改动的既有实施记录。
|
||||
- `docs/tests/records/2026-03-20-lsfx-mock-random-hit-rule-backend-verification.md`: Mock 随机命中改动的既有验证记录。
|
||||
- `docs/reports/implementation/2026-03-20-bank-tag-real-rule-phase1-backend-record.md`: 第一期真实规则改动的既有实施记录。
|
||||
- `docs/tests/records/2026-03-20-bank-tag-real-rule-phase1-backend-verification.md`: 第一期真实规则改动的既有验证记录。
|
||||
- `ruoyi-admin/src/main/resources/application-dev.yml`: 读取数据库连接与后端本地配置。
|
||||
- `sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql`: `LARGE_PURCHASE_TRANSACTION` 采购基线脚本。
|
||||
- `lsfx-mock-server/tests/test_file_service.py`: Mock 规则命中计划测试。
|
||||
- `lsfx-mock-server/tests/test_statement_service.py`: Mock 样本装配与缓存稳定性测试。
|
||||
- `lsfx-mock-server/tests/integration/test_full_workflow.py`: Mock 端到端链路测试。
|
||||
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java`: 规则参数映射测试。
|
||||
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java`: 真实 SQL 结构测试。
|
||||
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java`: 规则分发测试。
|
||||
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java`: 风险人数刷新回归测试。
|
||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`: 拉取本行信息接口。
|
||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java`: 手动重算接口。
|
||||
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java`: 流水详情接口。
|
||||
- `bin/restart_java_backend.sh`: 后端 Jar 启停脚本。
|
||||
|
||||
### Task 1: 锁定执行环境、目标项目和记录文档
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/reports/implementation/2026-03-20-bank-tag-new-model-validation-record.md`
|
||||
- Create: `docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md`
|
||||
- Reference: `docs/tests/plans/2026-03-20-bank-tag-new-model-validation-test-plan.md`
|
||||
- Reference: `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- Reference: `docs/reports/implementation/2026-03-20-lsfx-mock-random-hit-rule-backend-record.md`
|
||||
- Reference: `docs/reports/implementation/2026-03-20-bank-tag-real-rule-phase1-backend-record.md`
|
||||
|
||||
- [ ] **Step 1: 阅读既有计划与两份来源实施记录**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
sed -n '1,220p' docs/tests/plans/2026-03-20-bank-tag-new-model-validation-test-plan.md
|
||||
sed -n '1,220p' docs/reports/implementation/2026-03-20-lsfx-mock-random-hit-rule-backend-record.md
|
||||
sed -n '1,220p' docs/reports/implementation/2026-03-20-bank-tag-real-rule-phase1-backend-record.md
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 明确本次只验证双线已落地内容
|
||||
- 不把第二期规则和修复方案带进执行范围
|
||||
|
||||
- [ ] **Step 2: 从数据库中选出本次端到端验证使用的项目**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import pymysql, re
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
with conn, conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT project_id, project_name, lsfx_project_id, config_type
|
||||
FROM ccdi_project
|
||||
WHERE del_flag = '0'
|
||||
AND lsfx_project_id IS NOT NULL
|
||||
ORDER BY update_time DESC, project_id DESC
|
||||
LIMIT 10
|
||||
""")
|
||||
for row in cursor.fetchall():
|
||||
print(row)
|
||||
PY
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 至少找到 1 个可用于 LSFX 联调的项目
|
||||
- 在实施记录和验证记录中记下最终采用的 `project_id`
|
||||
|
||||
- [ ] **Step 3: 创建本次实施记录与验证记录骨架**
|
||||
|
||||
在两个文档中先写入以下固定章节:
|
||||
|
||||
- 实施记录:验证目标、范围、执行阶段、产物路径
|
||||
- 验证记录:执行命令、数据库核验、接口验证、结论、环境清理
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/reports/implementation/2026-03-20-bank-tag-new-model-validation-record.md docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md docs/tests/plans/2026-03-20-bank-tag-new-model-validation-execution-plan.md
|
||||
git commit -m "补充新增模型打标验证执行计划"
|
||||
```
|
||||
|
||||
### Task 2: 先完成 Mock 随机命中自动化回归
|
||||
|
||||
**Files:**
|
||||
- Reference: `lsfx-mock-server/tests/test_file_service.py`
|
||||
- Reference: `lsfx-mock-server/tests/test_statement_service.py`
|
||||
- Reference: `lsfx-mock-server/tests/integration/test_full_workflow.py`
|
||||
- Modify: `docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md`
|
||||
- Modify: `docs/reports/implementation/2026-03-20-bank-tag-new-model-validation-record.md`
|
||||
|
||||
- [ ] **Step 1: 跑 Mock 聚焦回归**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd lsfx-mock-server
|
||||
python3 -m pytest tests/test_file_service.py -k "rule_hit_plan or persist_rule_hit_plan" -v
|
||||
python3 -m pytest tests/test_statement_service.py -k "rule_plan_should_only_include or withdraw_cnt_samples" -v
|
||||
python3 -m pytest tests/test_statement_service.py -k "follow_rule_hit_plan or fixed_total_count_200 or cached_result" -v
|
||||
python3 -m pytest tests/integration/test_full_workflow.py -k "same_rule_subset or share_same_primary_binding" -v
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 全部 `PASS`
|
||||
- 能证明规则命中计划、样本装配和缓存稳定性未回退
|
||||
|
||||
- [ ] **Step 2: 跑 Mock 全量回归**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd lsfx-mock-server
|
||||
python3 -m pytest tests/test_file_service.py tests/test_statement_service.py tests/test_api.py tests/integration/test_full_workflow.py -v
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `PASS`
|
||||
- 若失败,停止后续阶段并把失败用例和首个错误栈写入验证记录
|
||||
|
||||
- [ ] **Step 3: 将结果写入验证记录**
|
||||
|
||||
记录:
|
||||
|
||||
- 实际执行时间
|
||||
- `passed / failed / warnings` 摘要
|
||||
- 若存在 warning,只说明是否为既有 warning,不做修复
|
||||
|
||||
### Task 3: 完成主工程第一期真实规则自动化回归
|
||||
|
||||
**Files:**
|
||||
- Reference: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java`
|
||||
- Reference: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java`
|
||||
- Reference: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java`
|
||||
- Reference: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java`
|
||||
- Modify: `docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md`
|
||||
|
||||
- [ ] **Step 1: 逐步跑第一期目标测试**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mvn test -pl ccdi-project -Dtest=BankTagRuleConfigResolverTest
|
||||
mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest
|
||||
mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest,CcdiBankTagServiceImplTest
|
||||
mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest,BankTagRuleConfigResolverTest,CcdiBankTagServiceImplTest,CcdiBankTagServiceRiskCountRefreshTest
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 全部 `BUILD SUCCESS`
|
||||
- 若任一命令失败,停止后续阶段并把失败命令、失败类名和错误摘要写入验证记录
|
||||
|
||||
- [ ] **Step 2: 在验证记录中沉淀主工程自动化结果**
|
||||
|
||||
记录:
|
||||
|
||||
- 每条命令的执行结果
|
||||
- 规则映射、真实 SQL、对象分发、风险人数刷新是否都保持通过
|
||||
|
||||
### Task 4: 做数据库基线和规则元数据核验
|
||||
|
||||
**Files:**
|
||||
- Reference: `sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql`
|
||||
- Reference: `sql/2026-03-16-bank-tagging.sql`
|
||||
- Modify: `docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md`
|
||||
- Modify: `docs/reports/implementation/2026-03-20-bank-tag-new-model-validation-record.md`
|
||||
|
||||
- [ ] **Step 1: 幂等执行采购基线脚本**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bin/mysql_utf8_exec.sh sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 脚本执行成功
|
||||
- 无中文乱码、无 SQL 报错
|
||||
|
||||
- [ ] **Step 2: 校验采购基线记录存在**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import pymysql, re
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
with conn, conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT purchase_id, actual_amount, supplier_name
|
||||
FROM ccdi_purchase_transaction
|
||||
WHERE purchase_id = 'LSFXMOCKPUR001'
|
||||
AND actual_amount > 100000
|
||||
""")
|
||||
print(cursor.fetchone())
|
||||
PY
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 返回 `LSFXMOCKPUR001`
|
||||
- `actual_amount` 大于 `100000`
|
||||
|
||||
- [ ] **Step 3: 校验第一期规则元数据保持大写且可识别**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import pymysql, re
|
||||
|
||||
TARGET_RULES = (
|
||||
'GAMBLING_SENSITIVE_KEYWORD','SPECIAL_AMOUNT_TRANSACTION','SUSPICIOUS_INCOME_KEYWORD',
|
||||
'FOREX_BUY_AMT','FOREX_SELL_AMT','LARGE_PURCHASE_TRANSACTION',
|
||||
'STOCK_TFR_LARGE','WITHDRAW_CNT','LARGE_STOCK_TRADING'
|
||||
)
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
sql = f"""
|
||||
SELECT model_code, rule_code, indicator_code
|
||||
FROM ccdi_bank_tag_rule
|
||||
WHERE rule_code IN ({','.join(['%s'] * len(TARGET_RULES))})
|
||||
ORDER BY model_code, sort_order, rule_code
|
||||
"""
|
||||
with conn, conn.cursor() as cursor:
|
||||
cursor.execute(sql, TARGET_RULES)
|
||||
for row in cursor.fetchall():
|
||||
print(row)
|
||||
PY
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 所有目标规则均能查到
|
||||
- `rule_code`、`indicator_code` 继续保持全大写风格
|
||||
|
||||
- [ ] **Step 4: 将 SQL 与结果摘要写入验证记录**
|
||||
|
||||
记录:
|
||||
|
||||
- 执行的 SQL 或脚本命令
|
||||
- 返回结果摘要
|
||||
- 若数据缺失,明确归类为“数据基线异常”
|
||||
|
||||
### Task 5: 执行接口端到端验证并清理进程
|
||||
|
||||
**Files:**
|
||||
- Reference: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
|
||||
- Reference: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java`
|
||||
- Reference: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java`
|
||||
- Modify: `docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md`
|
||||
- Modify: `docs/reports/implementation/2026-03-20-bank-tag-new-model-validation-record.md`
|
||||
|
||||
- [ ] **Step 1: 启动 Mock 服务**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
mkdir -p logs
|
||||
cd lsfx-mock-server
|
||||
nohup python3 main.py > ../logs/lsfx-mock-validation.log 2>&1 &
|
||||
echo $! > ../logs/lsfx-mock-validation.pid
|
||||
cd ..
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- `logs/lsfx-mock-validation.pid` 已生成
|
||||
- `http://localhost:8000/docs` 可访问
|
||||
|
||||
- [ ] **Step 2: 启动后端 Jar 服务**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
./bin/restart_java_backend.sh stop
|
||||
nohup ./bin/restart_java_backend.sh start > logs/backend-validation.log 2>&1 &
|
||||
echo $! > logs/backend-validation.pid
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 后端监听在 `http://localhost:62318`
|
||||
- 若启动失败,先看 `logs/backend-console.log`,记录失败后停止执行
|
||||
|
||||
- [ ] **Step 3: 登录并取 token**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:62318/login/test \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"admin123"}' | tee /tmp/bank-tag-login.json
|
||||
jq -r '.token' /tmp/bank-tag-login.json
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 返回非空 token
|
||||
|
||||
- [ ] **Step 4: 触发拉取本行信息链路生成新的 Mock 流水**
|
||||
|
||||
先从数据库挑 1-3 个真实身份证号,再调用接口:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY' >/tmp/bank-tag-id-cards.json
|
||||
from pathlib import Path
|
||||
import json, pymysql, re
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
with conn, conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT id_card
|
||||
FROM ccdi_base_staff
|
||||
WHERE del_flag = '0'
|
||||
AND id_card IS NOT NULL
|
||||
AND id_card <> ''
|
||||
ORDER BY id ASC
|
||||
LIMIT 3
|
||||
""")
|
||||
print(json.dumps([row['id_card'] for row in cursor.fetchall()], ensure_ascii=False))
|
||||
PY
|
||||
|
||||
TOKEN=$(jq -r '.token' /tmp/bank-tag-login.json)
|
||||
PROJECT_ID=<把 Task 1 选定的 project_id 填到这里>
|
||||
curl -s http://localhost:62318/ccdi/file-upload/pull-bank-info \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"projectId\":${PROJECT_ID},\"idCards\":$(cat /tmp/bank-tag-id-cards.json),\"startDate\":\"2026-03-01\",\"endDate\":\"2026-03-20\"}"
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 返回 `拉取任务已提交`
|
||||
- 说明 Mock 随机命中链路已被主工程实际调用
|
||||
|
||||
- [ ] **Step 5: 触发整项目手动重算**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
TOKEN=$(jq -r '.token' /tmp/bank-tag-login.json)
|
||||
curl -s http://localhost:62318/ccdi/project/tags/rebuild \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"projectId\":${PROJECT_ID},\"modelCode\":null}"
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 返回 `{"code":200,...}`
|
||||
- 手动重算任务成功提交
|
||||
|
||||
- [ ] **Step 6: 轮询数据库确认重算任务成功,并查出一条新增模型命中记录**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import pymysql, re, time
|
||||
|
||||
PROJECT_ID = int("<把 Task 1 选定的 project_id 填到这里>")
|
||||
TARGET_RULES = (
|
||||
'GAMBLING_SENSITIVE_KEYWORD','SPECIAL_AMOUNT_TRANSACTION','SUSPICIOUS_INCOME_KEYWORD',
|
||||
'FOREX_BUY_AMT','FOREX_SELL_AMT','LARGE_PURCHASE_TRANSACTION',
|
||||
'STOCK_TFR_LARGE','WITHDRAW_CNT','LARGE_STOCK_TRADING'
|
||||
)
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
with conn, conn.cursor() as cursor:
|
||||
task = None
|
||||
for _ in range(30):
|
||||
cursor.execute("""
|
||||
SELECT id, status, model_code, hit_count, success_rule_count, failed_rule_count
|
||||
FROM ccdi_bank_tag_task
|
||||
WHERE project_id = %s
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
""", (PROJECT_ID,))
|
||||
task = cursor.fetchone()
|
||||
print(task)
|
||||
if task and task['status'] == 'SUCCESS':
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT id, rule_code, bank_statement_id, object_key, reason_detail
|
||||
FROM ccdi_bank_statement_tag_result
|
||||
WHERE project_id = %s
|
||||
AND rule_code IN ({','.join(['%s'] * len(TARGET_RULES))})
|
||||
ORDER BY id DESC
|
||||
LIMIT 10
|
||||
""", (PROJECT_ID, *TARGET_RULES))
|
||||
for row in cursor.fetchall():
|
||||
print(row)
|
||||
PY
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 最新任务状态为 `SUCCESS`
|
||||
- 至少查到 1 条目标规则命中结果
|
||||
|
||||
- [ ] **Step 7: 用命中的 `bank_statement_id` 回查接口详情**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
TOKEN=$(jq -r '.token' /tmp/bank-tag-login.json)
|
||||
BANK_STATEMENT_ID=<把 Step 6 查到的 bank_statement_id 填到这里>
|
||||
curl -s "http://localhost:62318/ccdi/project/bank-statement/detail/${BANK_STATEMENT_ID}" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- 返回 `code = 200`
|
||||
- `data.hitTags` 中能看到至少 1 个目标规则对应的命中标签
|
||||
|
||||
- [ ] **Step 8: 写结论并关闭本次启动的进程**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
if [ -f logs/lsfx-mock-validation.pid ]; then kill "$(cat logs/lsfx-mock-validation.pid)" || true; rm -f logs/lsfx-mock-validation.pid; fi
|
||||
./bin/restart_java_backend.sh stop || true
|
||||
rm -f logs/backend-validation.pid
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- Mock 进程已关闭
|
||||
- 后端 Jar 进程已关闭
|
||||
- 验证记录中明确写出“已完成进程清理”
|
||||
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/reports/implementation/2026-03-20-bank-tag-new-model-validation-record.md docs/tests/records/2026-03-20-bank-tag-new-model-validation-verification.md docs/tests/plans/2026-03-20-bank-tag-new-model-validation-execution-plan.md
|
||||
git commit -m "补充新增模型打标完整验证记录"
|
||||
```
|
||||
@@ -0,0 +1,244 @@
|
||||
# 新增模型打标完整验证计划
|
||||
|
||||
## 背景
|
||||
|
||||
2026-03-20 已落地两条与“新增模型打标”直接相关的后端改动:
|
||||
|
||||
- `lsfx-mock-server` 新增按 `logId` 稳定随机命中的规则计划,用于为联调链路提供可重复、可命中的 Mock 流水样本。
|
||||
- 主工程 `ccdi-project` 已接通第一期真实规则打标链路,覆盖明细型规则、对象型规则、参数映射和真实 SQL 分发。
|
||||
|
||||
当前需要做的不是继续扩展规则,而是对这两条改动线做一轮完整验证,确认“新加入的模型”在 Mock 样本层、真实规则层和最终接口链路层都能正确打标。
|
||||
|
||||
## 目标
|
||||
|
||||
- 同时验证 `lsfx-mock-server` 随机命中规则与主工程第一期真实规则。
|
||||
- 验证深度覆盖自动化测试、数据库核验和接口端到端调用。
|
||||
- 若发现打标异常,只输出验证结论与问题清单,不进入修复。
|
||||
|
||||
## 范围
|
||||
|
||||
### In Scope
|
||||
|
||||
- `lsfx-mock-server` 中与随机命中规则计划、样本拼装、缓存稳定性相关的验证。
|
||||
- `ccdi-project` 中第一期真实规则相关的参数映射、真实 SQL、Service 分发与风险人数刷新链路验证。
|
||||
- 关键数据库基线与元数据核验。
|
||||
- 实际接口调用后的打标结果核验。
|
||||
- 本次验证对应的实施记录与验证记录沉淀。
|
||||
|
||||
### Out of Scope
|
||||
|
||||
- 第二期仍为占位状态的规则。
|
||||
- 新增规则、补丁逻辑、兜底逻辑或兼容性改造。
|
||||
- 任何修复实现与代码修改方案。
|
||||
|
||||
## 验证对象
|
||||
|
||||
本次只验证已在既有实施记录中明确落地的内容,不扩展额外模型。
|
||||
|
||||
### 1. Mock 随机命中链路
|
||||
|
||||
关注以下能力是否仍成立:
|
||||
|
||||
- `FileService` 能为新 `logId` 写入稳定随机的规则命中计划。
|
||||
- `StatementService` 能按命中计划拼装对应样本,而不是回退到全量样本。
|
||||
- 同一 `logId` 重复读取时,规则命中子集和分页结果保持稳定。
|
||||
- `LARGE_PURCHASE_TRANSACTION` 对应的采购基线数据已通过独立 SQL 提供。
|
||||
|
||||
### 2. 主工程第一期真实规则链路
|
||||
|
||||
关注以下规则在现有打标链路中是否仍能按真实规则命中:
|
||||
|
||||
- `GAMBLING_SENSITIVE_KEYWORD`
|
||||
- `SPECIAL_AMOUNT_TRANSACTION`
|
||||
- `SUSPICIOUS_INCOME_KEYWORD`
|
||||
- `FOREX_BUY_AMT`
|
||||
- `FOREX_SELL_AMT`
|
||||
- `LARGE_PURCHASE_TRANSACTION`
|
||||
- `STOCK_TFR_LARGE`
|
||||
- `WITHDRAW_CNT`
|
||||
- `LARGE_STOCK_TRADING`
|
||||
|
||||
重点关注点:
|
||||
|
||||
- 参数编码与规则编码保持全大写。
|
||||
- 阈值规则仍正确透传到 Mapper。
|
||||
- `WITHDRAW_CNT` 对象型规则仍保持“命中为空不回退任务状态”的链路约束。
|
||||
- `LARGE_PURCHASE_TRANSACTION` 仍走采购交易表数据源,不伪造银行流水替代。
|
||||
|
||||
## 方案对比
|
||||
|
||||
### 方案一:分三层顺序验证
|
||||
|
||||
按 `Mock 自动化 -> 主工程自动化 -> 数据库核验 -> 接口端到端` 顺序执行。
|
||||
|
||||
优点:
|
||||
|
||||
- 最容易定位问题落点。
|
||||
- 与当前仓库已有测试资产和实施记录天然对齐。
|
||||
- 能覆盖你要求的完整验证深度。
|
||||
|
||||
缺点:
|
||||
|
||||
- 执行步骤最多。
|
||||
|
||||
### 方案二:纯端到端驱动
|
||||
|
||||
直接起服务并调用接口,看最终打标结果。
|
||||
|
||||
优点:
|
||||
|
||||
- 离业务使用最近。
|
||||
|
||||
缺点:
|
||||
|
||||
- 一旦失败,难以快速分辨是 Mock 样本、真实 SQL、参数分发还是接口编排问题。
|
||||
|
||||
### 方案三:自动化为主,接口抽样补充
|
||||
|
||||
优点:
|
||||
|
||||
- 执行更快。
|
||||
|
||||
缺点:
|
||||
|
||||
- 对数据库事实和最终链路覆盖不足,不满足本次“完整验证”的要求。
|
||||
|
||||
## 推荐方案
|
||||
|
||||
采用方案一。
|
||||
|
||||
原因是当前需求不是只看“有没有结果”,而是要同时确认:
|
||||
|
||||
- Mock 样本能不能正确提供命中前提;
|
||||
- 主工程真实规则能不能正确识别;
|
||||
- 最终接口链路有没有把命中结果正确暴露出来。
|
||||
|
||||
分层验证能把这三层责任拆开,失败时也能严格停在结论和问题清单,不会直接滑向修复。
|
||||
|
||||
## 验证方案
|
||||
|
||||
### 一、环境与基线确认
|
||||
|
||||
先确认本次验证依赖的数据与环境处于可验证状态:
|
||||
|
||||
- 主工程数据库连接可用。
|
||||
- `sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql` 已执行或可幂等重跑。
|
||||
- 采购基线记录 `LSFXMOCKPUR001` 存在,且 `actual_amount > 100000`。
|
||||
|
||||
这一阶段的目标是确认“验证素材存在”,不直接下结论说模型已命中。
|
||||
|
||||
### 二、Mock 随机命中自动化验证
|
||||
|
||||
执行现有 `lsfx-mock-server` pytest 资产,覆盖四类能力:
|
||||
|
||||
- 规则命中计划生成;
|
||||
- 命中计划持久化;
|
||||
- 按规则子集拼装样本;
|
||||
- 同一 `logId` 下缓存和分页稳定性;
|
||||
- 端到端集成链路。
|
||||
|
||||
这一阶段通过后,才能认为 Mock 服务仍在稳定提供“可命中的输入数据”。
|
||||
|
||||
### 三、主工程第一期真实规则自动化验证
|
||||
|
||||
执行 `ccdi-project` 中与第一期真实规则直接相关的 Maven 测试,覆盖:
|
||||
|
||||
- 规则参数映射;
|
||||
- XML 真实 SQL;
|
||||
- Service 分发;
|
||||
- 风险人数刷新链路。
|
||||
|
||||
这一阶段通过后,才能认为主工程内部的规则识别与分发逻辑没有回退。
|
||||
|
||||
### 四、数据库关键事实核验
|
||||
|
||||
自动化通过后,再直接核验数据库事实,避免只依赖测试断言:
|
||||
|
||||
- 采购基线记录存在且满足门槛。
|
||||
- 第一期开启真实规则的元数据仍与预期一致。
|
||||
- 规则编码、参数编码、指标编码继续保持全大写。
|
||||
- 端到端验证依赖的关键项目、流水、采购或对象数据具备最小命中条件。
|
||||
|
||||
这一阶段的结论是“数据前提是否成立”,不是替代接口结果。
|
||||
|
||||
### 五、接口端到端打标验证
|
||||
|
||||
启动本次验证需要的最小服务集合,按真实链路执行一次完整调用:
|
||||
|
||||
- 触发 Mock 取数或上传链路;
|
||||
- 触发主工程打标分析链路;
|
||||
- 查询最终模型或标签结果;
|
||||
- 对照数据库事实与规则预期,确认新增模型是否真正出现在结果中。
|
||||
|
||||
这里的判定标准不是仅返回 HTTP 200,而是最终打标结果中是否包含预期命中的新增模型规则。
|
||||
|
||||
## 通过标准
|
||||
|
||||
只有同时满足以下三层条件,才视为本次验证通过:
|
||||
|
||||
- 自动化层:相关 pytest 与 Maven 测试全部通过。
|
||||
- 数据层:关键基线与元数据查询结果符合预期。
|
||||
- 接口层:最终接口返回中包含预期命中的新增模型规则。
|
||||
|
||||
只要任一层不满足,就记为失败,并停止给出“验证通过”的结论。
|
||||
|
||||
## 失败判定与停点
|
||||
|
||||
### 自动化失败
|
||||
|
||||
任一既有 pytest 或 Maven 目标失败,记为:
|
||||
|
||||
- 代码级回归;或
|
||||
- 环境级阻塞。
|
||||
|
||||
此时记录失败命令、失败用例和首个错误点,不继续放大为“模型已失败命中”的业务结论。
|
||||
|
||||
### 数据核验失败
|
||||
|
||||
若自动化通过,但基线数据不存在、门槛不满足或元数据不一致,记为:
|
||||
|
||||
- 验证数据不足;或
|
||||
- 数据基线异常。
|
||||
|
||||
此时停止进入最终通过判定。
|
||||
|
||||
### 接口链路失败
|
||||
|
||||
若服务能启动、接口能调用,但最终结果缺失预期命中项,记为:
|
||||
|
||||
- 链路级打标异常。
|
||||
|
||||
输出接口请求、响应摘要、相关数据库证据和可疑断点位置,但不进入修复。
|
||||
|
||||
## 记录与产物
|
||||
|
||||
本次验证至少沉淀两类文档:
|
||||
|
||||
- `docs/reports/implementation/` 下新增本次实施记录,说明验证执行内容与调整范围。
|
||||
- `docs/tests/records/` 下新增本次验证记录,说明执行命令、核验 SQL、接口结果与结论。
|
||||
|
||||
文档内容固定包含:
|
||||
|
||||
- 验证目标与范围;
|
||||
- 执行命令;
|
||||
- 数据库核验 SQL 与结果摘要;
|
||||
- 接口端到端步骤与结果摘要;
|
||||
- 最终结论;
|
||||
- 若失败则输出问题清单,不包含修复动作。
|
||||
|
||||
## 进程管理
|
||||
|
||||
若本次验证启动了 Java 后端、前端或 Mock 服务进程,验证结束后必须主动关闭,并在验证记录中写明已完成清理,避免残留端口占用。
|
||||
|
||||
## 风险与边界
|
||||
|
||||
- Mock 随机命中只保证“稳定随机子集”,不保证每个 `logId` 全量命中所有规则。
|
||||
- `LARGE_PURCHASE_TRANSACTION` 的命中依赖采购表基线,不应误判为银行流水样本问题。
|
||||
- 对象型规则 `WITHDRAW_CNT` 的结论需要和明细型规则区分,避免用相同口径判断失败。
|
||||
- 本次验证只为确认现状是否正确,不引申为修复方案或二期规则推进。
|
||||
|
||||
## 结论
|
||||
|
||||
本计划采用分层完整验证方案,对 2026-03-20 新加入的模型打标改动做统一校验。
|
||||
|
||||
执行时先验证 Mock 规则输入,再验证主工程真实规则识别,最后验证数据库事实和接口结果是否闭环一致。若任何一层失败,只输出证据和问题清单,不进入代码修复。
|
||||
@@ -0,0 +1,224 @@
|
||||
# 新增模型打标完整验证记录
|
||||
|
||||
## 执行命令
|
||||
```bash
|
||||
cd lsfx-mock-server
|
||||
python3 -m pytest tests/test_file_service.py -k "rule_hit_plan or persist_rule_hit_plan" -v
|
||||
python3 -m pytest tests/test_statement_service.py -k "rule_plan_should_only_include or withdraw_cnt_samples" -v
|
||||
python3 -m pytest tests/test_statement_service.py -k "follow_rule_hit_plan or fixed_total_count_200 or cached_result" -v
|
||||
python3 -m pytest tests/integration/test_full_workflow.py -k "same_rule_subset or share_same_primary_binding" -v
|
||||
python3 -m pytest tests/test_file_service.py tests/test_statement_service.py tests/test_api.py tests/integration/test_full_workflow.py -v
|
||||
|
||||
cd ..
|
||||
mvn test -pl ccdi-project -Dtest=BankTagRuleConfigResolverTest
|
||||
mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest
|
||||
mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest,CcdiBankTagServiceImplTest
|
||||
mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest,BankTagRuleConfigResolverTest,CcdiBankTagServiceImplTest,CcdiBankTagServiceRiskCountRefreshTest
|
||||
```
|
||||
|
||||
## Mock 自动化结果
|
||||
- 2026-03-20 15:21:54 CST 完成 Mock 聚焦回归与全量回归。
|
||||
- 聚焦回归结果:
|
||||
- `tests/test_file_service.py -k "rule_hit_plan or persist_rule_hit_plan"`: `2 passed, 4 deselected, 1 warning`
|
||||
- `tests/test_statement_service.py -k "rule_plan_should_only_include or withdraw_cnt_samples"`: `2 passed, 11 deselected, 1 warning`
|
||||
- `tests/test_statement_service.py -k "follow_rule_hit_plan or fixed_total_count_200 or cached_result"`: `3 passed, 10 deselected, 1 warning`
|
||||
- `tests/integration/test_full_workflow.py -k "same_rule_subset or share_same_primary_binding"`: `2 passed, 3 deselected, 3 warnings`
|
||||
- 全量回归结果:
|
||||
- `python3 -m pytest tests/test_file_service.py tests/test_statement_service.py tests/test_api.py tests/integration/test_full_workflow.py -v`
|
||||
- 摘要:`38 passed, 20 warnings in 4.15s`
|
||||
- warning 摘要:
|
||||
- `pydantic` 的 class-based config 弃用提示仍存在。
|
||||
- `httpx` 的 `app` shortcut 弃用提示仍存在。
|
||||
- 两类 warning 与既有 Mock 验证记录一致,本次未新增 failure 或 error。
|
||||
|
||||
## 主工程自动化结果
|
||||
- 2026-03-20 15:22:27 CST 执行 `mvn test -pl ccdi-project -Dtest=BankTagRuleConfigResolverTest`,结果 `BUILD SUCCESS`,`Tests run: 6, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
- 2026-03-20 15:22:47 CST 执行 `mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest`,结果 `BUILD SUCCESS`,`Tests run: 8, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
- 2026-03-20 15:22:57 CST 执行 `mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest,CcdiBankTagServiceImplTest`,结果 `BUILD SUCCESS`,`Tests run: 19, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
- 2026-03-20 15:23:10 CST 执行 `mvn test -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest,BankTagRuleConfigResolverTest,CcdiBankTagServiceImplTest,CcdiBankTagServiceRiskCountRefreshTest`,结果 `BUILD SUCCESS`,`Tests run: 27, Failures: 0, Errors: 0, Skipped: 0`。
|
||||
- 结果归纳:
|
||||
- `BankTagRuleConfigResolverTest` 证明第一期规则参数映射保持通过。
|
||||
- `CcdiBankTagAnalysisMapperXmlTest` 证明真实 SQL 结构保持通过。
|
||||
- `CcdiBankTagServiceImplTest` 证明规则分发和异常路径断言保持通过。
|
||||
- `CcdiBankTagServiceRiskCountRefreshTest` 证明风险人数刷新链路保持通过。
|
||||
- 日志说明:
|
||||
- 测试日志中的 `threshold missing` 与 `refresh failed` 为异常路径断言场景产生的预期日志,不代表本轮 Maven 回归失败。
|
||||
|
||||
## 数据库核验
|
||||
```bash
|
||||
bin/mysql_utf8_exec.sh sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import pymysql, re
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
with conn, conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT purchase_id, actual_amount, supplier_name
|
||||
FROM ccdi_purchase_transaction
|
||||
WHERE purchase_id = 'LSFXMOCKPUR001'
|
||||
AND actual_amount > 100000
|
||||
""")
|
||||
print(cursor.fetchone())
|
||||
PY
|
||||
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import pymysql, re
|
||||
|
||||
TARGET_RULES = (
|
||||
'GAMBLING_SENSITIVE_KEYWORD','SPECIAL_AMOUNT_TRANSACTION','SUSPICIOUS_INCOME_KEYWORD',
|
||||
'FOREX_BUY_AMT','FOREX_SELL_AMT','LARGE_PURCHASE_TRANSACTION',
|
||||
'STOCK_TFR_LARGE','WITHDRAW_CNT','LARGE_STOCK_TRADING'
|
||||
)
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
sql = f"""
|
||||
SELECT model_code, rule_code, indicator_code
|
||||
FROM ccdi_bank_tag_rule
|
||||
WHERE rule_code IN ({','.join(['%s'] * len(TARGET_RULES))})
|
||||
ORDER BY model_code, sort_order, rule_code
|
||||
"""
|
||||
with conn, conn.cursor() as cursor:
|
||||
cursor.execute(sql, TARGET_RULES)
|
||||
for row in cursor.fetchall():
|
||||
print(row)
|
||||
PY
|
||||
```
|
||||
|
||||
- 采购基线脚本执行结果:
|
||||
- `bin/mysql_utf8_exec.sh sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql` 执行成功,无报错、无乱码输出。
|
||||
- 采购基线查询结果:
|
||||
- 返回 `{'purchase_id': 'LSFXMOCKPUR001', 'actual_amount': Decimal('186000.00'), 'supplier_name': '兰溪市联调供应链有限公司'}`
|
||||
- 结论:`LSFXMOCKPUR001` 存在,且 `actual_amount > 100000`,采购基线正常。
|
||||
- 规则元数据查询结果:
|
||||
- 共查询到 9 条目标规则,`rule_code` 均存在。
|
||||
- 返回摘要:
|
||||
- `STOCK_TFR_LARGE -> indicator_code=STOCK_TFR_LARGE`
|
||||
- `WITHDRAW_CNT -> indicator_code=WITHDRAW_CNT`
|
||||
- `LARGE_STOCK_TRADING -> indicator_code=NULL`
|
||||
- `FOREX_BUY_AMT -> indicator_code=FOREX_BUY_AMT`
|
||||
- `FOREX_SELL_AMT -> indicator_code=FOREX_SELL_AMT`
|
||||
- 其余 4 条规则 `indicator_code=NULL`
|
||||
- 异常判定:
|
||||
- 根据既有实施记录,`FOREX_BUY_AMT` 预期应对齐为 `SINGLE_PURCHASE_AMOUNT`。
|
||||
- `FOREX_SELL_AMT` 预期应对齐为 `SINGLE_SETTLEMENT_AMOUNT`。
|
||||
- `LARGE_STOCK_TRADING` 预期应对齐为 `STOCK_TFR_LARGE`,当前查询为 `NULL`。
|
||||
- 首次执行因此在数据库层判定为“数据基线异常”。
|
||||
- 修复后复验:
|
||||
- 已执行 `bin/mysql_utf8_exec.sh sql/migration/2026-03-20-sync-bank-tag-phase1-rule-metadata.sql`
|
||||
- 修复后查询结果:
|
||||
- `FOREX_BUY_AMT -> indicator_code=SINGLE_PURCHASE_AMOUNT`
|
||||
- `FOREX_SELL_AMT -> indicator_code=SINGLE_SETTLEMENT_AMOUNT`
|
||||
- `LARGE_STOCK_TRADING -> indicator_code=STOCK_TFR_LARGE`
|
||||
- 9 条一期真实规则 `remark` 均已同步为真实规则说明
|
||||
- 结论:数据库元数据异常已修复,可继续进入接口端到端验证。
|
||||
|
||||
## 接口验证
|
||||
```bash
|
||||
curl -s http://localhost:62318/login/test \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"admin","password":"admin123"}'
|
||||
|
||||
python3 - <<'PY'
|
||||
# 读取 3 个有效身份证号并生成 /tmp/bank-tag-pull-request.json
|
||||
PY
|
||||
|
||||
curl -s http://localhost:62318/ccdi/file-upload/pull-bank-info \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data-binary @/tmp/bank-tag-pull-request.json
|
||||
|
||||
curl -s http://localhost:62318/ccdi/project/tags/rebuild \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"projectId":47,"modelCode":null}'
|
||||
|
||||
python3 - <<'PY'
|
||||
# 轮询 ccdi_bank_tag_task 并查询目标规则命中结果
|
||||
PY
|
||||
|
||||
curl -s "http://localhost:62318/ccdi/project/bank-statement/detail/66679" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
- 登录结果:
|
||||
- 返回 `code=200`,token 非空。
|
||||
- 拉取本行信息结果:
|
||||
- 选择身份证号:`558455197203132040`、`523342199111246421`、`38056420050404632X`
|
||||
- 接口返回 `{"msg":"拉取任务已提交","code":200,...}`
|
||||
- 自动触发任务 `id=36`,`trigger_type=AUTO_PULL_BANK_INFO`,状态 `SUCCESS`。
|
||||
- 手动重算结果:
|
||||
- 首次调用命中项目级重算锁,返回“当前项目标签正在重算中,请稍后再试”。
|
||||
- 自动拉取任务完成后再次调用,返回 `{"msg":"标签重算任务已提交","code":200}`。
|
||||
- 最新任务 `id=37`,状态 `SUCCESS`,`hit_count=3481`,`success_rule_count=33`,`failed_rule_count=0`。
|
||||
- 命中结果查询:
|
||||
- 已查到目标规则命中,包括:
|
||||
- `WITHDRAW_CNT`
|
||||
- `GAMBLING_SENSITIVE_KEYWORD`
|
||||
- `LARGE_PURCHASE_TRANSACTION`
|
||||
- 样例明细:
|
||||
- `rule_code=GAMBLING_SENSITIVE_KEYWORD`
|
||||
- `bank_statement_id=66679`
|
||||
- `reason_detail=摘要/对手命中赌博敏感词,摘要“游戏充值”,对手方“欢乐游戏科技有限公司”,支出金额 6888.00 元`
|
||||
- 详情接口回查:
|
||||
- `GET /ccdi/project/bank-statement/detail/66679` 返回 `code=200`
|
||||
- `data.hitTags` 中包含 `GAMBLING_SENSITIVE_KEYWORD`
|
||||
|
||||
## 补充复验
|
||||
- 2026-03-20 16:01 左右,基于修复后的详情查询 SQL 再次执行项目 `47` 端到端链路验证。
|
||||
- 登录结果:
|
||||
- `POST /login/test` 返回 `code=200`,token 非空。
|
||||
- 拉取本行信息结果:
|
||||
- 仍使用身份证号 `558455197203132040`、`523342199111246421`、`38056420050404632X`
|
||||
- `POST /ccdi/file-upload/pull-bank-info` 返回 `{"msg":"拉取任务已提交","code":200,...}`
|
||||
- 自动触发任务 `id=39`,`trigger_type=AUTO_PULL_BANK_INFO`,状态 `SUCCESS`
|
||||
- `hit_count=3636`,`success_rule_count=33`,`failed_rule_count=0`
|
||||
- 手动重算结果:
|
||||
- `POST /ccdi/project/tags/rebuild` 直接返回 `{"msg":"标签重算任务已提交","code":200}`
|
||||
- 最新任务 `id=40`,`trigger_type=MANUAL`,状态 `SUCCESS`
|
||||
- `hit_count=3636`,`success_rule_count=33`,`failed_rule_count=0`
|
||||
- 命中样例回查:
|
||||
- 最新 `GAMBLING_SENSITIVE_KEYWORD` 命中样例为 `bank_statement_id=67279`
|
||||
- `reason_detail=摘要/对手命中赌博敏感词,摘要“游戏充值”,对手方“欢乐游戏科技有限公司”,支出金额 6888.00 元`
|
||||
- 详情接口回查:
|
||||
- `GET /ccdi/project/bank-statement/detail/67279` 返回 `code=200`
|
||||
- 返回结果包含 `originalFileName=558455197203132040_10001.csv`
|
||||
- `data.hitTags` 中包含 `GAMBLING_SENSITIVE_KEYWORD`
|
||||
|
||||
## 结论
|
||||
- 首次执行在数据库核验阶段发现第一期规则元数据异常,问题已定位并修复。
|
||||
- 修复后重新验证结果如下:
|
||||
- Mock 自动化回归通过。
|
||||
- 主工程第一期真实规则自动化回归通过。
|
||||
- 数据库采购基线与第一期规则元数据核验通过。
|
||||
- 项目 `47` 的自动拉取、手动重算、规则命中查询与详情接口回查通过。
|
||||
- 补充复验确认:重复上传记录场景下,流水详情接口已不再出现 `selectOne()` 结果重复异常。
|
||||
- 最终结论:本次“新增模型打标完整验证”在修复元数据缺口后已通过。
|
||||
|
||||
## 环境清理
|
||||
- 已停止本次复验启动的 Mock 服务与后端 Jar 服务。
|
||||
- 端口复核结果:
|
||||
- `62318` 无监听进程
|
||||
- `8000` 无监听进程
|
||||
@@ -0,0 +1,70 @@
|
||||
# LSFX Mock 随机命中规则后端验证记录
|
||||
|
||||
## 执行命令
|
||||
|
||||
```bash
|
||||
cd lsfx-mock-server
|
||||
python3 -m pytest tests/test_file_service.py -k "rule_hit_plan or persist_rule_hit_plan" -v
|
||||
python3 -m pytest tests/test_statement_service.py -k "rule_plan_should_only_include or withdraw_cnt_samples" -v
|
||||
python3 -m pytest tests/test_statement_service.py -k "follow_rule_hit_plan or fixed_total_count_200 or cached_result" -v
|
||||
python3 -m pytest tests/integration/test_full_workflow.py -k "same_rule_subset or share_same_primary_binding" -v
|
||||
python3 -m pytest tests/test_file_service.py tests/test_statement_service.py tests/test_api.py tests/integration/test_full_workflow.py -v
|
||||
|
||||
cd ..
|
||||
bin/mysql_utf8_exec.sh sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql
|
||||
python3 - <<'PY'
|
||||
import pymysql
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
text = Path('ruoyi-admin/src/main/resources/application-dev.yml').read_text(encoding='utf-8')
|
||||
match = re.search(r"url:\s*jdbc:mysql://(?P<host>[^:/?#]+):(?P<port>\d+)/(?P<db>[^?\n]+).*?\n\s*username:\s*(?P<user>[^\n]+)\n\s*password:\s*(?P<pwd>[^\n]+)", text, re.S)
|
||||
conn = pymysql.connect(
|
||||
host=match.group('host'),
|
||||
port=int(match.group('port')),
|
||||
user=match.group('user').strip(),
|
||||
password=match.group('pwd').strip(),
|
||||
database=match.group('db').strip(),
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
)
|
||||
with conn, conn.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT COUNT(1) AS cnt
|
||||
FROM ccdi_purchase_transaction
|
||||
WHERE purchase_id = 'LSFXMOCKPUR001'
|
||||
AND actual_amount > 100000
|
||||
""")
|
||||
print(cursor.fetchone()['cnt'])
|
||||
PY
|
||||
```
|
||||
|
||||
## 执行时间与结果
|
||||
- 2026-03-20 14:50 CST 完成目标回归:
|
||||
`python3 -m pytest tests/test_file_service.py tests/test_statement_service.py tests/test_api.py tests/integration/test_full_workflow.py -v`
|
||||
- 回归结果:`38 passed, 20 warnings in 4.20s`
|
||||
- warnings 为现有 `pydantic` 与 `httpx` 弃用提示,本次改动未新增失败或 error。
|
||||
|
||||
## SQL 基线脚本执行结果
|
||||
- 执行命令:`bin/mysql_utf8_exec.sh sql/migration/2026-03-20-lsfx-mock-random-hit-rule-purchase-baseline.sql`
|
||||
- 执行结果:脚本执行成功,无报错输出。
|
||||
- 脚本内容采用“先删后插”的幂等方式,避免重复执行造成脏数据。
|
||||
|
||||
## 采购基线查询结果
|
||||
- 执行前查询:
|
||||
`SELECT COUNT(1) FROM ccdi_purchase_transaction WHERE purchase_id = 'LSFXMOCKPUR001'`
|
||||
返回 `0`
|
||||
- 执行后查询:
|
||||
`SELECT purchase_id, actual_amount, supplier_name FROM ccdi_purchase_transaction WHERE purchase_id = 'LSFXMOCKPUR001'`
|
||||
返回:
|
||||
- `purchase_id = LSFXMOCKPUR001`
|
||||
- `actual_amount = 186000.00`
|
||||
- `supplier_name = 兰溪市联调供应链有限公司`
|
||||
- 最终门槛校验:
|
||||
`SELECT COUNT(1) ... WHERE purchase_id = 'LSFXMOCKPUR001' AND actual_amount > 100000`
|
||||
返回 `1`
|
||||
|
||||
## 是否发现回归
|
||||
- 未发现本次改动引入的功能回归。
|
||||
- `FileService` 的规则命中计划生成、`statement_rule_samples.py` 的按规则子集装配、`StatementService` 的缓存稳定性、端到端接口链路均已通过验证。
|
||||
- 本次验证未启动额外前后端常驻进程,因此无需执行进程清理。
|
||||
Reference in New Issue
Block a user