新增业务外部接口日志管理
This commit is contained in:
@@ -11,6 +11,8 @@ import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
|
||||
import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
|
||||
import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -42,7 +44,8 @@ public class CcdiCreditInfoController extends BaseController {
|
||||
@Log(title = "征信维护", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/upload")
|
||||
public AjaxResult upload(@RequestParam("files") MultipartFile[] files) {
|
||||
return AjaxResult.success("上传成功", creditInfoService.upload(Arrays.asList(files)));
|
||||
CallerContext caller = CallerContext.from(SecurityUtils.getLoginUser());
|
||||
return AjaxResult.success("上传成功", creditInfoService.upload(Arrays.asList(files), caller));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询征信维护列表")
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
|
||||
import com.ruoyi.info.collection.domain.vo.CreditInfoDetailVO;
|
||||
import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
|
||||
import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
@@ -16,7 +17,7 @@ import java.util.List;
|
||||
*/
|
||||
public interface ICcdiCreditInfoService {
|
||||
|
||||
CreditInfoUploadResultVO upload(List<MultipartFile> files);
|
||||
CreditInfoUploadResultVO upload(List<MultipartFile> files, CallerContext caller);
|
||||
|
||||
Page<CreditInfoListVO> selectCreditInfoPage(Page<CreditInfoListVO> page, CcdiCreditInfoQueryDTO queryDTO);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.ruoyi.info.collection.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
|
||||
import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
|
||||
@@ -17,6 +16,7 @@ import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
|
||||
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
|
||||
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
|
||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
|
||||
@@ -59,12 +59,12 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
||||
private CcdiCreditInfoQueryMapper queryMapper;
|
||||
|
||||
@Override
|
||||
public CreditInfoUploadResultVO upload(List<MultipartFile> files) {
|
||||
public CreditInfoUploadResultVO upload(List<MultipartFile> files, CallerContext caller) {
|
||||
CreditInfoUploadResultVO result = new CreditInfoUploadResultVO();
|
||||
List<CreditInfoUploadFailureVO> failures = new ArrayList<>();
|
||||
int totalCount = files == null ? 0 : files.size();
|
||||
int successCount = 0;
|
||||
String userName = currentUserName();
|
||||
String userName = caller.username();
|
||||
|
||||
if (files == null || files.isEmpty()) {
|
||||
result.setTotalCount(0);
|
||||
@@ -77,7 +77,7 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
||||
for (MultipartFile file : files) {
|
||||
try {
|
||||
validateHtmlFile(file);
|
||||
handleSingleFile(file, userName);
|
||||
handleSingleFile(file, userName, caller);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
failures.add(buildFailure(file, null, null, e.getMessage()));
|
||||
@@ -148,9 +148,9 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSingleFile(MultipartFile multipartFile, String userName) throws Exception {
|
||||
private void handleSingleFile(MultipartFile multipartFile, String userName, CallerContext caller) throws Exception {
|
||||
CreditHtmlStorageService.StoredCreditHtml storedHtml = creditHtmlStorageService.save(multipartFile);
|
||||
CreditParseInvokeResponse response = creditParseClient.parse(storedHtml.remotePath());
|
||||
CreditParseInvokeResponse response = creditParseClient.parse(caller, storedHtml.remotePath());
|
||||
CreditParsePayload payload = requireResponse(response).getPayload();
|
||||
Map<String, Object> header = requireHeader(payload);
|
||||
String personId = stringValue(header.get("query_cert_no"));
|
||||
@@ -286,11 +286,4 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
|
||||
return negativeVO;
|
||||
}
|
||||
|
||||
private String currentUserName() {
|
||||
try {
|
||||
return SecurityUtils.getUsername();
|
||||
} catch (Exception e) {
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.ruoyi.info.collection.service.impl.CcdiCreditInfoServiceImpl;
|
||||
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
|
||||
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
|
||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseInvokeData;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
|
||||
@@ -43,6 +44,8 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiCreditInfoServiceImplTest {
|
||||
|
||||
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
|
||||
|
||||
@InjectMocks
|
||||
private CcdiCreditInfoServiceImpl service;
|
||||
|
||||
@@ -73,18 +76,18 @@ class CcdiCreditInfoServiceImplTest {
|
||||
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
|
||||
"/profile/credit-html/2026/05/12/family_1.html",
|
||||
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html"));
|
||||
when(creditParseClient.parse(anyString()))
|
||||
when(creditParseClient.parse(any(CallerContext.class), anyString()))
|
||||
.thenReturn(successResponse("330101199202020022", "李四", "2026-03-24"));
|
||||
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
|
||||
.thenReturn(List.of(buildDebt("330101199202020022")));
|
||||
when(assembler.buildNegative(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
|
||||
.thenReturn(buildNegative("330101199202020022"));
|
||||
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file));
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
|
||||
|
||||
assertEquals(1, result.getSuccessCount());
|
||||
assertEquals(0, result.getFailureCount());
|
||||
verify(creditParseClient).parse("http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html");
|
||||
verify(creditParseClient).parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html");
|
||||
verify(debtsInfoMapper).deleteByPersonId("330101199202020022");
|
||||
verify(negativeInfoMapper).deleteByPersonId("330101199202020022");
|
||||
}
|
||||
@@ -97,12 +100,12 @@ class CcdiCreditInfoServiceImplTest {
|
||||
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
|
||||
"/profile/credit-html/2026/05/12/a_1.html",
|
||||
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
|
||||
when(creditParseClient.parse(anyString()))
|
||||
when(creditParseClient.parse(any(CallerContext.class), anyString()))
|
||||
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
|
||||
when(queryMapper.selectLatestQueryDate("330101199001010011"))
|
||||
.thenReturn(LocalDate.parse("2026-03-05"));
|
||||
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file));
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
|
||||
|
||||
assertEquals(0, result.getSuccessCount());
|
||||
assertEquals("上传征信日期早于当前已维护最新记录", result.getFailures().get(0).getReason());
|
||||
@@ -118,9 +121,9 @@ class CcdiCreditInfoServiceImplTest {
|
||||
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
|
||||
CreditParseInvokeResponse response = successResponse("330101199001010011", "张三", "2026-03-03");
|
||||
response.setCode(99999);
|
||||
when(creditParseClient.parse(anyString())).thenReturn(response);
|
||||
when(creditParseClient.parse(any(CallerContext.class), anyString())).thenReturn(response);
|
||||
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file));
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
|
||||
|
||||
assertEquals(0, result.getSuccessCount());
|
||||
assertEquals("征信解析平台状态码异常: 99999", result.getFailures().get(0).getReason());
|
||||
@@ -138,9 +141,9 @@ class CcdiCreditInfoServiceImplTest {
|
||||
response.getData().setStatus(0);
|
||||
response.getData().setReasonCode(500);
|
||||
response.getData().setReasonMessage("结果解析失败");
|
||||
when(creditParseClient.parse(anyString())).thenReturn(response);
|
||||
when(creditParseClient.parse(any(CallerContext.class), anyString())).thenReturn(response);
|
||||
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file));
|
||||
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
|
||||
|
||||
assertEquals(0, result.getSuccessCount());
|
||||
assertEquals("结果解析失败", result.getFailures().get(0).getReason());
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 系统日志持久化 -->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.lsfx.client;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
@@ -45,20 +46,20 @@ public class CreditParseClient {
|
||||
@Value("${credit-parse.api.model:LXCUSTALL}")
|
||||
private String defaultModel;
|
||||
|
||||
public CreditParseInvokeResponse parse(String remotePath) {
|
||||
return parse(defaultModel, remotePath);
|
||||
public CreditParseInvokeResponse parse(CallerContext caller, String remotePath) {
|
||||
return parse(caller, defaultModel, remotePath);
|
||||
}
|
||||
|
||||
public CreditParseInvokeResponse parse(String model, String remotePath) {
|
||||
public CreditParseInvokeResponse parse(CallerContext caller, String model, String remotePath) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
String actualModel = StringUtils.isBlank(model) ? defaultModel : model;
|
||||
String serialNum = buildSerialNum();
|
||||
try {
|
||||
Map<String, Object> initiateParams = buildInitiateParams(serialNum, actualModel, remotePath);
|
||||
CreditParseInvokeResponse initiateResponse = request(creditParseUrl, initiateParams, "发起接口");
|
||||
CreditParseInvokeResponse initiateResponse = request(caller, creditParseUrl, initiateParams, "发起接口");
|
||||
requireSuccessfulInitiateResponse(initiateResponse, "征信解析发起接口");
|
||||
|
||||
CreditParseInvokeResponse response = queryResult(serialNum);
|
||||
CreditParseInvokeResponse response = queryResult(caller, serialNum);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
log.info("【征信解析】调用完成: success={}, code={}, businessStatusCode={}, cost={}ms",
|
||||
@@ -94,10 +95,10 @@ public class CreditParseClient {
|
||||
return params;
|
||||
}
|
||||
|
||||
private CreditParseInvokeResponse queryResult(String serialNum) {
|
||||
private CreditParseInvokeResponse queryResult(CallerContext caller, String serialNum) {
|
||||
Map<String, Object> params = buildBaseParams(serialNum);
|
||||
for (int attempt = 1; attempt <= RESULT_QUERY_MAX_ATTEMPTS; attempt++) {
|
||||
CreditParseInvokeResponse response = request(creditParseResultUrl, params,
|
||||
CreditParseInvokeResponse response = request(caller, creditParseResultUrl, params,
|
||||
"结果接口第" + attempt + "次查询");
|
||||
requireSuccessfulServiceResponse(response, "征信解析结果接口");
|
||||
if (response.getData() == null || response.getData().getMappingOutputFields() == null) {
|
||||
@@ -130,10 +131,10 @@ public class CreditParseClient {
|
||||
Thread.sleep(intervalMillis);
|
||||
}
|
||||
|
||||
private CreditParseInvokeResponse request(String url, Map<String, Object> params, String stage) {
|
||||
private CreditParseInvokeResponse request(CallerContext caller, String url, Map<String, Object> params, String stage) {
|
||||
try {
|
||||
log.info("【征信解析】{}请求: url={}, params={}", stage, url, toJson(params));
|
||||
String responseJson = httpUtil.postUrlEncodedFormForString(url, params, null);
|
||||
String responseJson = httpUtil.postUrlEncodedFormForString(caller, url, params, null);
|
||||
log.info("【征信解析】{}返回JSON: {}", stage, responseJson);
|
||||
return objectMapper.readValue(responseJson, CreditParseInvokeResponse.class);
|
||||
} catch (LsfxApiException e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.lsfx.client;
|
||||
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.DeleteFilesRequest;
|
||||
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
|
||||
@@ -68,7 +69,7 @@ public class LsfxAnalysisClient {
|
||||
/**
|
||||
* 获取Token
|
||||
*/
|
||||
public GetTokenResponse getToken(GetTokenRequest request) {
|
||||
public GetTokenResponse getToken(CallerContext caller, GetTokenRequest request) {
|
||||
log.info("【流水分析】获取Token请求: projectNo={}, entityName={}", request.getProjectNo(), request.getEntityName());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
@@ -88,7 +89,7 @@ public class LsfxAnalysisClient {
|
||||
params.put("analysisType", request.getAnalysisType() != null ? request.getAnalysisType() : LsfxConstants.ANALYSIS_TYPE);
|
||||
|
||||
String url = baseUrl + getTokenEndpoint;
|
||||
GetTokenResponse response = httpUtil.postFormData(url, params, null, GetTokenResponse.class);
|
||||
GetTokenResponse response = httpUtil.postFormData(caller, url, params, null, GetTokenResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
@@ -110,14 +111,14 @@ public class LsfxAnalysisClient {
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
public UploadFileResponse uploadFile(Integer groupId, File file) {
|
||||
return uploadFile(groupId, file, file.getName());
|
||||
public UploadFileResponse uploadFile(CallerContext caller, Integer groupId, File file) {
|
||||
return uploadFile(caller, groupId, file, file.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
public UploadFileResponse uploadFile(Integer groupId, File file, String uploadFileName) {
|
||||
public UploadFileResponse uploadFile(CallerContext caller, Integer groupId, File file, String uploadFileName) {
|
||||
String multipartFileName = StringUtils.hasText(uploadFileName) ? uploadFileName : file.getName();
|
||||
log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, multipartFileName);
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -132,7 +133,7 @@ public class LsfxAnalysisClient {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
UploadFileResponse response = httpUtil.uploadFile(url, params, headers, UploadFileResponse.class);
|
||||
UploadFileResponse response = httpUtil.uploadFile(caller, url, params, headers, UploadFileResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
@@ -155,7 +156,7 @@ public class LsfxAnalysisClient {
|
||||
/**
|
||||
* 拉取行内流水
|
||||
*/
|
||||
public FetchInnerFlowResponse fetchInnerFlow(FetchInnerFlowRequest request) {
|
||||
public FetchInnerFlowResponse fetchInnerFlow(CallerContext caller, FetchInnerFlowRequest request) {
|
||||
log.info("【流水分析】拉取行内流水请求: groupId={}, customerNo={}", request.getGroupId(), request.getCustomerNo());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
@@ -168,7 +169,7 @@ public class LsfxAnalysisClient {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
FetchInnerFlowResponse response = httpUtil.postFormData(url, params, headers, FetchInnerFlowResponse.class);
|
||||
FetchInnerFlowResponse response = httpUtil.postFormData(caller, url, params, headers, FetchInnerFlowResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
@@ -190,7 +191,7 @@ public class LsfxAnalysisClient {
|
||||
/**
|
||||
* 检查文件解析状态
|
||||
*/
|
||||
public CheckParseStatusResponse checkParseStatus(Integer groupId, String inprogressList) {
|
||||
public CheckParseStatusResponse checkParseStatus(CallerContext caller, Integer groupId, String inprogressList) {
|
||||
log.info("【流水分析】检查文件解析状态: groupId={}, inprogressList={}", groupId, inprogressList);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
@@ -205,7 +206,7 @@ public class LsfxAnalysisClient {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
CheckParseStatusResponse response = httpUtil.postFormData(url, params, headers, CheckParseStatusResponse.class);
|
||||
CheckParseStatusResponse response = httpUtil.postFormData(caller, url, params, headers, CheckParseStatusResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
@@ -234,7 +235,7 @@ public class LsfxAnalysisClient {
|
||||
* @param request 请求参数(groupId, logId, pageNow, pageSize)
|
||||
* @return 流水明细列表
|
||||
*/
|
||||
public GetBankStatementResponse getBankStatement(GetBankStatementRequest request) {
|
||||
public GetBankStatementResponse getBankStatement(CallerContext caller, GetBankStatementRequest request) {
|
||||
log.info("【流水分析】获取银行流水请求: groupId={}, logId={}, pageNow={}, pageSize={}",
|
||||
request.getGroupId(), request.getLogId(), request.getPageNow(), request.getPageSize());
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -248,7 +249,7 @@ public class LsfxAnalysisClient {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
GetBankStatementResponse response = httpUtil.postFormData(url, params, headers, GetBankStatementResponse.class);
|
||||
GetBankStatementResponse response = httpUtil.postFormData(caller, url, params, headers, GetBankStatementResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
@@ -281,7 +282,7 @@ public class LsfxAnalysisClient {
|
||||
* @param request 请求参数(groupId必填, logId可选)
|
||||
* @return 文件上传状态信息
|
||||
*/
|
||||
public GetFileUploadStatusResponse getFileUploadStatus(GetFileUploadStatusRequest request) {
|
||||
public GetFileUploadStatusResponse getFileUploadStatus(CallerContext caller, GetFileUploadStatusRequest request) {
|
||||
log.info("【流水分析】获取文件上传状态: groupId={}, logId={}",
|
||||
request.getGroupId(), request.getLogId());
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -299,7 +300,7 @@ public class LsfxAnalysisClient {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
GetFileUploadStatusResponse response = httpUtil.get(url, params, headers,
|
||||
GetFileUploadStatusResponse response = httpUtil.get(caller, url, params, headers,
|
||||
GetFileUploadStatusResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
@@ -334,7 +335,7 @@ public class LsfxAnalysisClient {
|
||||
* @param request 请求参数(groupId, logIds, userId必填)
|
||||
* @return 删除结果
|
||||
*/
|
||||
public DeleteFilesResponse deleteFiles(DeleteFilesRequest request) {
|
||||
public DeleteFilesResponse deleteFiles(CallerContext caller, DeleteFilesRequest request) {
|
||||
log.info("【流水分析】删除文件请求: groupId={}, logIds={}, userId={}",
|
||||
request.getGroupId(), Arrays.toString(request.getLogIds()), request.getUserId());
|
||||
long startTime = System.currentTimeMillis();
|
||||
@@ -351,7 +352,7 @@ public class LsfxAnalysisClient {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
DeleteFilesResponse response = httpUtil.postFormData(url, params, headers,
|
||||
DeleteFilesResponse response = httpUtil.postFormData(caller, url, params, headers,
|
||||
DeleteFilesResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
package com.ruoyi.lsfx.controller;
|
||||
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "征信解析接口测试", description = "用于测试征信解析接口")
|
||||
@Anonymous
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
|
||||
@RestController
|
||||
@RequestMapping("/lsfx/credit")
|
||||
public class CreditParseController {
|
||||
@@ -37,7 +39,8 @@ public class CreditParseController {
|
||||
String actualModel = StringUtils.isBlank(model) ? DEFAULT_MODEL : model;
|
||||
|
||||
try {
|
||||
CreditParseInvokeResponse response = creditParseClient.parse(actualModel, remotePath);
|
||||
CreditParseInvokeResponse response = creditParseClient.parse(
|
||||
CallerContext.from(SecurityUtils.getLoginUser()), actualModel, remotePath);
|
||||
return AjaxResult.success(response);
|
||||
} catch (LsfxApiException e) {
|
||||
return AjaxResult.error(e.getMessage());
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.ruoyi.lsfx.controller;
|
||||
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.DeleteFilesRequest;
|
||||
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
|
||||
@@ -15,6 +16,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -28,7 +30,7 @@ import java.nio.file.StandardCopyOption;
|
||||
* 流水分析平台接口测试控制器
|
||||
*/
|
||||
@Tag(name = "流水分析平台接口测试", description = "用于测试流水分析平台的7个接口")
|
||||
@Anonymous
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
|
||||
@RestController
|
||||
@RequestMapping("/lsfx/test")
|
||||
public class LsfxTestController {
|
||||
@@ -61,7 +63,7 @@ public class LsfxTestController {
|
||||
request.setDepartmentCode(LsfxConstants.DEFAULT_DEPARTMENT_CODE);
|
||||
}
|
||||
|
||||
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
|
||||
GetTokenResponse response = lsfxAnalysisClient.getToken(currentCaller(), request);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ public class LsfxTestController {
|
||||
Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
File convertedFile = tempFile.toFile();
|
||||
UploadFileResponse response = lsfxAnalysisClient.uploadFile(groupId, convertedFile);
|
||||
UploadFileResponse response = lsfxAnalysisClient.uploadFile(currentCaller(), groupId, convertedFile);
|
||||
return AjaxResult.success(response);
|
||||
} catch (IOException e) {
|
||||
return AjaxResult.error("文件转换失败:" + e.getMessage());
|
||||
@@ -134,7 +136,7 @@ public class LsfxTestController {
|
||||
request.setDataChannelCode(LsfxConstants.DEFAULT_DATA_CHANNEL_CODE);
|
||||
}
|
||||
|
||||
FetchInnerFlowResponse response = lsfxAnalysisClient.fetchInnerFlow(request);
|
||||
FetchInnerFlowResponse response = lsfxAnalysisClient.fetchInnerFlow(currentCaller(), request);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
|
||||
@@ -152,7 +154,7 @@ public class LsfxTestController {
|
||||
return AjaxResult.error("参数不完整:inprogressList为必填");
|
||||
}
|
||||
|
||||
CheckParseStatusResponse response = lsfxAnalysisClient.checkParseStatus(groupId, inprogressList);
|
||||
CheckParseStatusResponse response = lsfxAnalysisClient.checkParseStatus(currentCaller(), groupId, inprogressList);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
|
||||
@@ -174,7 +176,7 @@ public class LsfxTestController {
|
||||
return AjaxResult.error("参数不完整:pageSize为必填且大于0");
|
||||
}
|
||||
|
||||
GetBankStatementResponse response = lsfxAnalysisClient.getBankStatement(request);
|
||||
GetBankStatementResponse response = lsfxAnalysisClient.getBankStatement(currentCaller(), request);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
|
||||
@@ -194,7 +196,7 @@ public class LsfxTestController {
|
||||
request.setGroupId(groupId);
|
||||
request.setLogId(logId);
|
||||
|
||||
GetFileUploadStatusResponse response = lsfxAnalysisClient.getFileUploadStatus(request);
|
||||
GetFileUploadStatusResponse response = lsfxAnalysisClient.getFileUploadStatus(currentCaller(), request);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
|
||||
@@ -213,7 +215,11 @@ public class LsfxTestController {
|
||||
return AjaxResult.error("参数不完整:userId为必填");
|
||||
}
|
||||
|
||||
DeleteFilesResponse response = lsfxAnalysisClient.deleteFiles(request);
|
||||
DeleteFilesResponse response = lsfxAnalysisClient.deleteFiles(currentCaller(), request);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
|
||||
private CallerContext currentCaller() {
|
||||
return CallerContext.from(SecurityUtils.getLoginUser());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.lsfx.domain;
|
||||
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 业务外部接口调用发起人快照。
|
||||
*/
|
||||
public record CallerContext(Long userId, String username) {
|
||||
|
||||
public CallerContext {
|
||||
if (!StringUtils.hasText(username)) {
|
||||
throw new IllegalArgumentException("接口调用账号不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
public static CallerContext from(LoginUser loginUser) {
|
||||
if (loginUser == null) {
|
||||
throw new IllegalArgumentException("登录用户不能为空");
|
||||
}
|
||||
return new CallerContext(loginUser.getUserId(), loginUser.getUsername());
|
||||
}
|
||||
|
||||
public static CallerContext of(Long userId, String username) {
|
||||
return new CallerContext(userId, username);
|
||||
}
|
||||
|
||||
public static CallerContext system() {
|
||||
return new CallerContext(null, "system");
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,48 @@
|
||||
package com.ruoyi.lsfx.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import com.ruoyi.system.domain.SysApiLog;
|
||||
import com.ruoyi.system.service.ISysApiLogService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.MediaTypeFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestClientResponseException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HTTP请求工具类
|
||||
* 业务外部HTTP请求工具,统一保存完整调用日志。
|
||||
*/
|
||||
@Component
|
||||
public class HttpUtil {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HttpUtil.class);
|
||||
private static final String CALL_SUCCESS = "0";
|
||||
private static final String CALL_FAILED = "1";
|
||||
|
||||
@Resource
|
||||
private RestTemplate restTemplate;
|
||||
@@ -32,6 +50,9 @@ public class HttpUtil {
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Resource
|
||||
private ISysApiLogService apiLogService;
|
||||
|
||||
public static org.springframework.core.io.Resource namedFileResource(File file, String filename) {
|
||||
return new NamedFileSystemResource(file, filename);
|
||||
}
|
||||
@@ -50,299 +71,245 @@ public class HttpUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送GET请求(带查询参数和请求头)
|
||||
* @param url 请求URL
|
||||
* @param params 查询参数
|
||||
* @param headers 请求头
|
||||
* @param responseType 响应类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public <T> T get(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
|
||||
public <T> T get(CallerContext caller, String url, Map<String, Object> params,
|
||||
Map<String, String> headers, Class<T> responseType) {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
builder.queryParam(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
String fullUrl = builder.toUriString();
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
return execute(caller, fullUrl, HttpMethod.GET, httpHeaders, params,
|
||||
new HttpEntity<>(httpHeaders), responseType, "API返回数据为空", "GET请求异常");
|
||||
}
|
||||
|
||||
public <T> T get(CallerContext caller, String url, Map<String, String> headers, Class<T> responseType) {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
return execute(caller, url, HttpMethod.GET, httpHeaders, null,
|
||||
new HttpEntity<>(httpHeaders), responseType, "API返回数据为空", "网络请求失败");
|
||||
}
|
||||
|
||||
public <T> T postJson(CallerContext caller, String url, Object request,
|
||||
Map<String, String> headers, Class<T> responseType) {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
|
||||
return execute(caller, url, HttpMethod.POST, httpHeaders, request,
|
||||
new HttpEntity<>(request, httpHeaders), responseType, "API返回数据为空", "网络请求失败");
|
||||
}
|
||||
|
||||
public <T> T postFormData(CallerContext caller, String url, Map<String, Object> params,
|
||||
Map<String, String> headers, Class<T> responseType) {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
if (params != null) {
|
||||
params.forEach(body::add);
|
||||
}
|
||||
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
|
||||
new HttpEntity<>(body, httpHeaders), responseType, "API返回数据为空", "网络请求失败");
|
||||
}
|
||||
|
||||
public <T> T postUrlEncodedForm(CallerContext caller, String url, Map<String, Object> params,
|
||||
Map<String, String> headers, Class<T> responseType) {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
MultiValueMap<String, String> body = toUrlEncodedBody(params);
|
||||
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
|
||||
new HttpEntity<>(body, httpHeaders), responseType, "API返回数据为空", "网络请求失败");
|
||||
}
|
||||
|
||||
public String postUrlEncodedFormForString(CallerContext caller, String url,
|
||||
Map<String, Object> params, Map<String, String> headers) {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
MultiValueMap<String, String> body = toUrlEncodedBody(params);
|
||||
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
|
||||
new HttpEntity<>(body, httpHeaders), String.class, "API返回数据为空", "网络请求失败");
|
||||
}
|
||||
|
||||
public <T> T uploadFile(CallerContext caller, String url, Map<String, Object> params,
|
||||
Map<String, String> headers, Class<T> responseType) {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
if (value instanceof File file) {
|
||||
body.add(key, new FileSystemResource(file));
|
||||
} else {
|
||||
body.add(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
|
||||
new HttpEntity<>(body, httpHeaders), responseType, "文件上传返回数据为空", "文件上传请求失败");
|
||||
}
|
||||
|
||||
private <T> T execute(CallerContext caller, String url, HttpMethod method, HttpHeaders requestHeaders,
|
||||
Object requestParams, HttpEntity<?> requestEntity, Class<T> responseType,
|
||||
String emptyResponseMessage, String requestFailureMessage) {
|
||||
if (caller == null) {
|
||||
throw new IllegalArgumentException("接口调用用户不能为空");
|
||||
}
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
SysApiLog apiLog = createBaseApiLog(caller, url, method, startTime);
|
||||
try {
|
||||
// 构建URL with查询参数
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
|
||||
if (params != null && !params.isEmpty()) {
|
||||
params.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
builder.queryParam(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String fullUrl = builder.toUriString();
|
||||
log.debug("【HTTP GET】请求URL: {}", fullUrl);
|
||||
|
||||
// 创建请求头
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
if (headers != null) {
|
||||
headers.forEach(httpHeaders::add);
|
||||
}
|
||||
|
||||
// 构建请求实体
|
||||
HttpEntity<String> entity = new HttpEntity<>(httpHeaders);
|
||||
|
||||
// 执行GET请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
fullUrl,
|
||||
HttpMethod.GET,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
log.debug("【HTTP GET】响应状态: {}", response.getStatusCode());
|
||||
log.debug("【HTTP GET】响应内容: {}", response.getBody());
|
||||
|
||||
// 解析响应
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
return objectMapper.readValue(response.getBody(), responseType);
|
||||
} else {
|
||||
throw new LsfxApiException("GET请求失败: " + response.getStatusCode());
|
||||
}
|
||||
captureRequest(apiLog, requestHeaders, requestParams);
|
||||
} catch (Exception e) {
|
||||
log.error("【HTTP GET】请求异常: url={}, error={}", url, e.getMessage(), e);
|
||||
throw new LsfxApiException("GET请求异常: " + e.getMessage(), e);
|
||||
log.error("接口日志请求快照构建失败: method={}, url={}", method, url, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送GET请求(带请求头)
|
||||
* @param url 请求URL
|
||||
* @param headers 请求头
|
||||
* @param responseType 响应类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public <T> T get(String url, Map<String, String> headers, Class<T> responseType) {
|
||||
try {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
HttpEntity<Void> requestEntity = new HttpEntity<>(httpHeaders);
|
||||
|
||||
ResponseEntity<T> response = restTemplate.exchange(
|
||||
url, HttpMethod.GET, requestEntity, responseType
|
||||
);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, method, requestEntity, String.class);
|
||||
captureResponse(apiLog, response);
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode());
|
||||
throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode().value());
|
||||
}
|
||||
if (!StringUtils.hasText(response.getBody())) {
|
||||
throw new LsfxApiException(emptyResponseMessage);
|
||||
}
|
||||
|
||||
T body = response.getBody();
|
||||
if (body == null) {
|
||||
throw new LsfxApiException("API返回数据为空");
|
||||
}
|
||||
|
||||
return body;
|
||||
T result = parseResponse(response.getBody(), responseType);
|
||||
apiLog.setCallStatus(CALL_SUCCESS);
|
||||
return result;
|
||||
} catch (RestClientResponseException e) {
|
||||
apiLog.setResponseStatus(e.getStatusCode().value());
|
||||
apiLog.setResponseHeaders(safeSerialize(e.getResponseHeaders()));
|
||||
apiLog.setResponseBody(e.getResponseBodyAsString());
|
||||
apiLog.setErrorMsg(formatException(e));
|
||||
throw new LsfxApiException(requestFailureMessage + ": " + e.getMessage(), e);
|
||||
} catch (LsfxApiException e) {
|
||||
apiLog.setErrorMsg(formatException(e));
|
||||
throw e;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
|
||||
apiLog.setErrorMsg(formatException(e));
|
||||
throw new LsfxApiException(requestFailureMessage + ": " + e.getMessage(), e);
|
||||
} catch (Exception e) {
|
||||
apiLog.setErrorMsg(formatException(e));
|
||||
throw new LsfxApiException("响应解析失败: " + e.getMessage(), e);
|
||||
} finally {
|
||||
apiLog.setCostTime(System.currentTimeMillis() - startTime);
|
||||
persistApiLog(apiLog);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求(JSON格式,带请求头)
|
||||
* @param url 请求URL
|
||||
* @param request 请求对象
|
||||
* @param headers 请求头
|
||||
* @param responseType 响应类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public <T> T postJson(String url, Object request, Map<String, String> headers, Class<T> responseType) {
|
||||
try {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Object> requestEntity = new HttpEntity<>(request, httpHeaders);
|
||||
|
||||
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
T body = response.getBody();
|
||||
if (body == null) {
|
||||
throw new LsfxApiException("API返回数据为空");
|
||||
}
|
||||
|
||||
return body;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
|
||||
}
|
||||
private SysApiLog createBaseApiLog(CallerContext caller, String url, HttpMethod method, long startTime) {
|
||||
SysApiLog apiLog = new SysApiLog();
|
||||
apiLog.setCallerUserId(caller.userId());
|
||||
apiLog.setCallerUsername(caller.username());
|
||||
apiLog.setApiUrl(url);
|
||||
apiLog.setHttpMethod(method.name());
|
||||
apiLog.setCallStatus(CALL_FAILED);
|
||||
apiLog.setCallTime(new Date(startTime));
|
||||
return apiLog;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求(multipart/form-data格式,带请求头)
|
||||
* 用于提交表单数据(非文件上传场景)
|
||||
* @param url 请求URL
|
||||
* @param params 表单参数
|
||||
* @param headers 请求头
|
||||
* @param responseType 响应类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public <T> T postFormData(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
|
||||
try {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
if (params != null) {
|
||||
params.forEach(body::add);
|
||||
}
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, httpHeaders);
|
||||
|
||||
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
T responseBody = response.getBody();
|
||||
if (responseBody == null) {
|
||||
throw new LsfxApiException("API返回数据为空");
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
|
||||
}
|
||||
private void captureRequest(SysApiLog apiLog, HttpHeaders headers, Object params) {
|
||||
apiLog.setContentType(headers.getContentType() == null ? null : headers.getContentType().toString());
|
||||
apiLog.setRequestHeaders(safeSerialize(headers));
|
||||
apiLog.setRequestParams(safeSerialize(normalizeValue(params)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求(application/x-www-form-urlencoded格式,带请求头)
|
||||
* @param url 请求URL
|
||||
* @param params 表单参数
|
||||
* @param headers 请求头
|
||||
* @param responseType 响应类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public <T> T postUrlEncodedForm(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
|
||||
try {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
body.add(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);
|
||||
|
||||
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
T responseBody = response.getBody();
|
||||
if (responseBody == null) {
|
||||
throw new LsfxApiException("API返回数据为空");
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
|
||||
}
|
||||
private void captureResponse(SysApiLog apiLog, ResponseEntity<String> response) {
|
||||
apiLog.setResponseStatus(response.getStatusCode().value());
|
||||
apiLog.setResponseHeaders(safeSerialize(response.getHeaders()));
|
||||
apiLog.setResponseBody(response.getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求(application/x-www-form-urlencoded格式)并返回原始JSON字符串
|
||||
* @param url 请求URL
|
||||
* @param params 表单参数
|
||||
* @param headers 请求头
|
||||
* @return 原始响应内容
|
||||
*/
|
||||
public String postUrlEncodedFormForString(String url, Map<String, Object> params, Map<String, String> headers) {
|
||||
try {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
body.add(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody == null) {
|
||||
throw new LsfxApiException("API返回数据为空");
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T parseResponse(String responseBody, Class<T> responseType) throws Exception {
|
||||
if (String.class.equals(responseType)) {
|
||||
return (T) responseBody;
|
||||
}
|
||||
return objectMapper.readValue(responseBody, responseType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件(Multipart格式)
|
||||
* @param url 请求URL
|
||||
* @param params 参数(包含文件)
|
||||
* @param headers 请求头
|
||||
* @param responseType 响应类型
|
||||
* @return 响应对象
|
||||
*/
|
||||
public <T> T uploadFile(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
|
||||
try {
|
||||
HttpHeaders httpHeaders = createHeaders(headers);
|
||||
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
// 如果是File对象,包装为FileSystemResource
|
||||
if (value instanceof File) {
|
||||
File file = (File) value;
|
||||
body.add(key, new FileSystemResource(file));
|
||||
} else if (value instanceof org.springframework.core.io.Resource) {
|
||||
body.add(key, value);
|
||||
} else {
|
||||
body.add(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, httpHeaders);
|
||||
|
||||
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
throw new LsfxApiException("文件上传失败,HTTP状态码: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
T responseBody = response.getBody();
|
||||
if (responseBody == null) {
|
||||
throw new LsfxApiException("文件上传返回数据为空");
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("文件上传请求失败: " + e.getMessage(), e);
|
||||
private MultiValueMap<String, String> toUrlEncodedBody(Map<String, Object> params) {
|
||||
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
body.add(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求头
|
||||
* @param headers 请求头Map
|
||||
* @return HttpHeaders对象
|
||||
*/
|
||||
private HttpHeaders createHeaders(Map<String, String> headers) {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
if (headers != null) {
|
||||
headers.forEach(httpHeaders::set);
|
||||
}
|
||||
return httpHeaders;
|
||||
}
|
||||
|
||||
private Object normalizeValue(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof File file) {
|
||||
return fileMetadata(file.getName(), file.length());
|
||||
}
|
||||
if (value instanceof FileSystemResource resource) {
|
||||
return fileMetadata(resource.getFilename(), resource.getFile().length());
|
||||
}
|
||||
if (value instanceof org.springframework.core.io.Resource resource) {
|
||||
return fileMetadata(resource.getFilename(), null);
|
||||
}
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
Map<String, Object> normalized = new LinkedHashMap<>();
|
||||
map.forEach((key, item) -> normalized.put(String.valueOf(key), normalizeValue(item)));
|
||||
return normalized;
|
||||
}
|
||||
if (value instanceof Iterable<?> iterable) {
|
||||
List<Object> normalized = new ArrayList<>();
|
||||
iterable.forEach(item -> normalized.add(normalizeValue(item)));
|
||||
return normalized;
|
||||
}
|
||||
if (value.getClass().isArray()) {
|
||||
List<Object> normalized = new ArrayList<>();
|
||||
for (int i = 0; i < Array.getLength(value); i++) {
|
||||
normalized.add(normalizeValue(Array.get(value, i)));
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private Map<String, Object> fileMetadata(String filename, Long size) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put("filename", filename);
|
||||
metadata.put("size", size);
|
||||
metadata.put("contentType", MediaTypeFactory.getMediaType(filename == null ? "" : filename)
|
||||
.map(MediaType::toString).orElse(null));
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private String safeSerialize(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (Exception e) {
|
||||
log.error("接口日志序列化失败", e);
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
private String formatException(Exception exception) {
|
||||
StringWriter writer = new StringWriter();
|
||||
exception.printStackTrace(new PrintWriter(writer));
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private void persistApiLog(SysApiLog apiLog) {
|
||||
try {
|
||||
apiLogService.recordApiLog(apiLog);
|
||||
} catch (Exception e) {
|
||||
log.error("接口日志入库失败: method={}, url={}", apiLog.getHttpMethod(), apiLog.getApiUrl(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.lsfx.client;
|
||||
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.UploadFileResponse;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -48,10 +49,11 @@ class LsfxAnalysisClientTest {
|
||||
|
||||
ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
ArgumentCaptor<Map<String, String>> headersCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
when(httpUtil.uploadFile(eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
|
||||
CallerContext caller = CallerContext.of(7L, "tester");
|
||||
when(httpUtil.uploadFile(eq(caller), eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
client.uploadFile(200, tempFile.toFile(), "银行流水A.xlsx");
|
||||
client.uploadFile(caller, 200, tempFile.toFile(), "银行流水A.xlsx");
|
||||
|
||||
assertEquals(200, paramsCaptor.getValue().get("groupId"));
|
||||
Resource filePart = assertInstanceOf(Resource.class, paramsCaptor.getValue().get("files"));
|
||||
|
||||
@@ -2,10 +2,14 @@ package com.ruoyi.lsfx.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.lsfx.client.CreditParseClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -13,10 +17,14 @@ import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -24,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -34,12 +43,19 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CreditParseControllerTest {
|
||||
|
||||
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
|
||||
|
||||
@Mock
|
||||
private CreditParseClient client;
|
||||
|
||||
@InjectMocks
|
||||
private CreditParseController controller;
|
||||
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parse_shouldRejectBlankRemotePath() {
|
||||
AjaxResult result = controller.parse(null, null);
|
||||
@@ -48,12 +64,13 @@ class CreditParseControllerTest {
|
||||
|
||||
@Test
|
||||
void shouldUseDefaultModelWhenMissing() {
|
||||
setLoginUser(CALLER.userId(), CALLER.username());
|
||||
CreditParseInvokeResponse response = new CreditParseInvokeResponse();
|
||||
response.setSuccess(true);
|
||||
response.setCode(10000);
|
||||
|
||||
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
|
||||
when(client.parse(eq("LXCUSTALL"), eq(remotePath))).thenReturn(response);
|
||||
when(client.parse(eq(CALLER), eq("LXCUSTALL"), eq(remotePath))).thenReturn(response);
|
||||
|
||||
AjaxResult result = controller.parse(remotePath, null);
|
||||
|
||||
@@ -63,7 +80,8 @@ class CreditParseControllerTest {
|
||||
|
||||
@Test
|
||||
void shouldReturnAjaxErrorWhenClientThrows() {
|
||||
when(client.parse(anyString(), anyString()))
|
||||
setLoginUser(CALLER.userId(), CALLER.username());
|
||||
when(client.parse(any(CallerContext.class), anyString(), anyString()))
|
||||
.thenThrow(new LsfxApiException("超时"));
|
||||
|
||||
AjaxResult result = controller.parse("http://127.0.0.1:62318/profile/credit-html/a.html", null);
|
||||
@@ -71,6 +89,16 @@ class CreditParseControllerTest {
|
||||
assertEquals(500, result.get("code"));
|
||||
}
|
||||
|
||||
private void setLoginUser(Long userId, String username) {
|
||||
SysUser user = new SysUser();
|
||||
user.setUserName(username);
|
||||
LoginUser loginUser = new LoginUser(user, Set.of("*:*:*"));
|
||||
loginUser.setUserId(userId);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
void creditParseClient_shouldInitiateAndQueryResultWithSameSerialNum() throws Exception {
|
||||
@@ -88,18 +116,20 @@ class CreditParseControllerTest {
|
||||
String payload = "{\"lx_header\":{\"query_cert_no\":\"330101199001010011\",\"query_cust_name\":\"张三\",\"report_time\":\"2026-03-24\"},\"lx_debt\":{\"uncle_bank_house_bal\":\"1\"},\"lx_publictype\":{\"civil_cnt\":1}}";
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE"));
|
||||
|
||||
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
|
||||
CreditParseInvokeResponse actual = parseClient.parse(remotePath);
|
||||
CreditParseInvokeResponse actual = parseClient.parse(CALLER, remotePath);
|
||||
|
||||
assertEquals(true, actual.getSuccess());
|
||||
assertEquals(10000, actual.getCode());
|
||||
@@ -107,12 +137,14 @@ class CreditParseControllerTest {
|
||||
.getPayload().getLxHeader().get("query_cert_no"));
|
||||
ArgumentCaptor<Map<String, Object>> initiateParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
|
||||
verify(httpUtil).postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
initiateParamsCaptor.capture(),
|
||||
isNull()
|
||||
);
|
||||
ArgumentCaptor<Map<String, Object>> resultParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
|
||||
verify(httpUtil).postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
resultParamsCaptor.capture(),
|
||||
isNull()
|
||||
@@ -152,21 +184,24 @@ class CreditParseControllerTest {
|
||||
String resultResponse = resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE");
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, resultResponse);
|
||||
|
||||
CreditParseInvokeResponse actual = parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html");
|
||||
CreditParseInvokeResponse actual = parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html");
|
||||
|
||||
assertEquals("330101199001010011", actual.getData().getMappingOutputFields()
|
||||
.getPayload().getLxHeader().get("query_cert_no"));
|
||||
verify(httpUtil, times(5)).postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
@@ -188,18 +223,20 @@ class CreditParseControllerTest {
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn("{\"success\":true,\"code\":99999,\"data\":{\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"0\"}}}");
|
||||
|
||||
LsfxApiException exception = assertThrows(LsfxApiException.class,
|
||||
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
() -> parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("平台状态码异常"));
|
||||
}
|
||||
@@ -218,18 +255,20 @@ class CreditParseControllerTest {
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn(initiateSuccessResponse());
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"reasonMessage\":\"解析失败\",\"reasonCode\":500,\"status\":0,\"mappingOutputFields\":{\"message\":\"结果异常\"}}}");
|
||||
|
||||
LsfxApiException exception = assertThrows(LsfxApiException.class,
|
||||
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
() -> parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("解析失败"));
|
||||
}
|
||||
@@ -248,16 +287,18 @@ class CreditParseControllerTest {
|
||||
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
|
||||
|
||||
when(httpUtil.postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeature"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"mappingOutputFields\":{\"message\":\"文件写入失败\"},\"reasonMessage\":\"文件写入失败\",\"reasonCode\":500,\"status\":0}}");
|
||||
|
||||
LsfxApiException exception = assertThrows(LsfxApiException.class,
|
||||
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
() -> parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html"));
|
||||
|
||||
assertTrue(exception.getMessage().contains("文件写入失败"));
|
||||
verify(httpUtil, times(0)).postUrlEncodedFormForString(
|
||||
eq(CALLER),
|
||||
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
|
||||
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
|
||||
isNull()
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package com.ruoyi.lsfx.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import com.ruoyi.system.domain.SysApiLog;
|
||||
import com.ruoyi.system.service.ISysApiLogService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -8,52 +14,258 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HttpUtilTest {
|
||||
|
||||
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Mock
|
||||
private ISysApiLogService apiLogService;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void uploadFile_shouldUseExplicitResourceFilename() throws Exception {
|
||||
HttpUtil httpUtil = new HttpUtil();
|
||||
private HttpUtil httpUtil;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
httpUtil = new HttpUtil();
|
||||
ReflectionTestUtils.setField(httpUtil, "restTemplate", restTemplate);
|
||||
ReflectionTestUtils.setField(httpUtil, "objectMapper", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(httpUtil, "apiLogService", apiLogService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadFile_shouldUseExplicitFilenameAndOnlyPersistFileMetadata() throws Exception {
|
||||
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
|
||||
Files.writeString(tempFile, "content");
|
||||
Files.writeString(tempFile, "binary-content-must-not-enter-log");
|
||||
|
||||
ArgumentCaptor<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
|
||||
when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class)))
|
||||
.thenReturn(ResponseEntity.ok("ok"));
|
||||
ArgumentCaptor<HttpEntity<?>> requestCaptor = httpEntityCaptor();
|
||||
when(restTemplate.exchange(eq("http://lsfx/upload"), eq(HttpMethod.POST),
|
||||
requestCaptor.capture(), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("groupId", 200);
|
||||
params.put("files", HttpUtil.namedFileResource(tempFile.toFile(), "银行流水A.xlsx"));
|
||||
|
||||
String result = httpUtil.uploadFile("http://lsfx/upload", params, null, String.class);
|
||||
assertEquals("ok", httpUtil.uploadFile(CALLER, "http://lsfx/upload", params, null, String.class));
|
||||
|
||||
assertEquals("ok", result);
|
||||
MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) captor.getValue().getBody();
|
||||
Object filePart = body.getFirst("files");
|
||||
Resource resource = assertInstanceOf(Resource.class, filePart);
|
||||
MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) requestCaptor.getValue().getBody();
|
||||
Resource resource = assertInstanceOf(Resource.class, body.getFirst("files"));
|
||||
assertEquals("银行流水A.xlsx", resource.getFilename());
|
||||
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
SysApiLog saved = logCaptor.getValue();
|
||||
assertEquals(7L, saved.getCallerUserId());
|
||||
assertEquals("tester", saved.getCallerUsername());
|
||||
assertTrue(saved.getRequestParams().contains("银行流水A.xlsx"));
|
||||
assertTrue(saved.getRequestParams().contains("\"size\":" + Files.size(tempFile)));
|
||||
assertFalse(saved.getRequestParams().contains("binary-content-must-not-enter-log"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void non2xx_shouldPersistOriginalStatusHeadersAndBody() {
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.add("X-Trace-Id", "trace-1");
|
||||
HttpClientErrorException error = HttpClientErrorException.create(
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Bad Request",
|
||||
responseHeaders,
|
||||
"{\"error\":\"参数错误\"}".getBytes(StandardCharsets.UTF_8),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
when(restTemplate.exchange(eq("http://lsfx/fail"), eq(HttpMethod.POST),
|
||||
any(HttpEntity.class), eq(String.class))).thenThrow(error);
|
||||
|
||||
assertThrows(LsfxApiException.class, () ->
|
||||
httpUtil.postJson(CALLER, "http://lsfx/fail", Map.of("token", "raw-token"), null, String.class));
|
||||
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
SysApiLog saved = logCaptor.getValue();
|
||||
assertEquals(400, saved.getResponseStatus());
|
||||
assertEquals("{\"error\":\"参数错误\"}", saved.getResponseBody());
|
||||
assertTrue(saved.getResponseHeaders().contains("trace-1"));
|
||||
assertTrue(saved.getRequestParams().contains("raw-token"));
|
||||
assertEquals("1", saved.getCallStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logPersistenceFailure_shouldNotChangeSuccessfulBusinessResult() {
|
||||
when(restTemplate.exchange(eq("http://lsfx/success"), eq(HttpMethod.GET),
|
||||
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
|
||||
doThrow(new RuntimeException("db unavailable")).when(apiLogService).recordApiLog(any(SysApiLog.class));
|
||||
|
||||
assertEquals("ok", httpUtil.get(CALLER, "http://lsfx/success", (Map<String, String>) null, String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void networkFailure_shouldPersistExceptionWithoutResponseStatus() {
|
||||
when(restTemplate.exchange(eq("http://lsfx/network-error"), eq(HttpMethod.GET),
|
||||
any(HttpEntity.class), eq(String.class))).thenThrow(new ResourceAccessException("connection refused"));
|
||||
|
||||
assertThrows(LsfxApiException.class, () ->
|
||||
httpUtil.get(CALLER, "http://lsfx/network-error", (Map<String, String>) null, String.class));
|
||||
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
SysApiLog saved = logCaptor.getValue();
|
||||
assertEquals(null, saved.getResponseStatus());
|
||||
assertTrue(saved.getErrorMsg().contains("connection refused"));
|
||||
assertEquals("1", saved.getCallStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyResponse_shouldPersistRawResponseBeforeFailing() {
|
||||
when(restTemplate.exchange(eq("http://lsfx/empty"), eq(HttpMethod.GET),
|
||||
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok(" "));
|
||||
|
||||
assertThrows(LsfxApiException.class, () ->
|
||||
httpUtil.get(CALLER, "http://lsfx/empty", (Map<String, String>) null, String.class));
|
||||
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
SysApiLog saved = logCaptor.getValue();
|
||||
assertEquals(200, saved.getResponseStatus());
|
||||
assertEquals(" ", saved.getResponseBody());
|
||||
assertTrue(saved.getErrorMsg().contains("API返回数据为空"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseFailure_shouldPersistOriginalResponseBody() {
|
||||
String rawBody = "{\"value\":\"not-an-integer\",\"unknown\":true}";
|
||||
when(restTemplate.exchange(eq("http://lsfx/parse-error"), eq(HttpMethod.GET),
|
||||
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok(rawBody));
|
||||
|
||||
assertThrows(LsfxApiException.class, () ->
|
||||
httpUtil.get(CALLER, "http://lsfx/parse-error", (Map<String, String>) null, Integer.class));
|
||||
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
SysApiLog saved = logCaptor.getValue();
|
||||
assertEquals(rawBody, saved.getResponseBody());
|
||||
assertEquals("1", saved.getCallStatus());
|
||||
assertTrue(saved.getErrorMsg() != null && !saved.getErrorMsg().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulResponse_shouldPersistCompleteRawBodyIncludingUnknownFields() {
|
||||
String rawBody = "{\"known\":1,\"unknown\":{\"nested\":true}}";
|
||||
when(restTemplate.exchange(eq("http://lsfx/raw"), eq(HttpMethod.GET),
|
||||
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok(rawBody));
|
||||
|
||||
Map<?, ?> result = httpUtil.get(CALLER, "http://lsfx/raw", (Map<String, String>) null, Map.class);
|
||||
|
||||
assertEquals(Boolean.TRUE, ((Map<?, ?>) result.get("unknown")).get("nested"));
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
assertEquals(rawBody, logCaptor.getValue().getResponseBody());
|
||||
assertEquals("0", logCaptor.getValue().getCallStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestSnapshotFailure_shouldNotPreventExternalCallOrLogging() {
|
||||
Iterable<String> brokenSnapshotValue = () -> new Iterator<>() {
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
throw new IllegalStateException("snapshot failed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String next() {
|
||||
return "unused";
|
||||
}
|
||||
};
|
||||
when(restTemplate.exchange(eq("http://lsfx/snapshot-error"), eq(HttpMethod.POST),
|
||||
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
|
||||
|
||||
assertEquals("ok", httpUtil.postJson(CALLER, "http://lsfx/snapshot-error",
|
||||
brokenSnapshotValue, null, String.class));
|
||||
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
assertEquals("0", logCaptor.getValue().getCallStatus());
|
||||
verify(restTemplate).exchange(eq("http://lsfx/snapshot-error"), eq(HttpMethod.POST),
|
||||
any(HttpEntity.class), eq(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void allPublicRequestTypes_shouldUseUnifiedLoggingPath() {
|
||||
when(restTemplate.exchange(any(String.class), any(HttpMethod.class),
|
||||
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
|
||||
|
||||
Map<String, Object> params = Map.of("value", "完整参数");
|
||||
Map<String, String> headers = Map.of("client-id", "client-1");
|
||||
assertEquals("ok", httpUtil.get(CALLER, "http://lsfx/get", params, headers, String.class));
|
||||
assertEquals("ok", httpUtil.get(CALLER, "http://lsfx/get", headers, String.class));
|
||||
assertEquals("ok", httpUtil.postJson(CALLER, "http://lsfx/json", params, headers, String.class));
|
||||
assertEquals("ok", httpUtil.postFormData(CALLER, "http://lsfx/form-data", params, headers, String.class));
|
||||
assertEquals("ok", httpUtil.postUrlEncodedForm(CALLER, "http://lsfx/form", params, headers, String.class));
|
||||
assertEquals("ok", httpUtil.postUrlEncodedFormForString(CALLER, "http://lsfx/form-string", params, headers));
|
||||
assertEquals("ok", httpUtil.uploadFile(CALLER, "http://lsfx/upload", params, headers, String.class));
|
||||
|
||||
verify(apiLogService, times(7)).recordApiLog(any(SysApiLog.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingCaller_shouldFailBeforeHttpRequest() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
httpUtil.postJson(null, "http://lsfx/json", Map.of(), null, String.class));
|
||||
verify(restTemplate, never()).exchange(any(String.class), any(HttpMethod.class),
|
||||
any(HttpEntity.class), eq(String.class));
|
||||
verify(apiLogService, never()).recordApiLog(any(SysApiLog.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void systemCaller_shouldPersistNullUserIdAndSystemUsername() {
|
||||
when(restTemplate.exchange(eq("http://lsfx/system"), eq(HttpMethod.GET),
|
||||
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
|
||||
|
||||
assertEquals("ok", httpUtil.get(CallerContext.system(), "http://lsfx/system",
|
||||
(Map<String, String>) null, String.class));
|
||||
|
||||
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
|
||||
verify(apiLogService).recordApiLog(logCaptor.capture());
|
||||
assertEquals(null, logCaptor.getValue().getCallerUserId());
|
||||
assertEquals("system", logCaptor.getValue().getCallerUsername());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private ArgumentCaptor<HttpEntity<?>> httpEntityCaptor() {
|
||||
return (ArgumentCaptor) ArgumentCaptor.forClass(HttpEntity.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.core.page.TableSupport;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -77,8 +78,8 @@ public class CcdiFileUploadController extends BaseController {
|
||||
}
|
||||
|
||||
try {
|
||||
String username = SecurityUtils.getUsername();
|
||||
String batchId = fileUploadService.batchUploadFiles(projectId, files, username);
|
||||
CallerContext caller = CallerContext.from(SecurityUtils.getLoginUser());
|
||||
String batchId = fileUploadService.batchUploadFiles(projectId, files, caller);
|
||||
return AjaxResult.success("上传任务已提交", batchId);
|
||||
} catch (RejectedExecutionException e) {
|
||||
log.warn("线程池已满,拒绝上传请求: projectId={}, fileCount={}", projectId, files.length);
|
||||
@@ -130,16 +131,14 @@ public class CcdiFileUploadController extends BaseController {
|
||||
return AjaxResult.error("开始日期和结束日期不能为空");
|
||||
}
|
||||
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
CallerContext caller = CallerContext.from(SecurityUtils.getLoginUser());
|
||||
String batchId = fileUploadService.submitPullBankInfo(
|
||||
dto.getProjectId(),
|
||||
dto.getIdCards(),
|
||||
dataChannelCode,
|
||||
dto.getStartDate(),
|
||||
dto.getEndDate(),
|
||||
userId,
|
||||
username
|
||||
caller
|
||||
);
|
||||
return AjaxResult.success("拉取任务已提交", batchId);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -43,7 +44,7 @@ public class CcdiProjectController extends BaseController {
|
||||
@Operation(summary = "创建项目")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:add')")
|
||||
public AjaxResult createProject(@Validated @RequestBody CcdiProjectSaveDTO dto) {
|
||||
CcdiProjectVO vo = projectService.createProject(dto);
|
||||
CcdiProjectVO vo = projectService.createProject(dto, CallerContext.from(SecurityUtils.getLoginUser()));
|
||||
return AjaxResult.success("项目创建成功", vo);
|
||||
}
|
||||
|
||||
@@ -130,7 +131,7 @@ public class CcdiProjectController extends BaseController {
|
||||
@Operation(summary = "导入历史项目")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:add')")
|
||||
public AjaxResult importFromHistory(@Validated @RequestBody CcdiProjectImportHistoryDTO dto) {
|
||||
CcdiProjectVO vo = projectService.importFromHistory(dto, SecurityUtils.getUsername());
|
||||
CcdiProjectVO vo = projectService.importFromHistory(dto, CallerContext.from(SecurityUtils.getLoginUser()));
|
||||
return AjaxResult.success("项目创建成功", vo);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiFileUploadQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,7 +25,7 @@ public interface ICcdiFileUploadService {
|
||||
* @param username 上传人
|
||||
* @return 批次ID
|
||||
*/
|
||||
String batchUploadFiles(Long projectId, MultipartFile[] files, String username);
|
||||
String batchUploadFiles(Long projectId, MultipartFile[] files, CallerContext caller);
|
||||
|
||||
/**
|
||||
* 解析身份证文件
|
||||
@@ -51,8 +52,7 @@ public interface ICcdiFileUploadService {
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
Long userId,
|
||||
String username);
|
||||
CallerContext caller);
|
||||
|
||||
/**
|
||||
* 删除上传记录并清理关联数据
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -22,7 +23,7 @@ public interface ICcdiProjectService {
|
||||
* @param dto 项目保存DTO
|
||||
* @return 项目VO
|
||||
*/
|
||||
CcdiProjectVO createProject(CcdiProjectSaveDTO dto);
|
||||
CcdiProjectVO createProject(CcdiProjectSaveDTO dto, CallerContext caller);
|
||||
|
||||
/**
|
||||
* 更新项目
|
||||
@@ -81,7 +82,7 @@ public interface ICcdiProjectService {
|
||||
* @param operator 操作人
|
||||
* @return 新建项目
|
||||
*/
|
||||
CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator);
|
||||
CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, CallerContext caller);
|
||||
|
||||
/**
|
||||
* 查询各状态的项目总数(不受搜索条件影响)
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetFileUploadStatusRequest;
|
||||
@@ -157,8 +158,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
Long userId,
|
||||
String username) {
|
||||
CallerContext caller) {
|
||||
if (projectId == null) {
|
||||
throw new IllegalArgumentException("项目ID不能为空");
|
||||
}
|
||||
@@ -209,7 +209,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
record.setFileStatus("uploading");
|
||||
record.setAccountNos(normalized);
|
||||
record.setUploadTime(now);
|
||||
record.setUploadUser(username);
|
||||
record.setUploadUser(caller.username());
|
||||
records.add(record);
|
||||
}
|
||||
if (records.isEmpty()) {
|
||||
@@ -223,7 +223,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
public void afterCommit() {
|
||||
CompletableFuture.runAsync(() -> submitPullBankInfoTasks(
|
||||
projectId, lsfxProjectId, records, normalizedIdCards,
|
||||
normalizedDataChannelCode, startDate, endDate, batchId
|
||||
normalizedDataChannelCode, startDate, endDate, batchId, caller
|
||||
));
|
||||
}
|
||||
});
|
||||
@@ -345,9 +345,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public String batchUploadFiles(Long projectId, MultipartFile[] files, String username) {
|
||||
public String batchUploadFiles(Long projectId, MultipartFile[] files, CallerContext caller) {
|
||||
log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}",
|
||||
projectId, files.length, username);
|
||||
projectId, files.length, caller.username());
|
||||
|
||||
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
|
||||
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
|
||||
@@ -406,7 +406,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
record.setFileSize(file.getSize());
|
||||
record.setFileStatus("uploading");
|
||||
record.setUploadTime(now);
|
||||
record.setUploadUser(username);
|
||||
record.setUploadUser(caller.username());
|
||||
records.add(record);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -438,7 +438,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
public void afterCommit() {
|
||||
log.info("【文件上传】事务已提交,启动异步任务");
|
||||
CompletableFuture.runAsync(() -> {
|
||||
submitTasksAsync(projectId, finalLsfxProjectId, tempFilePaths, records, batchId);
|
||||
submitTasksAsync(projectId, finalLsfxProjectId, tempFilePaths, records, batchId, caller);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -496,7 +496,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
private void submitTasksAsync(Long projectId, Integer lsfxProjectId,
|
||||
List<String> tempFilePaths,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
String batchId) {
|
||||
String batchId,
|
||||
CallerContext caller) {
|
||||
log.info("【文件上传】调度线程启动: projectId={}, batchId={}", projectId, batchId);
|
||||
|
||||
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
|
||||
@@ -519,7 +520,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
try {
|
||||
// 尝试提交异步任务
|
||||
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(
|
||||
() -> processFileAsync(projectId, lsfxProjectId, tempFilePath, record.getId(), batchId, record),
|
||||
() -> processFileAsync(projectId, lsfxProjectId, tempFilePath, record.getId(), batchId, record, caller),
|
||||
fileUploadExecutor
|
||||
);
|
||||
futures.add(future);
|
||||
@@ -600,7 +601,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
String batchId) {
|
||||
String batchId,
|
||||
CallerContext caller) {
|
||||
log.info("【拉取本行信息】调度线程启动: projectId={}, batchId={}", projectId, batchId);
|
||||
|
||||
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
|
||||
@@ -619,7 +621,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
while (!submitted && retryCount < 2) {
|
||||
try {
|
||||
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(
|
||||
() -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard, dataChannelCode, startDate, endDate),
|
||||
() -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard,
|
||||
dataChannelCode, startDate, endDate, caller),
|
||||
fileUploadExecutor
|
||||
);
|
||||
futures.add(future);
|
||||
@@ -660,7 +663,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
String idCard,
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate ) {
|
||||
String endDate,
|
||||
CallerContext caller) {
|
||||
try {
|
||||
String normalizedDataChannelCode = normalizePullBankInfoDataChannelCode(dataChannelCode);
|
||||
FetchInnerFlowRequest request = new FetchInnerFlowRequest();
|
||||
@@ -677,7 +681,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
request.setUploadUserId(LsfxConstants.DEFAULT_USER_ID);
|
||||
|
||||
FetchInnerFlowResponse response = lsfxClient.fetchInnerFlow(request);
|
||||
FetchInnerFlowResponse response = lsfxClient.fetchInnerFlow(caller, request);
|
||||
if (response == null || response.getData() == null || response.getData().isEmpty()) {
|
||||
throw new RuntimeException("拉取本行信息失败: 未返回logId");
|
||||
}
|
||||
@@ -687,7 +691,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
throw new RuntimeException("拉取本行信息失败: 未返回logId");
|
||||
}
|
||||
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId);
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, caller);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("【拉取本行信息】处理失败: idCard={}, recordId={}", idCard, record.getId(), e);
|
||||
@@ -709,7 +713,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
*/
|
||||
@Async("fileUploadExecutor")
|
||||
public boolean processFileAsync(Long projectId, Integer lsfxProjectId, String tempFilePath,
|
||||
Long recordId, String batchId, CcdiFileUploadRecord record) {
|
||||
Long recordId, String batchId, CcdiFileUploadRecord record,
|
||||
CallerContext caller) {
|
||||
log.info("【文件上传】开始处理文件: fileName={}, recordId={}, tempPath={}",
|
||||
record.getFileName(), recordId, tempFilePath);
|
||||
|
||||
@@ -730,7 +735,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
throw new RuntimeException("临时文件不存在: " + tempFilePath);
|
||||
}
|
||||
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName());
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(caller, lsfxProjectId, file, record.getFileName());
|
||||
if (uploadResponse == null || uploadResponse.getData() == null
|
||||
|| uploadResponse.getData().getUploadLogList() == null
|
||||
|| uploadResponse.getData().getUploadLogList().isEmpty()) {
|
||||
@@ -744,7 +749,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
|
||||
log.info("【文件上传】文件上传成功: logId={}", logId);
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true);
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true, caller);
|
||||
log.info("【文件上传】处理完成: fileName={}", record.getFileName());
|
||||
return true;
|
||||
|
||||
@@ -780,22 +785,24 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
private void processRecordAfterLogIdReady(Long projectId,
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId) {
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false);
|
||||
Integer logId,
|
||||
CallerContext caller) {
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false, caller);
|
||||
}
|
||||
|
||||
private void processRecordAfterLogIdReady(Long projectId,
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId,
|
||||
boolean preserveRecordFileName) {
|
||||
boolean preserveRecordFileName,
|
||||
CallerContext caller) {
|
||||
log.info("【文件上传】步骤3: 更新状态为解析中, logId={}", logId);
|
||||
record.setLogId(logId);
|
||||
record.setFileStatus("parsing");
|
||||
recordMapper.updateById(record);
|
||||
|
||||
log.info("【文件上传】步骤4: 开始轮询解析状态");
|
||||
boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString());
|
||||
boolean parsingComplete = waitForParsingComplete(caller, lsfxProjectId, logId.toString());
|
||||
if (!parsingComplete) {
|
||||
throw new RuntimeException("解析超时(超过10分钟),请检查文件格式是否正确");
|
||||
}
|
||||
@@ -805,7 +812,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
statusRequest.setGroupId(lsfxProjectId);
|
||||
statusRequest.setLogId(logId);
|
||||
|
||||
GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest);
|
||||
GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(caller, statusRequest);
|
||||
if (statusResponse == null || statusResponse.getData() == null
|
||||
|| statusResponse.getData().getLogs() == null
|
||||
|| statusResponse.getData().getLogs().isEmpty()) {
|
||||
@@ -846,8 +853,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
|
||||
log.info("【文件上传】步骤7: 获取流水数据");
|
||||
String fallbackCretNo = extractIdCardFromFileName(record.getFileName());
|
||||
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId,
|
||||
fallbackCretNo);
|
||||
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(caller, projectId, lsfxProjectId,
|
||||
logId, fallbackCretNo);
|
||||
if (!fetchResult.isSuccess()) {
|
||||
updateFailedRecord(record, fetchResult.getErrorMessage());
|
||||
return;
|
||||
@@ -867,7 +874,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
* @param logId 文件ID
|
||||
* @return true=解析完成,false=超时未完成
|
||||
*/
|
||||
private boolean waitForParsingComplete(Integer groupId, String logId) {
|
||||
private boolean waitForParsingComplete(CallerContext caller, Integer groupId, String logId) {
|
||||
log.info("【文件上传】开始轮询解析状态: groupId={}, logId={}", groupId, logId);
|
||||
|
||||
int maxRetries = 300;
|
||||
@@ -876,7 +883,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
for (int i = 1; i <= maxRetries; i++) {
|
||||
try {
|
||||
// 调用检查解析状态接口
|
||||
CheckParseStatusResponse response = lsfxClient.checkParseStatus(groupId, logId);
|
||||
CheckParseStatusResponse response = lsfxClient.checkParseStatus(caller, groupId, logId);
|
||||
|
||||
if (response == null || response.getData() == null) {
|
||||
log.warn("【文件上传】轮询第{}次: 响应数据为空", i);
|
||||
@@ -919,9 +926,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
* @param groupId 流水分析平台项目ID
|
||||
* @param logId 文件ID
|
||||
*/
|
||||
private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId,
|
||||
Integer logId,
|
||||
String fallbackCretNo) {
|
||||
private FetchBankStatementResult fetchAndSaveBankStatements(CallerContext caller, Long projectId, Integer groupId,
|
||||
Integer logId, String fallbackCretNo) {
|
||||
log.info("【文件上传】开始获取流水数据: projectId={}, groupId={}, logId={}",
|
||||
projectId, groupId, logId);
|
||||
|
||||
@@ -934,7 +940,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
firstRequest.setPageNow(1);
|
||||
firstRequest.setPageSize(1);
|
||||
|
||||
GetBankStatementResponse firstResponse = lsfxClient.getBankStatement(firstRequest);
|
||||
GetBankStatementResponse firstResponse = lsfxClient.getBankStatement(caller, firstRequest);
|
||||
if (firstResponse == null || firstResponse.getData() == null) {
|
||||
result.setSuccess(false);
|
||||
result.setErrorMessage("获取流水数据失败: 响应数据为空");
|
||||
@@ -968,7 +974,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
request.setPageNow(pageNow);
|
||||
request.setPageSize(pageSize);
|
||||
|
||||
GetBankStatementResponse response = lsfxClient.getBankStatement(request);
|
||||
GetBankStatementResponse response = lsfxClient.getBankStatement(caller, request);
|
||||
if (response == null || response.getData() == null
|
||||
|| response.getData().getBankStatementList() == null) {
|
||||
result.setSuccess(false);
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.GetTokenRequest;
|
||||
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -61,9 +62,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
|
||||
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto, CallerContext caller) {
|
||||
// 1. 调用流水分析平台获取projectId
|
||||
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName());
|
||||
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName(), caller);
|
||||
|
||||
// 2. 创建项目实体
|
||||
CcdiProject project = new CcdiProject();
|
||||
@@ -163,18 +164,18 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator) {
|
||||
public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, CallerContext caller) {
|
||||
projectAccessService.assertSourceProjectsReadable(dto.getSourceProjectIds());
|
||||
CcdiProjectSaveDTO saveDTO = new CcdiProjectSaveDTO();
|
||||
saveDTO.setProjectName(dto.getProjectName());
|
||||
saveDTO.setDescription(dto.getDescription());
|
||||
saveDTO.setConfigType("default");
|
||||
CcdiProjectVO project = createProject(saveDTO);
|
||||
CcdiProjectVO project = createProject(saveDTO, caller);
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
applicationEventPublisher.publishEvent(
|
||||
new CcdiProjectHistoryImportSubmittedEvent(project.getProjectId(), project.getLsfxProjectId(), dto, operator)
|
||||
new CcdiProjectHistoryImportSubmittedEvent(project.getProjectId(), project.getLsfxProjectId(), dto, caller.username())
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -380,7 +381,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
* @return 流水分析平台项目ID
|
||||
* @throws ServiceException 调用失败或响应无效时抛出
|
||||
*/
|
||||
private Integer callLsfxPlatform(String projectName) {
|
||||
private Integer callLsfxPlatform(String projectName, CallerContext caller) {
|
||||
// 构建请求参数
|
||||
GetTokenRequest request = new GetTokenRequest();
|
||||
request.setProjectNo("902000_" + System.currentTimeMillis());
|
||||
@@ -393,7 +394,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
request.setDepartmentCode("902000");
|
||||
|
||||
// 调用流水分析平台(异常处理和日志已在 LsfxAnalysisClient 中完成)
|
||||
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
|
||||
GetTokenResponse response = lsfxAnalysisClient.getToken(caller, request);
|
||||
|
||||
// 业务层校验:确保响应有效
|
||||
if (response == null || response.getData() == null) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -30,6 +31,7 @@ import static org.mockito.Mockito.when;
|
||||
class CcdiFileUploadControllerTest {
|
||||
|
||||
private static final Long PROJECT_ID = 100L;
|
||||
private static final CallerContext CALLER = CallerContext.of(9527L, "admin");
|
||||
|
||||
@InjectMocks
|
||||
private CcdiFileUploadController controller;
|
||||
@@ -72,7 +74,7 @@ class CcdiFileUploadControllerTest {
|
||||
};
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.batchUploadFiles(PROJECT_ID, files, "admin"))
|
||||
when(fileUploadService.batchUploadFiles(PROJECT_ID, files, CALLER))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.batchUpload(PROJECT_ID, files);
|
||||
@@ -80,7 +82,7 @@ class CcdiFileUploadControllerTest {
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals("batch-1", result.get("data"));
|
||||
verify(projectAccessService).assertCanOperate(PROJECT_ID);
|
||||
verify(fileUploadService).batchUploadFiles(PROJECT_ID, files, "admin");
|
||||
verify(fileUploadService).batchUploadFiles(PROJECT_ID, files, CALLER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,7 +95,8 @@ class CcdiFileUploadControllerTest {
|
||||
dto.setEndDate("2026-03-10");
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "ZJRCU", "2026-03-01", "2026-03-10", 9527L, "admin"))
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "ZJRCU",
|
||||
"2026-03-01", "2026-03-10", CALLER))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
@@ -109,7 +112,7 @@ class CcdiFileUploadControllerTest {
|
||||
dto.setDataChannelCode("JZL");
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "JZL", null, null, 9527L, "admin"))
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "JZL", null, null, CALLER))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
|
||||
import com.ruoyi.lsfx.domain.response.CheckParseStatusResponse;
|
||||
@@ -34,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -63,6 +65,7 @@ import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiFileUploadServiceImplTest {
|
||||
@@ -71,6 +74,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
private static final Integer LSFX_PROJECT_ID = 200;
|
||||
private static final Long RECORD_ID = 300L;
|
||||
private static final Integer LOG_ID = 400;
|
||||
private static final CallerContext CALLER = CallerContext.of(9527L, "admin");
|
||||
private static final int MAX_ERROR_MESSAGE_LENGTH = 2000;
|
||||
|
||||
@InjectMocks
|
||||
@@ -149,8 +153,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-03-01",
|
||||
"2026-03-10",
|
||||
9527L,
|
||||
"admin"
|
||||
CALLER
|
||||
);
|
||||
|
||||
assertNotNull(batchId);
|
||||
@@ -177,8 +180,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-01-01",
|
||||
"2026-01-31",
|
||||
1L,
|
||||
"tester"
|
||||
CALLER
|
||||
));
|
||||
}
|
||||
|
||||
@@ -195,7 +197,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,7 +224,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
String batchId = service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester");
|
||||
String batchId = service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER);
|
||||
|
||||
assertNotNull(batchId);
|
||||
assertNotNull(inserted.get());
|
||||
@@ -251,12 +253,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER));
|
||||
|
||||
assertTrue(exception.getMessage().contains("身份证"));
|
||||
assertFalse(Files.exists(tempDir.resolve("temp")));
|
||||
verify(recordMapper, never()).insertBatch(any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -272,12 +274,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER));
|
||||
|
||||
assertTrue(exception.getMessage().contains("文件名不能为空"));
|
||||
assertFalse(Files.exists(tempDir.resolve("temp")));
|
||||
verify(recordMapper, never()).insertBatch(any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -293,6 +295,29 @@ class CcdiFileUploadServiceImplTest {
|
||||
assertFalse(Files.exists(batchLogDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitTasksAsync_shouldKeepEachCapturedCallerAfterSecurityContextIsCleared() throws Exception {
|
||||
setField("fileUploadExecutor", (Executor) Runnable::run);
|
||||
CallerContext callerA = CallerContext.of(101L, "userA");
|
||||
CallerContext callerB = CallerContext.of(202L, "userB");
|
||||
Path fileA = createTempFile();
|
||||
Path fileB = createTempFile();
|
||||
CcdiFileUploadRecord recordA = buildRecord();
|
||||
CcdiFileUploadRecord recordB = buildRecord();
|
||||
recordB.setId(RECORD_ID + 1);
|
||||
when(lsfxClient.uploadFile(any(CallerContext.class), eq(LSFX_PROJECT_ID), any(), any()))
|
||||
.thenThrow(new RuntimeException("stop after caller capture"));
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
invokeSubmitTasksAsync(List.of(fileA.toString()), List.of(recordA), "batch-a", callerA);
|
||||
invokeSubmitTasksAsync(List.of(fileB.toString()), List.of(recordB), "batch-b", callerB);
|
||||
|
||||
ArgumentCaptor<CallerContext> callerCaptor = ArgumentCaptor.forClass(CallerContext.class);
|
||||
verify(lsfxClient, org.mockito.Mockito.times(2)).uploadFile(
|
||||
callerCaptor.capture(), eq(LSFX_PROJECT_ID), any(), any());
|
||||
assertEquals(List.of(callerA, callerB), callerCaptor.getAllValues());
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleTagRebuildAfterBatchCompletion_shouldLogSkipWhenAllRecordsFailed() {
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(CcdiFileUploadServiceImpl.class);
|
||||
@@ -352,18 +377,18 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenThrow(new RuntimeException("bank statement fetch failed"));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
assertTrue(events.stream().anyMatch(event -> event.endsWith("record:parsed_failed")));
|
||||
assertFalse(events.stream().anyMatch(event -> event.endsWith("record:parsed_success")));
|
||||
@@ -379,12 +404,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(1);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
events.add(sequence.incrementAndGet() + ":bank-fetch");
|
||||
return buildEmptyBankStatementResponse();
|
||||
@@ -393,7 +418,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
int fetchIndex = findEventIndex(events, "bank-fetch");
|
||||
int successIndex = findEventIndex(events, "record:parsed_success");
|
||||
@@ -406,18 +431,18 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenThrow(new RuntimeException("bank statement fetch failed"));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
}
|
||||
@@ -427,11 +452,11 @@ class CcdiFileUploadServiceImplTest {
|
||||
GetFileUploadStatusResponse statusResponse = buildParsedSuccessStatusResponse("XX身份证.xlsx");
|
||||
statusResponse.getData().getLogs().get(0).setFileSize(2048L);
|
||||
|
||||
when(lsfxClient.fetchInnerFlow(any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.fetchInnerFlow(eq(CALLER), any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(statusResponse);
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(statusResponse);
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
@@ -444,7 +469,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
"110101199001018888",
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-03-01",
|
||||
"2026-03-10"
|
||||
"2026-03-10",
|
||||
CALLER
|
||||
);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
@@ -457,11 +483,11 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processPullBankInfoAsync_shouldFetchJzlWithZeroDateRange() {
|
||||
when(lsfxClient.fetchInnerFlow(any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.fetchInnerFlow(eq(CALLER), any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
@@ -473,10 +499,11 @@ class CcdiFileUploadServiceImplTest {
|
||||
"110101199001018888",
|
||||
LsfxConstants.DATA_CHANNEL_JZL,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
CALLER
|
||||
);
|
||||
|
||||
verify(lsfxClient).fetchInnerFlow(argThat((FetchInnerFlowRequest request) ->
|
||||
verify(lsfxClient).fetchInnerFlow(eq(CALLER), argThat((FetchInnerFlowRequest request) ->
|
||||
LsfxConstants.DATA_CHANNEL_JZL.equals(request.getDataChannelCode())
|
||||
&& Integer.valueOf(0).equals(request.getDataStartDateId())
|
||||
&& Integer.valueOf(0).equals(request.getDataEndDateId())
|
||||
@@ -485,21 +512,21 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("原始流水.xlsx")))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), eq("原始流水.xlsx")))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(lsfxClient).uploadFile(eq(LSFX_PROJECT_ID), argThat(file ->
|
||||
verify(lsfxClient).uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), argThat(file ->
|
||||
file.getName().startsWith("upload-") && file.getName().endsWith(".xlsx")
|
||||
), eq("原始流水.xlsx"));
|
||||
}
|
||||
@@ -517,14 +544,15 @@ class CcdiFileUploadServiceImplTest {
|
||||
project.setProjectId(PROJECT_ID);
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(1);
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("张三_330101199001010011_流水.xlsx")))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(),
|
||||
eq("张三_330101199001010011_流水.xlsx")))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
GetBankStatementRequest request = invocation.getArgument(0);
|
||||
GetBankStatementRequest request = invocation.getArgument(1);
|
||||
if (Integer.valueOf(1).equals(request.getPageSize())) {
|
||||
return buildBankStatementCountResponse(1);
|
||||
}
|
||||
@@ -535,7 +563,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
record.setFileName("张三_330101199001010011_流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record,
|
||||
CALLER);
|
||||
|
||||
assertNotNull(insertedStatements.get());
|
||||
assertEquals(1, insertedStatements.get().size());
|
||||
@@ -544,20 +573,20 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any()))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any()))
|
||||
.thenReturn(buildParsedSuccessStatusResponse("平台返回文件名.xlsx"));
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
@@ -573,17 +602,17 @@ class CcdiFileUploadServiceImplTest {
|
||||
logItem.setStatus(-1);
|
||||
logItem.setUploadStatusDesc("parse.failed");
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(statusResponse);
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(statusResponse);
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
@@ -610,7 +639,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
|
||||
|
||||
assertEquals("删除成功,已开始项目重新打标", result);
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(lsfxClient, never()).deleteFiles(any(), any());
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
RECORD_ID.equals(item.getId()) && "deleted".equals(item.getFileStatus())
|
||||
@@ -644,7 +673,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
() -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
|
||||
|
||||
assertTrue(exception.getMessage().contains("历史导入文件不支持删除"));
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(lsfxClient, never()).deleteFiles(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -659,7 +688,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
|
||||
|
||||
assertEquals("删除成功,已开始项目重新打标", result);
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(lsfxClient, never()).deleteFiles(any(), any());
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
"deleted".equals(item.getFileStatus())
|
||||
@@ -669,7 +698,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
// @Test
|
||||
// void processPullBankInfoAsync_shouldMarkParsedFailedWhenFetchInnerFlowThrows() {
|
||||
// when(lsfxClient.fetchInnerFlow(any())).thenThrow(new RuntimeException("fetch inner flow failed"));
|
||||
// when(lsfxClient.fetchInnerFlow(eq(CALLER), any())).thenThrow(new RuntimeException("fetch inner flow failed"));
|
||||
//
|
||||
// CcdiFileUploadRecord record = buildRecord();
|
||||
// service.processPullBankInfoAsync(
|
||||
@@ -692,19 +721,19 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithTotalCount(1))
|
||||
.thenThrow(new RuntimeException("paged fetch failed"));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
assertTrue(events.stream().anyMatch(event -> event.endsWith("record:parsed_failed")));
|
||||
assertFalse(events.stream().anyMatch(event -> event.endsWith("record:parsed_success")));
|
||||
@@ -716,18 +745,18 @@ class CcdiFileUploadServiceImplTest {
|
||||
List<CcdiFileUploadRecord> updates = new ArrayList<>();
|
||||
captureUpdatedRecords(updates);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenThrow(new RuntimeException("bank statement fetch failed:" + "x".repeat(3000)));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
CcdiFileUploadRecord failedRecord = findLastUpdatedRecordByStatus(updates, "parsed_failed");
|
||||
assertTrue(failedRecord.getErrorMessage().length() <= MAX_ERROR_MESSAGE_LENGTH);
|
||||
@@ -738,13 +767,13 @@ class CcdiFileUploadServiceImplTest {
|
||||
List<CcdiFileUploadRecord> updates = new ArrayList<>();
|
||||
captureUpdatedRecords(updates);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenThrow(new RuntimeException("upload failed:" + "x".repeat(3000)));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
CcdiFileUploadRecord failedRecord = findLastUpdatedRecordByStatus(updates, "parsed_failed");
|
||||
assertTrue(failedRecord.getErrorMessage().length() <= MAX_ERROR_MESSAGE_LENGTH);
|
||||
@@ -752,19 +781,19 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void fetchAndSaveBankStatements_shouldTrimLeAccountNoBeforeInsert() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem(" 62220001 "))))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem(" 62220001 "))));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(bankStatementMapper).insertBatch(any());
|
||||
verify(bankStatementMapper).insertBatch(org.mockito.ArgumentMatchers.argThat(list ->
|
||||
@@ -773,7 +802,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void fetchAndSaveBankStatements_shouldLogConservativeCountsWhenAffectedRowsAreAmbiguous() {
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))));
|
||||
when(bankStatementMapper.insertBatch(any())).thenReturn(1);
|
||||
@@ -787,6 +816,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
Object result = ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"fetchAndSaveBankStatements",
|
||||
CALLER,
|
||||
PROJECT_ID,
|
||||
LSFX_PROJECT_ID,
|
||||
LOG_ID,
|
||||
@@ -825,12 +855,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))));
|
||||
when(bankStatementMapper.insertBatch(any()))
|
||||
@@ -839,7 +869,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
assertTrue(events.stream().anyMatch(event -> event.endsWith("record:parsed_failed")));
|
||||
assertFalse(events.stream().anyMatch(event -> event.endsWith("record:parsed_success")));
|
||||
@@ -1026,10 +1056,17 @@ class CcdiFileUploadServiceImplTest {
|
||||
private void invokeSubmitTasksAsync(List<String> tempFilePaths,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
String batchId) throws Exception {
|
||||
invokeSubmitTasksAsync(tempFilePaths, records, batchId, CALLER);
|
||||
}
|
||||
|
||||
private void invokeSubmitTasksAsync(List<String> tempFilePaths,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
String batchId,
|
||||
CallerContext caller) throws Exception {
|
||||
Method method = CcdiFileUploadServiceImpl.class.getDeclaredMethod("submitTasksAsync",
|
||||
Long.class, Integer.class, List.class, List.class, String.class);
|
||||
Long.class, Integer.class, List.class, List.class, String.class, CallerContext.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(service, PROJECT_ID, LSFX_PROJECT_ID, tempFilePaths, records, batchId);
|
||||
method.invoke(service, PROJECT_ID, LSFX_PROJECT_ID, tempFilePaths, records, batchId, caller);
|
||||
}
|
||||
|
||||
private void setField(String fieldName, Object value) throws Exception {
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -48,6 +49,8 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiProjectServiceImplTest {
|
||||
|
||||
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
|
||||
|
||||
@InjectMocks
|
||||
private CcdiProjectServiceImpl service;
|
||||
|
||||
@@ -282,7 +285,7 @@ class CcdiProjectServiceImplTest {
|
||||
dto.setStartDate("2026-01-01");
|
||||
dto.setEndDate("2026-01-31");
|
||||
|
||||
when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(3001));
|
||||
when(lsfxAnalysisClient.getToken(any(CallerContext.class), any())).thenReturn(buildTokenResponse(3001));
|
||||
doAnswer(invocation -> {
|
||||
CcdiProject project = invocation.getArgument(0);
|
||||
project.setProjectId(90L);
|
||||
@@ -291,7 +294,7 @@ class CcdiProjectServiceImplTest {
|
||||
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
CcdiProjectVO project = service.importFromHistory(dto, "tester");
|
||||
CcdiProjectVO project = service.importFromHistory(dto, CALLER);
|
||||
|
||||
assertNotNull(project);
|
||||
assertEquals(90L, project.getProjectId());
|
||||
@@ -320,7 +323,7 @@ class CcdiProjectServiceImplTest {
|
||||
dto.setDescription("测试项目");
|
||||
dto.setConfigType("default");
|
||||
|
||||
when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(2001));
|
||||
when(lsfxAnalysisClient.getToken(any(CallerContext.class), any())).thenReturn(buildTokenResponse(2001));
|
||||
doAnswer(invocation -> {
|
||||
CcdiProject project = invocation.getArgument(0);
|
||||
project.setProjectId(88L);
|
||||
@@ -333,7 +336,7 @@ class CcdiProjectServiceImplTest {
|
||||
logger.addAppender(logAppender);
|
||||
|
||||
try {
|
||||
service.createProject(dto);
|
||||
service.createProject(dto, CALLER);
|
||||
|
||||
assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
|
||||
.anyMatch(message -> message.contains("项目状态初始化")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 业务外部接口日志后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在不改变业务外部接口调用结果的前提下,统一记录流水分析与征信解析请求的发起用户、完整请求、原始响应、HTTP状态、耗时和异常,并提供接口日志分页查询与详情接口。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 新建`sys_api_log`,请求头、请求参数、响应头、响应正文和异常使用`LONGTEXT`,表和字符字段统一使用`utf8mb4_general_ci`。
|
||||
2. 在`ruoyi-system`新增独立Entity、查询DTO、列表VO、详情VO、Mapper和事务Service;列表SQL不读取大字段,详情按日志ID读取完整内容。
|
||||
3. 在`ruoyi-admin`新增`/monitor/apilog/list`与`/monitor/apilog/{logId}`,分别校验`monitor:apilog:list`和`monitor:apilog:query`权限。
|
||||
4. 在`ccdi-lsfx`新增不可变`CallerContext`,替换流水分析、征信解析和`HttpUtil`原有调用签名,所有调用方必须显式传入发起人。
|
||||
5. 批量上传与拉取本行信息在提交异步任务前生成调用用户快照,并贯穿上传、拉取、轮询、状态查询和流水分页查询全链路。
|
||||
6. `HttpUtil`统一获取原始响应文本后再反序列化;非2xx保存状态、响应头和错误正文,网络异常和解析异常同样保存失败日志。
|
||||
7. 认证字段按原文保存;multipart文件仅保存文件名、大小和媒体类型,不读取或保存文件内容。
|
||||
8. 日志写入使用`REQUIRES_NEW`事务;日志序列化或入库失败只记录应用错误,不覆盖原业务结果。
|
||||
9. 通过增量SQL幂等创建表、接口日志菜单和查询权限按钮,不自动写入`sys_role_menu`。
|
||||
10. 流水分析和征信解析测试控制器取消匿名访问,统一校验`ccdi:project:edit`,人工测试调用按当前登录用户记录,不再记为`system`。
|
||||
|
||||
## 验证标准
|
||||
|
||||
- JDK 21下后端主工程编译通过。
|
||||
- 全部`HttpUtil`调用类型均能保存成功或失败日志,原始响应字段不因DTO反序列化丢失。
|
||||
- 异步任务在线程安全上下文清理后仍保存最初发起用户,不同用户连续任务不串号。
|
||||
- 未授权用户不能访问接口日志列表和详情。
|
||||
- 日志入库失败不影响外部接口返回或原始业务异常。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 业务外部接口日志前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在“系统管理 > 日志管理”下新增“接口日志”页面,以紧凑列表和独立详情展示业务外部接口调用情况及完整请求响应。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 新增`src/api/monitor/apilog.js`,封装分页列表和详情接口。
|
||||
2. 新增`src/views/monitor/apilog/index.vue`,支持接口地址、HTTP方法、HTTP调用状态和调用时间筛选。
|
||||
3. 列表展示日志编号、调用账号、接口地址、HTTP方法、HTTP状态、HTTP调用状态、耗时和调用时间。
|
||||
4. 详情按需二次加载,展示调用账号和用户ID、URL、Content-Type、请求头、请求参数、响应头、原始返回正文和异常。
|
||||
5. 请求响应长文本使用等宽字体和固定最大高度;可解析的JSON基于原始字符串进行无损结构美化并按2空格缩进展示,不将数值反序列化后再输出,普通文本保留原始换行,长字段按容器自动折行;使用文本插值,禁止`v-html`。
|
||||
6. 页面只提供查询和详情,不增加删除、清空或导出操作。
|
||||
7. 详情弹窗使用响应式宽度和桌面最大宽度,长URL和长文本不得撑破页面。
|
||||
|
||||
## 验证标准
|
||||
|
||||
- 使用项目`.nvmrc`切换Node版本后,生产构建通过。
|
||||
- 管理员可从真实系统菜单进入接口日志页,列表、筛选、分页和详情正常。
|
||||
- 成功、失败、空响应和超长响应均能稳定展示,JSON缩进、超大整数原值、中文、原始换行和长字段自动折行正确。
|
||||
- 外部响应中的HTML只作为文本显示,不产生脚本或页面节点。
|
||||
- 未授权用户看不到详情入口且直接请求详情接口被拒绝。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 业务外部接口日志实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 新增`sys_api_log`表及“日志管理 > 接口日志”菜单增量SQL。
|
||||
- 新增接口日志持久化、列表查询、详情查询及对应权限控制。
|
||||
- 将流水分析和征信解析调用统一接入完整请求响应日志。
|
||||
- 新增显式调用用户快照,并贯穿项目创建、征信解析、批量上传和拉取本行信息异步链路。
|
||||
- 流水分析和征信解析测试控制器取消匿名访问,改为校验`ccdi:project:edit`并记录当前登录用户。
|
||||
- 新增接口日志前端列表、筛选和完整详情页面。
|
||||
- 接口日志详情支持JSON无损结构美化、2空格缩进展示和长文本自动换行;超大整数不经过JavaScript数值再序列化,非JSON内容保持原文。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `ruoyi-system`:接口日志实体、DTO/VO、Mapper和事务Service。
|
||||
- `ruoyi-admin`:接口日志监控接口。
|
||||
- `ccdi-lsfx`:调用用户上下文、HTTP原始响应处理与日志写入。
|
||||
- `ccdi-project`、`ccdi-info-collection`:调用用户显式透传。
|
||||
- `ruoyi-ui`:接口日志菜单页面和API封装。
|
||||
- `sql/migration`:表结构与菜单权限增量脚本。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- JDK 21主工程编译及`ruoyi-admin`全模块跳过测试打包:已通过。
|
||||
- `ccdi-lsfx`定向测试:20个测试全部通过,覆盖7类HTTP调用、成功原始正文、非2xx、网络异常、空响应、反序列化失败、请求快照失败、multipart文件元数据、日志入库失败、调用用户必填及`NULL/system`系统调用人。
|
||||
- `ccdi-project`定向测试:57个既有回归测试全部通过;新增异步调用人隔离用例后,`CcdiFileUploadServiceImplTest`共33个测试全部通过,验证清理`SecurityContext`后用户A、用户B连续任务不串号。
|
||||
- `ccdi-info-collection`征信调用人透传测试:5个测试全部通过。
|
||||
- 前端生产构建:通过`.nvmrc`切换Node 14.21.3后执行`npm run build:prod`成功,仅保留项目既有资源体积告警。
|
||||
- 数据库迁移:使用`bin/mysql_utf8_exec.sh`连续执行两次均成功;`sys_api_log`表排序规则为`utf8mb4_general_ci`,`caller_username`无默认值,`api_url`为`TEXT`,请求参数和响应正文为`LONGTEXT`;菜单与查询按钮均保持单条。
|
||||
- 真实接口日志:本地Mock下生成6条真实调用日志,覆盖Token、multipart上传、征信URL-encoded发起及结果轮询;日志调用人为`admin`、用户ID为1,multipart仅保存文件名、10423字节大小和媒体类型,详情保留完整原始响应。
|
||||
- 权限验证:未登录请求`/monitor/apilog/list`返回业务码401;管理员列表与详情接口返回正常。
|
||||
- Browser use真实页面验收:应用内浏览器从“系统管理 > 日志管理 > 接口日志”进入成功;列表显示调用账号,按`xfeature`筛选从6条收敛为2条,详情完整展示用户、URL、请求响应头、请求参数、原始正文、HTTP状态及耗时,长文本滚动区域无溢出。
|
||||
- 详情格式化验收:可解析JSON按层级换行缩进展示,`9223372036854775807`等超大整数保持原值,长字段在内容区域内自动折行;异常堆栈等非JSON文本保留原始换行。
|
||||
- 测试清理:Browser use验收完成后删除本轮生成的6条接口日志测试数据,并关闭后端、前端和Mock测试进程。
|
||||
|
||||
## 说明
|
||||
|
||||
- 认证字段按已确认需求原文保存,接口日志访问权限必须严格控制。
|
||||
- `call_status`表示HTTP传输与响应反序列化状态,页面统一标注为“HTTP调用状态”,不表示外部系统业务码结果。
|
||||
- multipart日志仅保存文件元数据,不保存文件字节。
|
||||
- 失败日志的异常字段保存完整异常堆栈,不只保存异常消息。
|
||||
- 日志入库异常不会改变外部接口调用结果。
|
||||
- 临时测试数据、临时测试文件和浏览器验收产物不纳入Git提交范围;既有回归测试因公共方法签名变化产生的适配随业务代码一并交付。
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.system.domain.dto.SysApiLogQueryDTO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogListVO;
|
||||
import com.ruoyi.system.service.ISysApiLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业务外部接口日志。
|
||||
*/
|
||||
@Tag(name = "接口日志")
|
||||
@RestController
|
||||
@RequestMapping("/monitor/apilog")
|
||||
public class SysApiLogController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private ISysApiLogService apiLogService;
|
||||
|
||||
@Operation(summary = "查询接口日志列表")
|
||||
@PreAuthorize("@ss.hasPermi('monitor:apilog:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysApiLogQueryDTO queryDTO) {
|
||||
startPage();
|
||||
List<SysApiLogListVO> list = apiLogService.selectApiLogList(queryDTO);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询接口日志详情")
|
||||
@PreAuthorize("@ss.hasPermi('monitor:apilog:query')")
|
||||
@GetMapping("/{logId}")
|
||||
public AjaxResult detail(@PathVariable Long logId) {
|
||||
return success(apiLogService.selectApiLogById(logId));
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,12 @@
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.system.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 业务外部接口调用日志。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLog {
|
||||
|
||||
private Long logId;
|
||||
private Long callerUserId;
|
||||
private String callerUsername;
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private String contentType;
|
||||
private String requestHeaders;
|
||||
private String requestParams;
|
||||
private Integer responseStatus;
|
||||
private String responseHeaders;
|
||||
private String responseBody;
|
||||
private String callStatus;
|
||||
private String errorMsg;
|
||||
private Long costTime;
|
||||
private Date callTime;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.system.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 接口日志查询条件。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLogQueryDTO {
|
||||
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private String callStatus;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date beginTime;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.system.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 接口日志完整详情。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLogDetailVO {
|
||||
|
||||
private Long logId;
|
||||
private Long callerUserId;
|
||||
private String callerUsername;
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private String contentType;
|
||||
private String requestHeaders;
|
||||
private String requestParams;
|
||||
private Integer responseStatus;
|
||||
private String responseHeaders;
|
||||
private String responseBody;
|
||||
private String callStatus;
|
||||
private String errorMsg;
|
||||
private Long costTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date callTime;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ruoyi.system.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 接口日志列表项,不包含请求响应大字段。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLogListVO {
|
||||
|
||||
private Long logId;
|
||||
private Long callerUserId;
|
||||
private String callerUsername;
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private Integer responseStatus;
|
||||
private String callStatus;
|
||||
private Long costTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date callTime;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.system.mapper;
|
||||
|
||||
import com.ruoyi.system.domain.SysApiLog;
|
||||
import com.ruoyi.system.domain.dto.SysApiLogQueryDTO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogDetailVO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogListVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 接口日志数据层。
|
||||
*/
|
||||
public interface SysApiLogMapper {
|
||||
|
||||
int insertApiLog(SysApiLog apiLog);
|
||||
|
||||
List<SysApiLogListVO> selectApiLogList(SysApiLogQueryDTO queryDTO);
|
||||
|
||||
SysApiLogDetailVO selectApiLogById(Long logId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.system.service;
|
||||
|
||||
import com.ruoyi.system.domain.SysApiLog;
|
||||
import com.ruoyi.system.domain.dto.SysApiLogQueryDTO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogDetailVO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogListVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 接口日志服务。
|
||||
*/
|
||||
public interface ISysApiLogService {
|
||||
|
||||
void recordApiLog(SysApiLog apiLog);
|
||||
|
||||
List<SysApiLogListVO> selectApiLogList(SysApiLogQueryDTO queryDTO);
|
||||
|
||||
SysApiLogDetailVO selectApiLogById(Long logId);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.system.service.impl;
|
||||
|
||||
import com.ruoyi.system.domain.SysApiLog;
|
||||
import com.ruoyi.system.domain.dto.SysApiLogQueryDTO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogDetailVO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogListVO;
|
||||
import com.ruoyi.system.mapper.SysApiLogMapper;
|
||||
import com.ruoyi.system.service.ISysApiLogService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 接口日志服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class SysApiLogServiceImpl implements ISysApiLogService {
|
||||
|
||||
@Resource
|
||||
private SysApiLogMapper apiLogMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void recordApiLog(SysApiLog apiLog) {
|
||||
apiLogMapper.insertApiLog(apiLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SysApiLogListVO> selectApiLogList(SysApiLogQueryDTO queryDTO) {
|
||||
return apiLogMapper.selectApiLogList(queryDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysApiLogDetailVO selectApiLogById(Long logId) {
|
||||
return apiLogMapper.selectApiLogById(logId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.system.mapper.SysApiLogMapper">
|
||||
|
||||
<insert id="insertApiLog" parameterType="com.ruoyi.system.domain.SysApiLog" useGeneratedKeys="true" keyProperty="logId">
|
||||
insert into sys_api_log (
|
||||
caller_user_id, caller_username, api_url, http_method, content_type,
|
||||
request_headers, request_params, response_status, response_headers,
|
||||
response_body, call_status, error_msg, cost_time, call_time
|
||||
) values (
|
||||
#{callerUserId}, #{callerUsername}, #{apiUrl}, #{httpMethod}, #{contentType},
|
||||
#{requestHeaders}, #{requestParams}, #{responseStatus}, #{responseHeaders},
|
||||
#{responseBody}, #{callStatus}, #{errorMsg}, #{costTime}, #{callTime}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<select id="selectApiLogList" parameterType="com.ruoyi.system.domain.dto.SysApiLogQueryDTO"
|
||||
resultType="com.ruoyi.system.domain.vo.SysApiLogListVO">
|
||||
select log_id as logId,
|
||||
caller_user_id as callerUserId,
|
||||
caller_username as callerUsername,
|
||||
api_url as apiUrl,
|
||||
http_method as httpMethod,
|
||||
response_status as responseStatus,
|
||||
call_status as callStatus,
|
||||
cost_time as costTime,
|
||||
call_time as callTime
|
||||
from sys_api_log
|
||||
<where>
|
||||
<if test="apiUrl != null and apiUrl != ''">
|
||||
and api_url like concat('%', #{apiUrl}, '%')
|
||||
</if>
|
||||
<if test="httpMethod != null and httpMethod != ''">
|
||||
and http_method = #{httpMethod}
|
||||
</if>
|
||||
<if test="callStatus != null and callStatus != ''">
|
||||
and call_status = #{callStatus}
|
||||
</if>
|
||||
<if test="beginTime != null">
|
||||
and call_time >= #{beginTime}
|
||||
</if>
|
||||
<if test="endTime != null">
|
||||
and call_time <= #{endTime}
|
||||
</if>
|
||||
</where>
|
||||
order by call_time desc, log_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectApiLogById" parameterType="Long"
|
||||
resultType="com.ruoyi.system.domain.vo.SysApiLogDetailVO">
|
||||
select log_id as logId,
|
||||
caller_user_id as callerUserId,
|
||||
caller_username as callerUsername,
|
||||
api_url as apiUrl,
|
||||
http_method as httpMethod,
|
||||
content_type as contentType,
|
||||
request_headers as requestHeaders,
|
||||
request_params as requestParams,
|
||||
response_status as responseStatus,
|
||||
response_headers as responseHeaders,
|
||||
response_body as responseBody,
|
||||
call_status as callStatus,
|
||||
error_msg as errorMsg,
|
||||
cost_time as costTime,
|
||||
call_time as callTime
|
||||
from sys_api_log
|
||||
where log_id = #{logId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
18
ruoyi-ui/src/api/monitor/apilog.js
Normal file
18
ruoyi-ui/src/api/monitor/apilog.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询业务外部接口日志列表
|
||||
export function listApiLog(query) {
|
||||
return request({
|
||||
url: '/monitor/apilog/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
// 查询业务外部接口日志详情
|
||||
export function getApiLog(logId) {
|
||||
return request({
|
||||
url: '/monitor/apilog/' + logId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
289
ruoyi-ui/src/views/monitor/apilog/index.vue
Normal file
289
ruoyi-ui/src/views/monitor/apilog/index.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form
|
||||
ref="queryForm"
|
||||
:model="queryParams"
|
||||
size="small"
|
||||
:inline="true"
|
||||
v-show="showSearch"
|
||||
label-width="82px"
|
||||
>
|
||||
<el-form-item label="接口地址" prop="apiUrl">
|
||||
<el-input
|
||||
v-model="queryParams.apiUrl"
|
||||
placeholder="请输入接口地址"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter.native="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="HTTP方法" prop="httpMethod">
|
||||
<el-select v-model="queryParams.httpMethod" placeholder="全部" clearable style="width: 140px">
|
||||
<el-option label="GET" value="GET" />
|
||||
<el-option label="POST" value="POST" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="HTTP调用状态" prop="callStatus">
|
||||
<el-select v-model="queryParams.callStatus" placeholder="全部" clearable style="width: 140px">
|
||||
<el-option label="成功" value="0" />
|
||||
<el-option label="失败" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="调用时间">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
value-format="yyyy-MM-dd HH:mm:ss"
|
||||
range-separator="-"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:default-time="['00:00:00', '23:59:59']"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="日志编号" align="center" prop="logId" width="100" />
|
||||
<el-table-column label="调用账号" align="center" prop="callerUsername" width="130" show-overflow-tooltip />
|
||||
<el-table-column label="接口地址" prop="apiUrl" min-width="320" show-overflow-tooltip />
|
||||
<el-table-column label="方法" align="center" prop="httpMethod" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" effect="plain">{{ scope.row.httpMethod }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="HTTP状态" align="center" prop="responseStatus" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.responseStatus == null ? '-' : scope.row.responseStatus }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="HTTP调用状态" align="center" prop="callStatus" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.callStatus === '0'" type="success" size="mini">成功</el-tag>
|
||||
<el-tag v-else type="danger" size="mini">失败</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时" align="right" prop="costTime" width="110">
|
||||
<template slot-scope="scope">{{ scope.row.costTime }} 毫秒</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="调用时间" align="center" prop="callTime" width="165">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.callTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="90">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
v-hasPermi="['monitor:apilog:query']"
|
||||
@click="handleView(scope.row)"
|
||||
>详细</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
:page.sync="queryParams.pageNum"
|
||||
:limit.sync="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
title="接口日志详细"
|
||||
:visible.sync="open"
|
||||
width="90%"
|
||||
custom-class="api-log-dialog"
|
||||
append-to-body
|
||||
>
|
||||
<div v-loading="detailLoading" class="api-log-detail">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="调用账号">{{ detail.callerUsername || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID">{{ detail.callerUserId == null ? '-' : detail.callerUserId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="HTTP方法">{{ detail.httpMethod || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="Content-Type">{{ detail.contentType || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="HTTP状态">{{ detail.responseStatus == null ? '-' : detail.responseStatus }}</el-descriptions-item>
|
||||
<el-descriptions-item label="HTTP调用状态">{{ detail.callStatus === '0' ? '成功' : '失败' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="耗时">{{ detail.costTime == null ? '-' : detail.costTime + ' 毫秒' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="调用时间">{{ parseTime(detail.callTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="接口地址" :span="2">{{ detail.apiUrl || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<section class="detail-section">
|
||||
<h4>请求头</h4>
|
||||
<pre class="log-content">{{ formatLogContent(detail.requestHeaders) }}</pre>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>请求参数</h4>
|
||||
<pre class="log-content">{{ formatLogContent(detail.requestParams) }}</pre>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>响应头</h4>
|
||||
<pre class="log-content">{{ formatLogContent(detail.responseHeaders) }}</pre>
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<h4>原始返回正文</h4>
|
||||
<pre class="log-content log-content-large">{{ formatLogContent(detail.responseBody) }}</pre>
|
||||
</section>
|
||||
<section v-if="detail.errorMsg" class="detail-section">
|
||||
<h4 class="error-title">异常信息</h4>
|
||||
<pre class="log-content error-content">{{ formatLogContent(detail.errorMsg) }}</pre>
|
||||
</section>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="open = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { js_beautify } from 'js-beautify'
|
||||
import { getApiLog, listApiLog } from '@/api/monitor/apilog'
|
||||
|
||||
export default {
|
||||
name: 'ApiLog',
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
detailLoading: false,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
list: [],
|
||||
open: false,
|
||||
detail: {},
|
||||
dateRange: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
apiUrl: undefined,
|
||||
httpMethod: undefined,
|
||||
callStatus: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
},
|
||||
methods: {
|
||||
getList() {
|
||||
this.loading = true
|
||||
const query = { ...this.queryParams }
|
||||
if (this.dateRange && this.dateRange.length === 2) {
|
||||
query.beginTime = this.dateRange[0]
|
||||
query.endTime = this.dateRange[1]
|
||||
}
|
||||
listApiLog(query).then(response => {
|
||||
this.list = response.rows
|
||||
this.total = response.total
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.dateRange = []
|
||||
this.resetForm('queryForm')
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
formatLogContent(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
try {
|
||||
JSON.parse(value)
|
||||
return js_beautify(value, { indent_size: 2 })
|
||||
} catch (error) {
|
||||
return value
|
||||
}
|
||||
},
|
||||
handleView(row) {
|
||||
this.open = true
|
||||
this.detail = {}
|
||||
this.detailLoading = true
|
||||
getApiLog(row.logId).then(response => {
|
||||
this.detail = response.data || {}
|
||||
}).finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-log-detail {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
::v-deep .api-log-dialog {
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
::v-deep .api-log-dialog .el-descriptions-item__content {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.detail-section h4 {
|
||||
margin: 0 0 8px;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-height: 220px;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 6px;
|
||||
background: #f7f8fa;
|
||||
color: #303133;
|
||||
font-family: Monaco, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
letter-spacing: 0;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.log-content-large {
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
color: #c03639 !important;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
border-color: #f3c5c7;
|
||||
background: #fff6f6;
|
||||
color: #9f2d30;
|
||||
}
|
||||
</style>
|
||||
75
sql/migration/2026-07-20-add-external-api-log.sql
Normal file
75
sql/migration/2026-07-20-add-external-api-log.sql
Normal file
@@ -0,0 +1,75 @@
|
||||
-- 业务外部接口调用日志及菜单
|
||||
-- 可重复执行,不自动授予普通角色权限
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sys_api_log (
|
||||
log_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '日志主键',
|
||||
caller_user_id BIGINT NULL COMMENT '调用发起用户ID',
|
||||
caller_username VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '调用发起账号',
|
||||
api_url TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '接口完整地址',
|
||||
http_method VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'HTTP方法',
|
||||
content_type VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求内容类型',
|
||||
request_headers LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '完整请求头',
|
||||
request_params LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '完整请求参数',
|
||||
response_status INT NULL COMMENT 'HTTP响应状态码',
|
||||
response_headers LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '完整响应头',
|
||||
response_body LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '完整响应正文',
|
||||
call_status CHAR(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'HTTP调用状态(0成功 1失败)',
|
||||
error_msg LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '异常信息',
|
||||
cost_time BIGINT NOT NULL DEFAULT 0 COMMENT 'HTTP调用耗时(毫秒)',
|
||||
call_time DATETIME NOT NULL COMMENT '调用开始时间',
|
||||
PRIMARY KEY (log_id),
|
||||
KEY idx_sys_api_log_time (call_time, log_id),
|
||||
KEY idx_sys_api_log_status_time (call_status, call_time, log_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='业务外部接口调用日志';
|
||||
|
||||
SET @log_parent_id = (
|
||||
SELECT menu_id
|
||||
FROM sys_menu
|
||||
WHERE menu_name = '日志管理'
|
||||
AND menu_type = 'M'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_name, parent_id, order_num, path, component, query, route_name,
|
||||
is_frame, is_cache, menu_type, visible, status, perms, icon,
|
||||
create_by, create_time, update_by, update_time, remark
|
||||
)
|
||||
SELECT
|
||||
'接口日志', @log_parent_id, 3, 'apilog', 'monitor/apilog/index', '', '',
|
||||
1, 0, 'C', '0', '0', 'monitor:apilog:list', 'log',
|
||||
'admin', NOW(), '', NULL, '业务外部接口日志菜单'
|
||||
FROM dual
|
||||
WHERE @log_parent_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu
|
||||
WHERE parent_id = @log_parent_id
|
||||
AND path = 'apilog'
|
||||
);
|
||||
|
||||
SET @api_log_menu_id = (
|
||||
SELECT menu_id
|
||||
FROM sys_menu
|
||||
WHERE parent_id = @log_parent_id
|
||||
AND path = 'apilog'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO sys_menu (
|
||||
menu_name, parent_id, order_num, path, component, query, route_name,
|
||||
is_frame, is_cache, menu_type, visible, status, perms, icon,
|
||||
create_by, create_time, update_by, update_time, remark
|
||||
)
|
||||
SELECT
|
||||
'接口日志查询', @api_log_menu_id, 1, '#', '', '', '',
|
||||
1, 0, 'F', '0', '0', 'monitor:apilog:query', '#',
|
||||
'admin', NOW(), '', NULL, ''
|
||||
FROM dual
|
||||
WHERE @api_log_menu_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_menu
|
||||
WHERE parent_id = @api_log_menu_id
|
||||
AND perms = 'monitor:apilog:query'
|
||||
);
|
||||
Reference in New Issue
Block a user