Implement credit parse result polling and sentinel handling
This commit is contained in:
@@ -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` | 征信解析健康检查 |
|
||||
|
||||
## ⚠️ 错误码列表
|
||||
|
||||
|
||||
@@ -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": ["正常", "逾期", "不良"]
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user