修复采购规则跨项目人员串入问题

This commit is contained in:
wkc
2026-03-23 15:11:05 +08:00
parent 15d17e4175
commit 63d8904d01
5 changed files with 659 additions and 12 deletions

View File

@@ -30,6 +30,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
NULL AS reasonDetail
</sql>
<sql id="projectScopedDirectStaffSql">
select distinct
cast(staff.staff_id as char) as staffId,
staff.id_card as idCard
from ccdi_bank_statement bs
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
where bs.project_id = #{projectId}
and bs.cret_no is not null
and trim(bs.cret_no) != ''
</sql>
<sql id="cashDepositPredicate">
(
(
@@ -911,8 +922,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
pt.supplier_name AS supplierName,
pt.actual_amount AS actualAmount
from ccdi_purchase_transaction pt
inner join ccdi_base_staff staff
on CAST(staff.staff_id AS CHAR) = pt.applicant_id
inner join (
<include refid="projectScopedDirectStaffSql"/>
) project_staff on project_staff.staffId = pt.applicant_id
where IFNULL(pt.actual_amount, 0) > 100000
union
select distinct
@@ -921,8 +933,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
pt.supplier_name AS supplierName,
pt.actual_amount AS actualAmount
from ccdi_purchase_transaction pt
inner join ccdi_base_staff staff
on CAST(staff.staff_id AS CHAR) = pt.purchase_leader_id
inner join (
<include refid="projectScopedDirectStaffSql"/>
) project_staff on project_staff.staffId = pt.purchase_leader_id
where pt.purchase_leader_id is not null
and IFNULL(pt.actual_amount, 0) > 100000
) t
@@ -955,24 +968,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
SUM(source.actualAmount) / NULLIF(total_amount.totalAmount, 0) AS supplierRatio
from (
select distinct
staff.id_card AS objectKey,
project_staff.idCard AS objectKey,
pt.purchase_id AS purchaseId,
pt.supplier_name AS supplierName,
IFNULL(pt.actual_amount, 0) AS actualAmount
from ccdi_purchase_transaction pt
inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.applicant_id
inner join (
<include refid="projectScopedDirectStaffSql"/>
) project_staff on project_staff.staffId = pt.applicant_id
where IFNULL(pt.actual_amount, 0) > 0
and IFNULL(pt.supplier_name, '') &lt;&gt; ''
union
select distinct
staff.id_card AS objectKey,
project_staff.idCard AS objectKey,
pt.purchase_id AS purchaseId,
pt.supplier_name AS supplierName,
IFNULL(pt.actual_amount, 0) AS actualAmount
from ccdi_purchase_transaction pt
inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.purchase_leader_id
inner join (
<include refid="projectScopedDirectStaffSql"/>
) project_staff on project_staff.staffId = pt.purchase_leader_id
where pt.purchase_leader_id is not null
and IFNULL(pt.actual_amount, 0) > 0
and IFNULL(pt.supplier_name, '') &lt;&gt; ''
@@ -983,21 +1000,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ROUND(SUM(source_total.actualAmount), 2) AS totalAmount
from (
select distinct
staff.id_card AS objectKey,
project_staff.idCard AS objectKey,
pt.purchase_id AS purchaseId,
IFNULL(pt.actual_amount, 0) AS actualAmount
from ccdi_purchase_transaction pt
inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.applicant_id
inner join (
<include refid="projectScopedDirectStaffSql"/>
) project_staff on project_staff.staffId = pt.applicant_id
where IFNULL(pt.actual_amount, 0) > 0
union
select distinct
staff.id_card AS objectKey,
project_staff.idCard AS objectKey,
pt.purchase_id AS purchaseId,
IFNULL(pt.actual_amount, 0) AS actualAmount
from ccdi_purchase_transaction pt
inner join ccdi_base_staff staff on CAST(staff.staff_id AS CHAR) = pt.purchase_leader_id
inner join (
<include refid="projectScopedDirectStaffSql"/>
) project_staff on project_staff.staffId = pt.purchase_leader_id
where pt.purchase_leader_id is not null
and IFNULL(pt.actual_amount, 0) > 0
) source_total

View File

@@ -138,6 +138,16 @@ class CcdiBankTagAnalysisMapperXmlTest {
}
}
@Test
void purchaseRules_shouldBeScopedToCurrentProjectStaff() throws Exception {
String xml = readXml(RESOURCE);
assertAll(
() -> assertPurchaseRuleScopedByProject(xml, "selectLargePurchaseTransactionStatements"),
() -> assertPurchaseRuleScopedByProject(xml, "selectSupplierConcentrationObjects")
);
}
@Test
void assetRegistrationMismatchRules_shouldUseRealSqlAndAssetTable() throws Exception {
String xml = readXml(RESOURCE);
@@ -192,4 +202,21 @@ class CcdiBankTagAnalysisMapperXmlTest {
assertTrue(selectSql.contains("reasonDetail"), () -> selectId + " 缺少 reasonDetail");
assertTrue(!selectSql.contains("where 1 = 0"), () -> selectId + " 仍是占位 SQL");
}
private void assertPurchaseRuleScopedByProject(String xml, String selectId) {
String selectSql = extractSelectSql(xml, selectId);
String scopeSql = extractSqlFragment(xml, "projectScopedDirectStaffSql");
assertTrue(selectSql.contains("projectScopedDirectStaffSql"), () -> selectId + " 未引用项目员工范围片段");
assertTrue(scopeSql.contains("ccdi_bank_statement"), () -> selectId + " 缺少项目流水范围约束");
assertTrue(scopeSql.contains("#{projectId}"), () -> selectId + " 缺少 projectId 过滤条件");
}
private String extractSqlFragment(String xml, String sqlId) {
Pattern pattern = Pattern.compile(
"<sql\\s+id=\"" + sqlId + "\"[\\s\\S]*?</sql>"
);
Matcher matcher = pattern.matcher(xml);
assertTrue(matcher.find(), () -> "未找到 sql 片段: " + sqlId);
return matcher.group();
}
}

View File

@@ -0,0 +1,447 @@
# Credit Parsing Mock Server Backend 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:** 在现有 `lsfx-mock-server` 中新增征信解析 Mock 能力,提供 `POST /xfeature-mngs/conversation/htmlEval` 接口,支持稳定随机生成征信 `payload`、最小错误码模拟与健康检查能力。
**Architecture:** 复用现有 FastAPI 应用与测试基础设施,不新建独立服务。征信解析能力拆成 `schema 配置 + payload 生成 service + 调试 service + 独立 router` 四层,接口层只负责 `form-data` 接收与响应组装,字段生成和错误处理分别下沉到独立 service 中,避免与现有 LSFX Mock 逻辑混杂。
**Tech Stack:** Python 3, FastAPI, pytest, python-multipart, JSON, Bash
---
## File Structure
- `lsfx-mock-server/config/credit_feature_schema.json`: 固化 Excel 中 30 个征信指标字段的主题域、字段名、类型与枚举范围。
- `lsfx-mock-server/config/credit_response_examples.json`: 固化成功与失败响应模板,避免响应结构散落在路由中。
- `lsfx-mock-server/services/credit_payload_service.py`: 负责稳定随机种子生成、schema 加载和 `payload` 构造。
- `lsfx-mock-server/services/credit_debug_service.py`: 负责参数校验、调试标记识别与错误响应封装。
- `lsfx-mock-server/routers/credit_api.py`: 负责新增征信解析接口和征信健康检查接口。
- `lsfx-mock-server/main.py`: 注册征信解析 router并在应用描述中补充接口说明。
- `lsfx-mock-server/README.md`: 补充征信解析 Mock 的启动方式、接口路径和调试用法。
- `lsfx-mock-server/tests/test_credit_payload_service.py`: 锁定稳定随机与字段类型语义。
- `lsfx-mock-server/tests/test_credit_api.py`: 锁定征信解析接口成功、缺参、错误码与健康检查行为。
- `lsfx-mock-server/tests/test_startup.py`: 锁定新 router 已被主应用注册。
- `docs/reports/implementation/2026-03-23-credit-parsing-mock-server-backend-record.md`: 记录本次后端实施内容。
- `docs/tests/records/2026-03-23-credit-parsing-mock-server-backend-verification.md`: 记录本次后端验证命令与结果。
### Task 1: 建立征信字段 schema 和稳定随机 payload 生成能力
**Files:**
- Create: `lsfx-mock-server/config/credit_feature_schema.json`
- Create: `lsfx-mock-server/config/credit_response_examples.json`
- Create: `lsfx-mock-server/services/credit_payload_service.py`
- Create: `lsfx-mock-server/tests/test_credit_payload_service.py`
- Reference: `docs/design/2026-03-23-credit-parsing-mock-server-design.md`
- Reference: `assets/征信解析/征信解析接口payload.xlsx`
- [ ] **Step 1: Write the failing test**
`lsfx-mock-server/tests/test_credit_payload_service.py` 中先补两条失败用例锁定“同一输入返回稳定随机相同结果”和“schema 中的状态字段严格落在枚举范围内”:
```python
from services.credit_payload_service import CreditPayloadService
def test_generate_payload_should_be_stable_for_same_input():
service = CreditPayloadService("config/credit_feature_schema.json")
payload1 = service.generate_payload(
model="LXCUSTALL",
h_type="PERSON",
filename="credit-report-a.html",
)
payload2 = service.generate_payload(
model="LXCUSTALL",
h_type="PERSON",
filename="credit-report-a.html",
)
assert payload1 == payload2
assert set(payload1.keys()) == {"lx_header", "lx_debt", "lx_publictype"}
assert len(payload1["lx_debt"]) == 21
assert len(payload1["lx_publictype"]) == 6
def test_generate_payload_should_use_schema_type_rules():
service = CreditPayloadService("config/credit_feature_schema.json")
payload = service.generate_payload(
model="LXCUSTALL",
h_type="ENTERPRISE",
filename="credit-report-b.html",
)
assert payload["lx_debt"]["uncle_bank_house_state"] in {"正常", "逾期", "不良"}
assert payload["lx_header"]["report_time"].count("-") == 2
assert payload["lx_publictype"]["civil_cnt"].isdigit()
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd lsfx-mock-server
python3 -m pytest tests/test_credit_payload_service.py -v
```
Expected:
- `FAIL`
- 原因是 `CreditPayloadService`、schema 配置文件和响应模板尚不存在
- [ ] **Step 3: Write minimal implementation**
按最小路径落地:
1.`lsfx-mock-server/config/credit_feature_schema.json` 中把 Excel 30 个字段完整整理为 JSON至少包含
```json
[
{ "domain": "lx_header", "field": "query_cert_no", "type": "string" },
{ "domain": "lx_header", "field": "query_cust_name", "type": "string" },
{ "domain": "lx_header", "field": "report_time", "type": "string" },
{ "domain": "lx_debt", "field": "uncle_bank_house_state", "type": "status", "options": ["正常", "逾期", "不良"] }
]
```
2.`lsfx-mock-server/config/credit_response_examples.json` 中固化成功/失败模板:
```json
{
"success": { "message": "成功", "payload": {}, "status_code": "0" },
"errors": {
"ERR_99999": { "message": "关键参数缺失,参数名: XX", "payload": null, "status_code": "ERR_99999" }
}
}
```
3.`lsfx-mock-server/services/credit_payload_service.py` 中实现最小服务:
```python
class CreditPayloadService:
def __init__(self, schema_path: str):
self.schema_path = schema_path
self.schema = self._load_schema()
def generate_payload(self, model: str, h_type: str, filename: str) -> dict:
rng = random.Random(self._build_seed(model, h_type, filename))
...
```
4. 生成规则只保留设计文档确认的 4 类:
- `string`
- `amount`
- `count`
- `status`
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd lsfx-mock-server
python3 -m pytest tests/test_credit_payload_service.py -v
```
Expected:
- `PASS`
- 同一输入生成稳定一致结果,不同字段类型输出格式符合约定
- [ ] **Step 5: Commit**
```bash
git add lsfx-mock-server/config/credit_feature_schema.json lsfx-mock-server/config/credit_response_examples.json lsfx-mock-server/services/credit_payload_service.py lsfx-mock-server/tests/test_credit_payload_service.py
git commit -m "新增征信解析字段配置与生成服务"
```
### Task 2: 实现参数校验、错误码模拟和征信解析接口
**Files:**
- Create: `lsfx-mock-server/services/credit_debug_service.py`
- Create: `lsfx-mock-server/routers/credit_api.py`
- Create: `lsfx-mock-server/tests/test_credit_api.py`
- Modify: `lsfx-mock-server/tests/conftest.py`
- Reference: `docs/design/2026-03-23-credit-parsing-mock-server-design.md`
- [ ] **Step 1: Write the failing test**
`lsfx-mock-server/tests/test_credit_api.py` 中先补 4 类失败/成功用例,确保接口不走 FastAPI 默认 422而是走说明书约定的业务响应
```python
def test_html_eval_should_return_credit_payload(client):
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"model": "LXCUSTALL", "hType": "PERSON"},
files={"file": ("credit.html", b"<html></html>", "text/html")},
)
assert response.status_code == 200
data = response.json()
assert data["status_code"] == "0"
assert data["message"] == "成功"
assert "lx_header" in data["payload"]
def test_html_eval_should_return_err_99999_for_missing_model(client):
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"hType": "PERSON"},
files={"file": ("credit.html", b"<html></html>", "text/html")},
)
assert response.status_code == 200
assert response.json()["status_code"] == "ERR_99999"
def test_html_eval_should_return_err_10003_for_invalid_h_type(client):
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"model": "LXCUSTALL", "hType": "JSON"},
files={"file": ("credit.html", b"<html></html>", "text/html")},
)
assert response.status_code == 200
assert response.json()["status_code"] == "ERR_10003"
def test_html_eval_should_support_debug_error_marker(client):
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"model": "error_ERR_10001", "hType": "PERSON"},
files={"file": ("credit.html", b"<html></html>", "text/html")},
)
assert response.status_code == 200
assert response.json()["status_code"] == "ERR_10001"
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd lsfx-mock-server
python3 -m pytest tests/test_credit_api.py -v
```
Expected:
- `FAIL`
- 原因是征信解析 router 和调试 service 尚不存在
- [ ] **Step 3: Write minimal implementation**
按设计文档实现最小闭环:
1.`lsfx-mock-server/services/credit_debug_service.py` 中定义错误码映射和业务校验入口:
```python
class CreditDebugService:
ERROR_MESSAGES = {
"ERR_99999": "关键参数缺失,参数名: XX",
"ERR_10001": "无效的证件号码",
"ERR_10002": "无效的主题域",
"ERR_10003": "报文类型无效仅支持JSON/XML",
"ERR_10004": "无效机构号或行社号",
"ERR_10005": "无权限访问",
"ERR_10006": "尽调报告生成异常:异步事件发送失败",
}
```
2.`lsfx-mock-server/routers/credit_api.py` 中让参数全部使用可空 `Form(None)` / `File(None)`,再手工校验,避免缺参时被 FastAPI 直接拦成 422
```python
@router.post("/xfeature-mngs/conversation/htmlEval")
async def html_eval(
model: Optional[str] = Form(None),
hType: Optional[str] = Form(None),
file: Optional[UploadFile] = File(None),
):
...
```
3. 正常流程调用 `CreditPayloadService.generate_payload()`
4. 失败流程统一返回 `message + payload(null) + status_code`
5.`tests/conftest.py` 中按需补共享 fixture例如测试上传的默认 HTML 文件内容。
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd lsfx-mock-server
python3 -m pytest tests/test_credit_api.py -v
```
Expected:
- `PASS`
- 接口成功、缺参、非法 `hType`、调试错误码都返回预期业务结构
- [ ] **Step 5: Commit**
```bash
git add lsfx-mock-server/services/credit_debug_service.py lsfx-mock-server/routers/credit_api.py lsfx-mock-server/tests/test_credit_api.py lsfx-mock-server/tests/conftest.py
git commit -m "新增征信解析接口与错误模拟"
```
### Task 3: 注册新 router 并补启动文档
**Files:**
- Modify: `lsfx-mock-server/main.py`
- Modify: `lsfx-mock-server/README.md`
- Modify: `lsfx-mock-server/tests/test_startup.py`
- Reference: `lsfx-mock-server/dev.py`
- [ ] **Step 1: Write the failing test**
先在 `lsfx-mock-server/tests/test_startup.py` 中补应用注册断言,锁定主应用已经包含征信解析接口和健康检查接口:
```python
from main import app
def test_app_should_register_credit_mock_routes():
paths = {route.path for route in app.routes}
assert "/xfeature-mngs/conversation/htmlEval" in paths
assert "/credit/health" in paths
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd lsfx-mock-server
python3 -m pytest tests/test_startup.py -v
```
Expected:
- `FAIL`
- 原因是 `main.py` 还未注册征信解析 router
- [ ] **Step 3: Write minimal implementation**
1.`lsfx-mock-server/main.py` 中引入并注册 `credit_api.router`
```python
from routers import api, credit_api
app.include_router(api.router, tags=["流水分析接口"])
app.include_router(credit_api.router, tags=["征信解析接口"])
```
2. 在应用描述中补充征信解析能力说明。
3.`lsfx-mock-server/README.md` 中补充:
- 新接口路径
- `curl``requests` 调用示例
- `error_ERR_10001` 调试方式
- `GET /credit/health` 用法
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
cd lsfx-mock-server
python3 -m pytest tests/test_startup.py tests/test_credit_api.py -v
```
Expected:
- `PASS`
- 主应用已注册征信解析 routerREADME 与运行方式保持一致
- [ ] **Step 5: Commit**
```bash
git add lsfx-mock-server/main.py lsfx-mock-server/README.md lsfx-mock-server/tests/test_startup.py
git commit -m "注册征信解析Mock路由并补充说明"
```
### Task 4: 端到端验证并沉淀实施记录
**Files:**
- Create: `docs/reports/implementation/2026-03-23-credit-parsing-mock-server-backend-record.md`
- Create: `docs/tests/records/2026-03-23-credit-parsing-mock-server-backend-verification.md`
- Reference: `docs/design/2026-03-23-credit-parsing-mock-server-design.md`
- Reference: `docs/plans/backend/2026-03-23-credit-parsing-mock-server-backend-implementation.md`
- [ ] **Step 1: Run targeted automated tests**
Run:
```bash
cd lsfx-mock-server
python3 -m pytest tests/test_credit_payload_service.py tests/test_credit_api.py tests/test_startup.py -v
```
Expected:
- `PASS`
- 征信字段生成、接口返回和路由注册全部通过
- [ ] **Step 2: Run manual startup and smoke verification**
Run:
```bash
cd lsfx-mock-server
python3 main.py > /tmp/credit-mock-server.log 2>&1 &
echo $! > /tmp/credit-mock-server.pid
```
再执行:
```bash
curl -s http://127.0.0.1:8000/credit/health
curl -s -X POST http://127.0.0.1:8000/xfeature-mngs/conversation/htmlEval \
-F model=LXCUSTALL \
-F hType=PERSON \
-F file=@/tmp/sample-credit.html
```
Expected:
- 健康检查返回 `healthy`
- 成功接口返回 `status_code: "0"`,且 `payload` 包含三大主题域
- [ ] **Step 3: Stop the started process**
Run:
```bash
kill "$(cat /tmp/credit-mock-server.pid)"
rm -f /tmp/credit-mock-server.pid
```
Expected:
- 本次验证启动的 Mock 进程已停止,不留下端口占用
- [ ] **Step 4: Write implementation and verification records**
`docs/reports/implementation/2026-03-23-credit-parsing-mock-server-backend-record.md` 中记录:
- 新增接口、配置文件、service 和测试文件
- 稳定随机策略
- 说明书歧义处理方式
`docs/tests/records/2026-03-23-credit-parsing-mock-server-backend-verification.md` 中记录:
- 自动化测试命令与结果
- 手工 `curl` 验证命令与结果
- 启停服务过程
- [ ] **Step 5: Commit**
```bash
git add docs/reports/implementation/2026-03-23-credit-parsing-mock-server-backend-record.md docs/tests/records/2026-03-23-credit-parsing-mock-server-backend-verification.md
git commit -m "补充征信解析Mock后端实施与验证记录"
```

View File

@@ -0,0 +1,124 @@
# Credit Parsing Mock Server Frontend 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:** 在不新增前端页面和业务补丁的前提下,明确本次征信解析 Mock Server 建设对前端的影响边界,并沉淀联调核验与零改动结论。
**Architecture:** 本次需求主体是 `lsfx-mock-server` 的后端 Mock 能力建设,不要求 `ruoyi-ui` 新增页面、路由、接口封装或交互。前端计划采用“零源码改动 + 契约核验 + 记录沉淀”的最短路径;若核验发现必须新增前端消费字段,则应停止执行并回到设计阶段,而不是在本计划中临时扩展功能。
**Tech Stack:** Vue 2, Axios request wrapper, rg, Markdown docs
---
## File Structure
- `ruoyi-ui/src/api/`: 只用于核验当前仓库是否已有征信解析相关调用,不预期修改。
- `ruoyi-ui/src/views/`: 只用于核验是否存在需要接入征信解析 Mock 的页面触点,不预期修改。
- `ruoyi-ui/src/utils/request.js`: 只用于确认现有请求封装是否足以承接未来联调,不预期修改。
- `docs/reports/implementation/2026-03-23-credit-parsing-mock-server-frontend-record.md`: 记录本次前端零代码改动的范围与依据。
- `docs/tests/records/2026-03-23-credit-parsing-mock-server-frontend-verification.md`: 记录前端契约核验命令、结果和结论。
### Task 1: 核验当前前端不存在征信解析 Mock 接入改造需求
**Files:**
- Reference: `ruoyi-ui/src/api/`
- Reference: `ruoyi-ui/src/views/`
- Reference: `ruoyi-ui/src/utils/request.js`
- Reference: `docs/design/2026-03-23-credit-parsing-mock-server-design.md`
- [ ] **Step 1: Check the existing frontend touchpoints**
检查当前前端是否已经存在以下任一触点:
- 征信 HTML 上传入口
- 征信解析结果展示页
- 指向 `htmlEval``xfeature``征信解析` 的接口封装
如无上述触点,则本次计划默认保持前端零代码改动。
- [ ] **Step 2: Verify with search commands**
Run:
```bash
cd ruoyi-ui
rg -n "htmlEval|xfeature|征信|credit" src
```
Expected:
- 若无输出或仅命中无关文字,说明当前前端仓库没有现成的征信解析接入点
- 本次 Mock Server 不需要同步新增前端代码
- [ ] **Step 3: Confirm no contract adaptation is needed**
对照设计文档确认:
- Mock 接口为独立服务能力,不替换现有前端接口返回结构
- 不要求前端新增必填请求字段
- 不要求前端渲染新增结果列或交互入口
若上述任一不成立,则停止执行并回到设计阶段。
- [ ] **Step 4: Record the no-op conclusion**
在后续实施记录中明确写明:
- 本次需求不涉及 `ruoyi-ui` 源码改动
- 不为了“看起来完整”而额外创建演示页或调试页
- [ ] **Step 5: Commit**
```bash
git add docs/reports/implementation/2026-03-23-credit-parsing-mock-server-frontend-record.md docs/tests/records/2026-03-23-credit-parsing-mock-server-frontend-verification.md
git commit -m "记录征信解析Mock前端零改动结论"
```
### Task 2: 沉淀前端联调与验证记录
**Files:**
- Create: `docs/reports/implementation/2026-03-23-credit-parsing-mock-server-frontend-record.md`
- Create: `docs/tests/records/2026-03-23-credit-parsing-mock-server-frontend-verification.md`
- [ ] **Step 1: Write implementation record**
`docs/reports/implementation/2026-03-23-credit-parsing-mock-server-frontend-record.md` 中记录:
- 本次需求核心在 Mock 服务后端
- 当前前端仓库不存在征信解析接入点
- 因此本轮实施不修改 `ruoyi-ui` 任何源码
- [ ] **Step 2: Write verification record**
`docs/tests/records/2026-03-23-credit-parsing-mock-server-frontend-verification.md` 中记录:
- 执行过的 `rg` 核验命令
- 查验的目录范围
- “无需前端改动”的判断依据
- [ ] **Step 3: Verify frontend diff stays empty**
Run:
```bash
git diff --name-only -- ruoyi-ui
```
Expected:
- 无输出
- 证明本次前端计划执行保持零源码改动
- [ ] **Step 4: Confirm no frontend build is required**
在验证记录中明确写明:
- 因为 `ruoyi-ui` 无源码改动,本次不执行 `npm run build:prod`
- 若后续新增实际接入页面,再补充构建和联调测试
- [ ] **Step 5: Commit**
```bash
git add docs/reports/implementation/2026-03-23-credit-parsing-mock-server-frontend-record.md docs/tests/records/2026-03-23-credit-parsing-mock-server-frontend-verification.md
git commit -m "补充征信解析Mock前端核验记录"
```

View File

@@ -0,0 +1,28 @@
# Project 51 预警人数异常修复记录
## 本次改动
- 修复 `ccdi-project` 模块中采购类规则未按项目人员范围过滤的问题。
-`ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml` 新增 `projectScopedDirectStaffSql` 片段,统一收敛“当前项目流水中已识别员工”的范围。
-`selectLargePurchaseTransactionStatements` 改为仅统计当前项目员工对应的采购记录。
-`selectSupplierConcentrationObjects` 改为仅统计当前项目员工对应的采购记录,避免把其他项目或全局采购数据映射进当前项目预警结果。
-`ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java` 新增回归测试,约束采购规则必须引用项目员工范围片段。
## 根因说明
- `project_id = 51` 的项目总人数来自 `ccdi_project.target_count`,当前值为 `1`
- 预警人数来自 `ccdi_project_overview_employee_result` 聚合结果,排查发现其中除了上传文件对应员工 `韩桂英` 外,还混入了 `罗洋`
- 进一步核对 `ccdi_bank_statement_tag_result` 后确认,多出的 `罗洋` 来自 `SUPPLIER_CONCENTRATION` 规则生成的对象命中结果。
- 原始 SQL 直接扫描 `ccdi_purchase_transaction` 全表,并通过员工工号映射到 `ccdi_base_staff`,没有任何 `projectId` 或项目人员范围限制,导致全局采购数据串入当前项目。
## 变更文件
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java`
## 验证结果
- 通过数据库核对确认:`project_id = 51` 当前仅上传成功 1 个文件,但 `ccdi_project_overview_employee_result` 中存在 2 名员工,异常现象可复现。
- 执行 `mvn -pl ccdi-project -Dtest=CcdiBankTagAnalysisMapperXmlTest test`
- 结果:`BUILD SUCCESS`
- 新增回归测试已覆盖采购规则项目范围约束,防止后续再次把全局采购员工带入单个项目的预警统计。