Implement credit parse result polling and sentinel handling

This commit is contained in:
wkc
2026-05-18 10:56:25 +08:00
parent 9917d10e59
commit 1fadb38d99
25 changed files with 918 additions and 81 deletions

View File

@@ -113,18 +113,50 @@ response = requests.post(
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeature \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d serialNum=CCDI_CREDIT_1 \
-d orgCode=902000 \
-d orgCode=999000 \
-d runType=1 \
-d remotePath=http://127.0.0.1:62318/profile/credit-html/sample-credit.html \
-d model=LXCUSTALL
```
成功时返回:
发起成功时返回:
```json
{
"success": true,
"code": 1000,
"code": 10000,
"data": {
"mappingOutputFields": {
"message": "文件写入成功,流水号为: CCDI_CREDIT_1"
},
"reasonMessage": "Running successfully",
"outputFields": {
"C_S_ZXJXMESSAGE": "文件写入成功,流水号为: CCDI_CREDIT_1"
},
"procCode": "999000",
"bizId": "CCDI_CREDIT_1",
"reasonCode": 200,
"status": 1
}
}
```
再用相同 `serialNum` 查询解析结果:
```bash
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeatureResult \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d serialNum=CCDI_CREDIT_1 \
-d orgCode=999000 \
-d runType=1
```
结果成功时返回:
```json
{
"success": true,
"code": 10000,
"data": {
"mappingOutputFields": {
"message": "成功",
@@ -134,7 +166,10 @@ curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeatu
"lx_debt": {},
"lx_publictype": {}
}
}
},
"reasonMessage": "成功",
"reasonCode": 200,
"status": 1
}
}
```
@@ -145,7 +180,7 @@ curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeatu
curl -s -X POST http://localhost:8000/api/service/interface/invokeService/xfeature \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d serialNum=CCDI_CREDIT_1 \
-d orgCode=902000 \
-d orgCode=999000 \
-d runType=1 \
-d remotePath=http://127.0.0.1:62318/profile/credit-html/sample-credit.html \
-d model=error_ERR_10001
@@ -270,8 +305,9 @@ pytest tests/ -v --cov=. --cov-report=html
| 4 | POST | `/watson/api/project/upload/getpendings` | 检查解析状态 |
| 5 | POST | `/watson/api/project/batchDeleteUploadFile` | 删除文件 |
| 6 | POST | `/watson/api/project/getBSByLogId` | 获取银行流水 |
| 7 | POST | `/api/service/interface/invokeService/xfeature` | 征信解析 Mock |
| 8 | GET | `/credit/health` | 征信解析健康检查 |
| 7 | POST | `/api/service/interface/invokeService/xfeature` | 征信解析发起 Mock |
| 8 | POST | `/api/service/interface/invokeService/xfeatureResult` | 征信解析结果 Mock |
| 9 | GET | `/credit/health` | 征信解析健康检查 |
## ⚠️ 错误码列表

View File

@@ -112,17 +112,17 @@
},
{
"domain": "lx_debt",
"field": "uncle_credit_cart_bal",
"field": "uncle_credit_card_bal",
"type": "amount"
},
{
"domain": "lx_debt",
"field": "uncle_credit_cart_lmt",
"field": "uncle_credit_card_lmt",
"type": "amount"
},
{
"domain": "lx_debt",
"field": "uncle_credit_cart_state",
"field": "uncle_credit_card_state",
"type": "status",
"options": ["正常", "逾期", "不良"]
},

View File

@@ -26,7 +26,7 @@ app = FastAPI(
- **解析状态** - 轮询检查文件解析状态
- **文件删除** - 批量删除上传的文件
- **流水查询** - 分页获取银行流水数据
- **征信解析** - 读取 HTML 远程地址并返回结构化征信 payload
- **征信解析** - 发起 HTML 远程解析并通过结果接口返回结构化征信 payload
### 错误模拟
@@ -40,7 +40,8 @@ app = FastAPI(
2. 上传文件: POST /watson/api/project/remoteUploadSplitFile
3. 轮询解析状态: POST /watson/api/project/upload/getpendings
4. 获取流水: POST /watson/api/project/getBSByLogId
5. 征信解析: POST /api/service/interface/invokeService/xfeature
5. 征信解析发起: POST /api/service/interface/invokeService/xfeature
6. 征信解析结果: POST /api/service/interface/invokeService/xfeatureResult
""",
version=settings.APP_VERSION,
docs_url="/docs",

View File

@@ -12,6 +12,7 @@ router = APIRouter()
payload_service = CreditPayloadService("config/credit_feature_schema.json")
debug_service = CreditDebugService("config/credit_response_examples.json")
identity_service = CreditHtmlIdentityService()
result_cache = {}
@router.post("/api/service/interface/invokeService/xfeature")
@@ -40,6 +41,27 @@ async def html_eval(
filename=remote_filename(remotePath),
subject_identity=subject_identity,
)
result_cache[serialNum] = payload
return debug_service.build_initiate_success_response(serialNum)
@router.post("/api/service/interface/invokeService/xfeatureResult")
async def html_eval_result(
serialNum: Optional[str] = Form(None),
orgCode: Optional[str] = Form(None),
runType: Optional[str] = Form(None),
):
error_response = debug_service.validate_result_request(
serial_num=serialNum,
org_code=orgCode,
run_type=runType,
)
if error_response:
return error_response
payload = result_cache.get(serialNum)
if payload is None:
return debug_service.build_result_not_found_response(serialNum)
return debug_service.build_success_response(payload)

View File

@@ -41,10 +41,51 @@ class CreditDebugService:
return self.build_error_response("ERR_10002")
return None
def validate_result_request(
self,
serial_num: Optional[str],
org_code: Optional[str],
run_type: Optional[str],
):
if not serial_num:
return self.build_missing_param_response("serialNum")
if not org_code:
return self.build_missing_param_response("orgCode")
if not run_type:
return self.build_missing_param_response("runType")
return None
def build_initiate_success_response(self, serial_num: str) -> dict:
message = f"文件写入成功,流水号为: {serial_num}"
return {
"success": True,
"code": 10000,
"data": {
"mappingOutputFields": {
"message": message,
},
"reasonMessage": "Running successfully",
"outputFields": {
"C_S_ZXJXMESSAGE": message,
},
"procCode": "999000",
"bizId": serial_num,
"reasonCode": 200,
"status": 1,
},
}
def build_success_response(self, payload: dict) -> dict:
response = copy.deepcopy(self.templates["success"])
response["payload"] = payload
return self.wrap_mapping_response(response)
return self.wrap_mapping_response(response, status=1, reason_code=200)
def build_result_not_found_response(self, serial_num: str) -> dict:
return self.wrap_mapping_response({
"message": f"征信解析结果未返回: {serial_num}",
"payload": None,
"status_code": "ERR_99999",
}, status=0, reason_code=500, reason_message=f"征信解析结果未返回: {serial_num}")
def build_missing_param_response(self, param_name: str) -> dict:
response = self.build_error_response("ERR_99999")
@@ -53,7 +94,8 @@ class CreditDebugService:
return response
def build_error_response(self, error_code: str) -> dict:
return self.wrap_mapping_response(copy.deepcopy(self.templates["errors"][error_code]))
return self.wrap_mapping_response(copy.deepcopy(self.templates["errors"][error_code]),
status=0, reason_code=500)
def detect_error_marker(self, model: str) -> Optional[str]:
matched = re.search(r"error_(ERR_\d+)", model)
@@ -64,12 +106,21 @@ class CreditDebugService:
return error_code
return None
def wrap_mapping_response(self, mapping_output_fields: dict) -> dict:
def wrap_mapping_response(
self,
mapping_output_fields: dict,
status: int = 0,
reason_code: int = 500,
reason_message: Optional[str] = None,
) -> dict:
return {
"success": True,
"code": 10000,
"data": {
"mappingOutputFields": mapping_output_fields,
"reasonMessage": reason_message or mapping_output_fields.get("message"),
"reasonCode": reason_code,
"status": status,
},
}

View File

@@ -26,12 +26,16 @@ class FakeStaffIdentityRepository:
@pytest.fixture(autouse=True)
def reset_file_service_state():
"""避免 file_service 单例状态影响测试顺序。"""
from routers import credit_api
file_service.file_records.clear()
file_service.log_counter = settings.INITIAL_LOG_ID
file_service.staff_identity_repository = FakeStaffIdentityRepository()
credit_api.result_cache.clear()
yield
file_service.file_records.clear()
file_service.log_counter = settings.INITIAL_LOG_ID
credit_api.result_cache.clear()
@pytest.fixture
@@ -41,7 +45,7 @@ def client():
try:
from routers import credit_api
if not any(route.path == "/xfeature-mngs/conversation/htmlEval" for route in app.routes):
if not any(route.path == "/api/service/interface/invokeService/xfeature" for route in app.routes):
app.include_router(credit_api.router, tags=["征信解析接口"])
app.openapi_schema = None
except ModuleNotFoundError:

View File

@@ -1,47 +1,124 @@
def test_html_eval_should_return_credit_payload(client, sample_credit_html_file):
from routers import credit_api
def mapping_fields(response):
return response.json()["data"]["mappingOutputFields"]
def mock_remote_html(monkeypatch, sample_credit_html_file):
monkeypatch.setattr(credit_api, "fetch_remote_html", lambda remote_path: sample_credit_html_file[1])
def test_credit_parse_should_initiate_and_return_result(client, monkeypatch, sample_credit_html_file):
mock_remote_html(monkeypatch, sample_credit_html_file)
serial_num = "CCDI_CREDIT_TEST_001"
initiate_response = client.post(
"/api/service/interface/invokeService/xfeature",
data={
"serialNum": serial_num,
"orgCode": "999000",
"runType": "1",
"remotePath": "http://127.0.0.1:62318/profile/credit-html/sample.html",
"model": "LXCUSTALL",
},
)
assert initiate_response.status_code == 200
initiate_data = initiate_response.json()
assert initiate_data["success"] is True
assert initiate_data["code"] == 10000
assert initiate_data["data"]["status"] == 1
assert initiate_data["data"]["reasonCode"] == 200
assert "文件写入成功" in mapping_fields(initiate_response)["message"]
assert "payload" not in mapping_fields(initiate_response)
result_response = client.post(
"/api/service/interface/invokeService/xfeatureResult",
data={
"serialNum": serial_num,
"orgCode": "999000",
"runType": "1",
},
)
assert result_response.status_code == 200
result_data = result_response.json()
assert result_data["data"]["status"] == 1
assert result_data["data"]["reasonCode"] == 200
result_fields = mapping_fields(result_response)
assert result_fields["status_code"] == "0"
assert result_fields["message"] == "成功"
assert result_fields["payload"]["lx_header"]["query_cust_name"] == "测试员工"
assert result_fields["payload"]["lx_header"]["query_cert_no"] == "320101199001010030"
assert "uncle_credit_card_bal" in result_fields["payload"]["lx_debt"]
assert "uncle_credit_cart_bal" not in result_fields["payload"]["lx_debt"]
def test_credit_parse_should_return_err_99999_for_missing_model(client, monkeypatch, sample_credit_html_file):
mock_remote_html(monkeypatch, sample_credit_html_file)
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"model": "LXCUSTALL", "hType": "PERSON"},
files={"file": sample_credit_html_file},
"/api/service/interface/invokeService/xfeature",
data={
"serialNum": "CCDI_CREDIT_TEST_002",
"orgCode": "999000",
"runType": "1",
"remotePath": "http://127.0.0.1:62318/profile/credit-html/sample.html",
},
)
assert response.status_code == 200
data = response.json()
assert data["status_code"] == "0"
assert data["message"] == "成功"
assert "lx_header" in data["payload"]
assert data["payload"]["lx_header"]["query_cust_name"] == "测试员工"
assert data["payload"]["lx_header"]["query_cert_no"] == "320101199001010030"
assert response.json()["data"]["status"] == 0
assert response.json()["data"]["reasonCode"] == 500
assert mapping_fields(response)["status_code"] == "ERR_99999"
assert "model" in mapping_fields(response)["message"]
def test_html_eval_should_return_err_99999_for_missing_model(client, sample_credit_html_file):
def test_credit_parse_should_support_debug_error_marker(client, monkeypatch, sample_credit_html_file):
mock_remote_html(monkeypatch, sample_credit_html_file)
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"hType": "PERSON"},
files={"file": sample_credit_html_file},
"/api/service/interface/invokeService/xfeature",
data={
"serialNum": "CCDI_CREDIT_TEST_003",
"orgCode": "999000",
"runType": "1",
"remotePath": "http://127.0.0.1:62318/profile/credit-html/sample.html",
"model": "error_ERR_10001",
},
)
assert response.status_code == 200
assert response.json()["status_code"] == "ERR_99999"
assert mapping_fields(response)["status_code"] == "ERR_10001"
def test_html_eval_should_return_err_10003_for_invalid_h_type(client, sample_credit_html_file):
def test_credit_result_should_return_err_99999_for_unknown_serial_num(client):
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"model": "LXCUSTALL", "hType": "JSON"},
files={"file": sample_credit_html_file},
"/api/service/interface/invokeService/xfeatureResult",
data={
"serialNum": "CCDI_CREDIT_NOT_EXISTS",
"orgCode": "999000",
"runType": "1",
},
)
assert response.status_code == 200
assert response.json()["status_code"] == "ERR_10003"
result_fields = mapping_fields(response)
assert result_fields["status_code"] == "ERR_99999"
assert "征信解析结果未返回" in result_fields["message"]
def test_html_eval_should_support_debug_error_marker(client, sample_credit_html_file):
def test_credit_result_should_return_err_99999_for_missing_serial_num(client):
response = client.post(
"/xfeature-mngs/conversation/htmlEval",
data={"model": "error_ERR_10001", "hType": "PERSON"},
files={"file": sample_credit_html_file},
"/api/service/interface/invokeService/xfeatureResult",
data={
"orgCode": "999000",
"runType": "1",
},
)
assert response.status_code == 200
assert response.json()["status_code"] == "ERR_10001"
result_fields = mapping_fields(response)
assert result_fields["status_code"] == "ERR_99999"
assert "serialNum" in result_fields["message"]

View File

@@ -23,5 +23,6 @@ def test_dev_parse_args_should_reject_invalid_mode():
def test_app_should_register_credit_mock_routes():
paths = {route.path for route in app.routes}
assert "/xfeature-mngs/conversation/htmlEval" in paths
assert "/api/service/interface/invokeService/xfeature" in paths
assert "/api/service/interface/invokeService/xfeatureResult" in paths
assert "/credit/health" in paths