# 新增模型打标完整验证记录

## 执行命令
```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` 无监听进程
