# 新增模型打标完整验证 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 "补充新增模型打标完整验证记录"
```
