From 078f3244947d37da84765df442e75db4bcfa7939 Mon Sep 17 00:00:00 2001 From: "jiazheng.zhao" <2624533894@qq.com> Date: Tue, 11 Aug 2026 10:10:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 12 + .../application/ConfirmationStatusResult.java | 17 ++ .../DenominationDecompositionResult.java | 24 ++ .../application/OperationBooleanResult.java | 17 ++ .../application/OperationIdStatusResult.java | 24 ++ .../application/OperationMessageResult.java | 17 ++ .../application/OperationStatusResult.java | 17 ++ .../application/OperationValueResult.java | 17 ++ .../StandardCurrencyOperationResult.java | 22 ++ ...alBankInstitutionIdentifierController.java | 7 +- ...rencyGenerationVerificationController.java | 29 ++- .../rest/StandardCurrencyController.java | 25 +- .../digitalrmb/shared/config/IFileUtil.java | 89 +++++++ .../shared/config/LocalFileUtil.java | 178 ++++++++++++++ .../shared/config/ResultDataEntity.java | 65 +++++ .../shared/config/ResultEntity.java | 44 ++++ .../interfaces/FileManagementController.java | 225 ++++++++++++++++++ src/main/resources/application-dev.yml | 4 + ...generation_verification_unique_upgrade.sql | 9 + .../institution_identifier_training.sql | 1 + ...onInformationIdentifierControllerTest.java | 55 ++--- 21 files changed, 842 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/ConfirmationStatusResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/DenominationDecompositionResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationBooleanResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationIdStatusResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationMessageResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationStatusResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationValueResult.java create mode 100644 src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyOperationResult.java create mode 100644 src/main/java/com/yau/digitalrmb/shared/config/IFileUtil.java create mode 100644 src/main/java/com/yau/digitalrmb/shared/config/LocalFileUtil.java create mode 100644 src/main/java/com/yau/digitalrmb/shared/config/ResultDataEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/shared/config/ResultEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/shared/interfaces/FileManagementController.java create mode 100644 src/main/resources/db/manual/currency_generation_verification_unique_upgrade.sql diff --git a/pom.xml b/pom.xml index aec2eca..a325a7b 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,13 @@ org.springframework.boot spring-boot-starter-validation + + + + + cn.hutool + hutool-all + 5.8.16 org.springframework.boot @@ -104,6 +111,11 @@ spring-security-test test + + io.projectreactor + reactor-core + 3.8.5 + diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/ConfirmationStatusResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/ConfirmationStatusResult.java new file mode 100644 index 0000000..6ba0f62 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/ConfirmationStatusResult.java @@ -0,0 +1,17 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "报文确认状态") +public class ConfirmationStatusResult { + @Schema(description = "报文确认状态", allowableValues = {"待确认", "已确认"}) + private final String confirmationStatus; + + public ConfirmationStatusResult(String confirmationStatus) { + this.confirmationStatus = confirmationStatus; + } + + public String getConfirmationStatus() { + return confirmationStatus; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/DenominationDecompositionResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/DenominationDecompositionResult.java new file mode 100644 index 0000000..b57ac3e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/DenominationDecompositionResult.java @@ -0,0 +1,24 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; + +@Schema(description = "面额拆解结果") +public class DenominationDecompositionResult { + private final Integer totalCount; + private final List breakdown; + + public DenominationDecompositionResult(Integer totalCount, List breakdown) { + this.totalCount = totalCount; + this.breakdown = breakdown; + } + + public Integer getTotalCount() { + return totalCount; + } + + public List getBreakdown() { + return breakdown; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationBooleanResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationBooleanResult.java new file mode 100644 index 0000000..e5aa03f --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationBooleanResult.java @@ -0,0 +1,17 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "操作校验结果") +public class OperationBooleanResult { + @Schema(description = "本次校验是否通过") + private final boolean valid; + + public OperationBooleanResult(boolean valid) { + this.valid = valid; + } + + public boolean isValid() { + return valid; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationIdStatusResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationIdStatusResult.java new file mode 100644 index 0000000..2265b71 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationIdStatusResult.java @@ -0,0 +1,24 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "创建或接收操作结果") +public class OperationIdStatusResult { + @Schema(description = "后续操作所需的业务记录编号") + private final Long verificationId; + @Schema(description = "本次操作完成后的状态") + private final String status; + + public OperationIdStatusResult(Long id, String status) { + this.verificationId = id; + this.status = status; + } + + public Long getVerificationId() { + return verificationId; + } + + public String getStatus() { + return status; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationMessageResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationMessageResult.java new file mode 100644 index 0000000..2fcc18c --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationMessageResult.java @@ -0,0 +1,17 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "操作生成的报文") +public class OperationMessageResult { + @Schema(description = "本次操作生成的报文") + private final String message; + + public OperationMessageResult(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationStatusResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationStatusResult.java new file mode 100644 index 0000000..80d6bc5 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationStatusResult.java @@ -0,0 +1,17 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "操作状态") +public class OperationStatusResult { + @Schema(description = "本次操作完成后的状态") + private final String status; + + public OperationStatusResult(String status) { + this.status = status; + } + + public String getStatus() { + return status; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationValueResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationValueResult.java new file mode 100644 index 0000000..9182bb6 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/OperationValueResult.java @@ -0,0 +1,17 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "操作生成值") +public class OperationValueResult { + @Schema(description = "本次操作生成的值") + private final String value; + + public OperationValueResult(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyOperationResult.java b/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyOperationResult.java new file mode 100644 index 0000000..d72461f --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/application/StandardCurrencyOperationResult.java @@ -0,0 +1,22 @@ +package com.yau.digitalrmb.institutionidentity.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "标准币串步骤操作结果") +public class StandardCurrencyOperationResult { + private final String status; + private final Integer totalCount; + + public StandardCurrencyOperationResult(String status, Integer totalCount) { + this.status = status; + this.totalCount = totalCount; + } + + public String getStatus() { + return status; + } + + public Integer getTotalCount() { + return totalCount; + } +} diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CentralBankInstitutionIdentifierController.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CentralBankInstitutionIdentifierController.java index 6453e76..f8f6618 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CentralBankInstitutionIdentifierController.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CentralBankInstitutionIdentifierController.java @@ -11,6 +11,7 @@ import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResu import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.InstitutionVerificationProcessResult; +import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult; import com.yau.digitalrmb.institutionidentity.application.SignatureResult; import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmInstitutionIdentifierRequest; import com.yau.digitalrmb.institutionidentity.interfaces.dto.VerifyInstitutionApplicationRequest; @@ -102,10 +103,10 @@ public class CentralBankInstitutionIdentifierController { @PostMapping("/applications/feedback") @Operation(summary = "央行信息反馈", description = "将已确权的最终机构标识同步到商业银行端,商业银行状态由1变为2。") - public ApiResponse feedback() { + public ApiResponse feedback() { JwtUser jwtUser = AuthContextHolder.get(); - return ApiResponse.success(feedbackService.feedback( - InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId()); + feedbackService.feedback(InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()); + return ApiResponse.success(new OperationStatusResult("FEEDBACK_SENT"), traceId()); } private String traceId() { diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationVerificationController.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationVerificationController.java index 1f4909f..b2e87a7 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationVerificationController.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/CurrencyGenerationVerificationController.java @@ -1,5 +1,6 @@ package com.yau.digitalrmb.institutionidentity.interfaces.rest; +import com.yau.digitalrmb.institutionidentity.application.ConfirmationStatusResult; import com.yau.digitalrmb.institutionidentity.application.CurrencyConfirmationMessageResult; import com.yau.digitalrmb.institutionidentity.application.CurrencyConfirmationSignatureResult; import com.yau.digitalrmb.institutionidentity.application.CurrencyDigestResult; @@ -8,6 +9,7 @@ import com.yau.digitalrmb.institutionidentity.application.CurrencyQuotaVerificat import com.yau.digitalrmb.institutionidentity.application.CurrencyGenerationVerificationService; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.OperationBooleanResult; +import com.yau.digitalrmb.institutionidentity.application.OperationIdStatusResult; import com.yau.digitalrmb.institutionidentity.application.OperationMessageResult; import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult; import com.yau.digitalrmb.institutionidentity.application.SignatureResult; @@ -36,12 +38,13 @@ public class CurrencyGenerationVerificationController { @PostMapping("/steps/receive") @Operation(summary = "接收步骤二请求报文", description = "对应页面“接收步骤二请求报文”按钮。步骤二必须达到RECEIVED;重复点击时复用verificationId,并将步骤三恢复到RECEIVED,清空后续按钮产生的数据。") - public ApiResponse receive( + public ApiResponse receive( @Valid @RequestBody ReceiveCurrencyVerificationRequest request) { JwtUser user = AuthContextHolder.get(); CurrencyGenerationVerificationResult result = service.receive(request.getRequestId(), InstitutionKeySubject.from(user), user.getUsername()); - return ApiResponse.success(new OperationStatusResult(result.getStatus()), MDC.get(TraceIdFilter.MDC_KEY)); + return ApiResponse.success(new OperationIdStatusResult(result.getVerificationId(), result.getStatus()), + MDC.get(TraceIdFilter.MDC_KEY)); } @PostMapping("/steps/verify-signature") @@ -103,27 +106,33 @@ public class CurrencyGenerationVerificationController { @PostMapping("/steps/package-response") @Operation(summary = "打包确认报文", description = "对应页面“打包报文”按钮,只组装确认报文,不执行返回。") - public ApiResponse packageResponse( + public ApiResponse packageResponse( @Valid @RequestBody CurrencyVerificationStepRequest request) { JwtUser user = AuthContextHolder.get(); - return ok(service.packageResponse(request.getVerificationId(), - InstitutionKeySubject.from(user), user.getUsername())); + CurrencyGenerationVerificationResult result = service.packageResponse(request.getVerificationId(), + InstitutionKeySubject.from(user), user.getUsername()); + return ApiResponse.success(new OperationMessageResult(result.getConfirmationMessage()), + MDC.get(TraceIdFilter.MDC_KEY)); } @PostMapping("/steps/return-response") @Operation(summary = "返回确认报文", description = "对应页面“返回确认报文”按钮,只返回已打包完成的确认报文。") - public ApiResponse returnResponse( + public ApiResponse returnResponse( @Valid @RequestBody CurrencyVerificationStepRequest request) { JwtUser user = AuthContextHolder.get(); - return ok(service.returnResponse(request.getVerificationId(), InstitutionKeySubject.from(user), user.getUsername())); + CurrencyGenerationVerificationResult result = service.returnResponse(request.getVerificationId(), + InstitutionKeySubject.from(user), user.getUsername()); + return ApiResponse.success(new OperationStatusResult(result.getStatus()), MDC.get(TraceIdFilter.MDC_KEY)); } @PostMapping("/steps/receive-confirmation") @Operation(summary = "商业银行接收报文确认信息", description = "对应商业银行端“接收报文确认信息”按钮。根据当前Token查询本次实训数据,confirmationStatus在央行未返回时为“待确认”,已返回时为“已确认”;查询不改变流程状态。") - public ApiResponse receiveConfirmation() { - return ApiResponse.success(service.receiveConfirmation( - InstitutionKeySubject.from(AuthContextHolder.get())), MDC.get(TraceIdFilter.MDC_KEY)); + public ApiResponse receiveConfirmation() { + CurrencyConfirmationMessageResult result = service.receiveConfirmation( + InstitutionKeySubject.from(AuthContextHolder.get())); + return ApiResponse.success(new ConfirmationStatusResult(result.getConfirmationStatus()), + MDC.get(TraceIdFilter.MDC_KEY)); } @GetMapping("/current") diff --git a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/StandardCurrencyController.java b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/StandardCurrencyController.java index d930468..22f95fc 100644 --- a/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/StandardCurrencyController.java +++ b/src/main/java/com/yau/digitalrmb/institutionidentity/interfaces/rest/StandardCurrencyController.java @@ -1,8 +1,10 @@ package com.yau.digitalrmb.institutionidentity.interfaces.rest; +import com.yau.digitalrmb.institutionidentity.application.DenominationDecompositionResult; import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyBatchResult; import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyListItemResult; +import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyOperationResult; import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyService; import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyRuleRequest; import com.yau.digitalrmb.shared.api.ApiResponse; @@ -33,25 +35,32 @@ public class StandardCurrencyController { @PostMapping("/steps/decompose") @Operation(summary = "执行面额拆解", description = "无需请求体。后端按当前JWT主体查询步骤八已确认额度控制位金额,并按100、50、20、10、5、1、0.5、0.2、0.1、0.05、0.01从大到小拆解。") - public ApiResponse decompose() { + public ApiResponse decompose() { JwtUser user = AuthContextHolder.get(); - return ApiResponse.success(service.decompose( - InstitutionKeySubject.from(user), user.getUsername()), traceId()); + StandardCurrencyBatchResult result = service.decompose( + InstitutionKeySubject.from(user), user.getUsername()); + return ApiResponse.success(new DenominationDecompositionResult( + result.getTotalCount(), result.getBreakdown()), traceId()); } @PostMapping("/steps/confirm-rule") @Operation(summary = "确认币串生成规则", description = "校验DC前缀、币串ID规则和待生效初始状态。") - public ApiResponse confirmRule(@Valid @RequestBody CurrencyRuleRequest request) { + public ApiResponse confirmRule(@Valid @RequestBody CurrencyRuleRequest request) { JwtUser user = AuthContextHolder.get(); - return ApiResponse.success(service.confirmRule(request.getPrefix(), request.getIdRule(), - request.getCurrencyStatus(), InstitutionKeySubject.from(user), user.getUsername()), traceId()); + StandardCurrencyBatchResult result = service.confirmRule(request.getPrefix(), request.getIdRule(), + request.getCurrencyStatus(), InstitutionKeySubject.from(user), user.getUsername()); + return ApiResponse.success(new StandardCurrencyOperationResult( + result.getStatus(), result.getTotalCount()), traceId()); } @PostMapping("/steps/generate") @Operation(summary = "逐枚生成标准面额币串", description = "根据拆解清单为每一枚标准面额生成唯一币串。") - public ApiResponse generate() { + public ApiResponse generate() { JwtUser user = AuthContextHolder.get(); - return ApiResponse.success(service.generate(InstitutionKeySubject.from(user), user.getUsername()), traceId()); + StandardCurrencyBatchResult result = service.generate( + InstitutionKeySubject.from(user), user.getUsername()); + return ApiResponse.success(new StandardCurrencyOperationResult( + result.getStatus(), result.getTotalCount()), traceId()); } @GetMapping("/currencies") diff --git a/src/main/java/com/yau/digitalrmb/shared/config/IFileUtil.java b/src/main/java/com/yau/digitalrmb/shared/config/IFileUtil.java new file mode 100644 index 0000000..bee99cf --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/shared/config/IFileUtil.java @@ -0,0 +1,89 @@ +package com.yau.digitalrmb.shared.config; + + +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; + +/** + * 文件上传下载接口定义 + * + * @author 陈沅 + */ + +public interface IFileUtil { + + + + /** + * 从http请求上传文件 + * + * @param file form传参http文件对象 + * @return 返回文件在服务器上保存的相对路径 + */ + String upload(MultipartFile file); + + /** + * 根据文件流上传文件 + * + * @param objectName 指定文件名(带扩展名) + * @param fileIn 文件流 + * @return 返回文件在服务器上保存的相对路径 + */ + String upload(String objectName, InputStream fileIn); + + /** + * 删除服务器上的一个文件 + * + * @param path 文件在服务器上保存的相对路径 + * @return 返回true则表示删除成功 + */ + boolean remove(String path); + + /** + * 下载文件流,指定文件名 + * + * @param response + * @param fileName 指定下载文件名称 + * @param filePath 文件保存的相对路径 + */ + void download(HttpServletResponse response, String fileName, String filePath); + + /** + * 下载文件流,不指定文件名 + * + * @param response + * @param filePath 文件保存的相对路径 + */ + void download(HttpServletResponse response, String filePath); + + /** + * 相对路径 + * + * @param relativePath 文件保存的相对路径 + * @return 文件保存的绝对路径 + */ + String getFullPath(String relativePath); + + + /** + * 获取文件后缀名 + * + * @param fileName 文件名称 + * @return suffix + */ + String getSuffix(String fileName); + + + /** + * 获取文件存储至磁盘的相对路径 + * + * @param fileName 文件名 + * @param suffix 动态拼接的文件后缀,若该值为空则不拼接 + * @return 相对路径 + */ + String getDiskRelativePath(String fileName, String suffix); + +} diff --git a/src/main/java/com/yau/digitalrmb/shared/config/LocalFileUtil.java b/src/main/java/com/yau/digitalrmb/shared/config/LocalFileUtil.java new file mode 100644 index 0000000..3ee725a --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/shared/config/LocalFileUtil.java @@ -0,0 +1,178 @@ +package com.yau.digitalrmb.shared.config; + +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.IdUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +@Component +public class LocalFileUtil implements IFileUtil{ + + private final static List excludeSp = Arrays.asList("exe", "bin", "sh"); + + private final String localPath; + + public LocalFileUtil(@Value("${file.path}")String localPath) { + this.localPath = localPath; + } + + @Override + public String upload(MultipartFile file) { + return upload(null, file); + } + + @Override + public String upload(String objectName, InputStream fileIn) { + Assert.notNull(fileIn, "文件不能为空"); + Assert.hasText(objectName, "文件名为空"); + Assert.isTrue(objectName.lastIndexOf(".") > 0, "文件名称需携带扩展后缀"); + FileOutputStream out = null; + try { + String relativePath = getDiskRelativePath(objectName, null); + File file = new File(getFullPath(relativePath)); + file.getParentFile().mkdirs(); + out = new FileOutputStream(file); + byte[] buff = new byte[4096]; + int len; + while ((len = fileIn.read(buff)) != -1) { + out.write(buff, 0, len); + } + out.flush(); + return relativePath; + } catch (IOException e) { + e.printStackTrace(); + throw new IllegalArgumentException("上传文件失败,IO错误"); + } finally { + try { + fileIn.close(); + if (out != null) out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public boolean remove(String path) { + if (!StringUtils.hasText(path)) return false; + File file = new File(getFullPath(path)); + if (!file.exists()) return false; + return file.delete(); + } + + @Override + public void download(HttpServletResponse response, String fileName, String relativePath) { + Assert.hasText(relativePath, "路径为空"); + System.out.println("------------------------------------" + getFullPath(relativePath)); + File file = new File(getFullPath(relativePath)); + Assert.isTrue(file.exists(), "附件不存在"); + String sp = relativePath.substring(relativePath.lastIndexOf(".") + 1); + Assert.isTrue(!excludeSp.contains(sp.toLowerCase()), "文件类型错误"); + if (!StringUtils.hasText(fileName)) { + fileName = file.getName().substring(0, file.getName().lastIndexOf(".")); + } + InputStream fis = null; + try { + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode((fileName + "." + sp), CharsetUtil.UTF_8)); + fis = new BufferedInputStream(Files.newInputStream(file.toPath())); + response.addHeader("Content-Length", String.valueOf(fis.available())); + byte[] buff = new byte[4096]; + int len; + while ((len = fis.read(buff)) != -1) { + response.getOutputStream().write(buff, 0, len); + } + } catch (Exception e) { + throw new IllegalArgumentException("下载文件失败: " + e.getMessage()); + } finally { + try { + if (fis != null) fis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void download(HttpServletResponse response, String relativePath) { + download(response, null, relativePath); + } + + @Override + public String getFullPath(String relativePath) { + if (!relativePath.startsWith("/")) { + relativePath = "/" + relativePath; + } + return localPath + relativePath; + } + + @Override + public String getSuffix(String fileName) { + Assert.hasText(fileName, "文件名不存在!"); + if (fileName.lastIndexOf(".") < 0) { + return null; + } + return fileName.substring(fileName.lastIndexOf(".") + 1); + } + + public String upload(String fileName, MultipartFile file) { + return upload(fileName, file, null); + } + + + @Override + public String getDiskRelativePath(String fileName, String suffix) { + Calendar c = Calendar.getInstance(); + Date d = new Date(); + c.setTime(d); + String year = Integer.toString(c.get(Calendar.YEAR)); + String month = Integer.toString(c.get(Calendar.MONTH) + 1); + String day = Integer.toString(c.get(Calendar.DATE)); + StringBuilder path = new StringBuilder(); + path.append("/").append(year) + .append("/").append(month) + .append("/").append(day) + .append("/").append(fileName); + if (StringUtils.hasText(suffix)) path.append(".").append(suffix); + return path.toString(); + } + + + private String upload(String fileName, MultipartFile file, String relativePath) { + Assert.isTrue(!file.isEmpty(), "文件不存在"); + String originalFilename = file.getOriginalFilename(); + if (fileName == null) fileName = IdUtil.simpleUUID(); + String sp = getSuffix(originalFilename); + Assert.notNull(sp, "文件类型错误"); + Assert.isTrue(!excludeSp.contains(sp.toLowerCase()), "文件类型错误"); + try { + String filePath; + if (relativePath == null) { + filePath = getDiskRelativePath(fileName, sp); + } else { + relativePath = relativePath.endsWith("/") ? relativePath : relativePath + "/"; + filePath = relativePath + fileName + "." + sp; + } + + File destFile = new File(getFullPath(filePath)); + destFile.getParentFile().mkdirs(); + file.transferTo(destFile); + return filePath; + } catch (FileNotFoundException e) { + throw new IllegalArgumentException("上传文件失败,FileNotFound错误"); + } catch (IOException e) { + throw new IllegalArgumentException("上传文件失败,IO错误"); + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/shared/config/ResultDataEntity.java b/src/main/java/com/yau/digitalrmb/shared/config/ResultDataEntity.java new file mode 100644 index 0000000..f95ef60 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/shared/config/ResultDataEntity.java @@ -0,0 +1,65 @@ +package com.yau.digitalrmb.shared.config; + +import lombok.Getter; +import org.springframework.http.HttpStatus; +import reactor.core.publisher.Flux; + + +/** + * 出参对象统一封装 + */ +@Getter +public class ResultDataEntity { + /** + * 状态码 + */ + private final Integer code; + /** + * 异常提示 + */ +// @JsonInclude(JsonInclude.Include.NON_NULL) + private String msg = null; + /** + * 返回体对象 + */ +// @JsonInclude(JsonInclude.Include.NON_NULL) + private T data = null; + + + private Flux flow; + + public ResultDataEntity(T data) { + this.code = HttpStatus.OK.value(); + this.data = data; + } + + public ResultDataEntity(HttpStatus status, T data) { + this.code = status.value(); + this.data = data; + } + + public ResultDataEntity(HttpStatus status, Flux data) { + this.code = status.value(); + this.flow = data; + } + + public ResultDataEntity(HttpStatus status, String msg, T data) { + this.code = status.value(); + this.msg = msg; + this.data = data; + } + + public ResultDataEntity(String msg) { + this.code = HttpStatus.OK.value(); + this.msg = msg; + } + + public ResultDataEntity(HttpStatus status, String msg) { + this.code = status.value(); + this.msg = msg; + } + + public ResultDataEntity(HttpStatus status) { + this.code = status.value(); + } +} diff --git a/src/main/java/com/yau/digitalrmb/shared/config/ResultEntity.java b/src/main/java/com/yau/digitalrmb/shared/config/ResultEntity.java new file mode 100644 index 0000000..6c585d9 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/shared/config/ResultEntity.java @@ -0,0 +1,44 @@ +package com.yau.digitalrmb.shared.config; + +import io.swagger.annotations.ApiModel; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import reactor.core.publisher.Flux; + + +/** + * 对ResponseEntity做了简单包装 + * + */ +@ApiModel("API返回对象") +@Getter +public class ResultEntity extends ResponseEntity> { + public ResultEntity(T data) { + super(new ResultDataEntity<>(HttpStatus.OK, data), HttpStatus.OK); + } + + public ResultEntity(HttpStatus status, T data) { + super(new ResultDataEntity<>(status, data), status); + } + + public ResultEntity(HttpStatus status, String msg, T data) { + super(new ResultDataEntity<>(status, msg, data), status); + } + + public ResultEntity(String msg) { + super(new ResultDataEntity<>(HttpStatus.OK, msg), HttpStatus.OK); + } + + public ResultEntity(HttpStatus status, String msg) { + super(new ResultDataEntity<>(status, msg), status); + } + + public ResultEntity(HttpStatus status) { + super(new ResultDataEntity<>(status), status); + } + + public ResultEntity(HttpStatus status, Flux flow) { + super(new ResultDataEntity<>(status, flow), status); + } +} diff --git a/src/main/java/com/yau/digitalrmb/shared/interfaces/FileManagementController.java b/src/main/java/com/yau/digitalrmb/shared/interfaces/FileManagementController.java new file mode 100644 index 0000000..39c0f9d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/shared/interfaces/FileManagementController.java @@ -0,0 +1,225 @@ +package com.yau.digitalrmb.shared.interfaces; + + +import com.yau.digitalrmb.shared.config.IFileUtil; +import com.yau.digitalrmb.shared.config.ResultEntity; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * 文件管理控制器 + * 提供文件上传、下载、删除等基础功能 + */ +@Slf4j +@Tag(name = "文件管理") +@RestController +@Validated +public class FileManagementController { + + @Autowired + private IFileUtil fileUtil; + + /** + * 上传文件 + * 支持可重复调用,每次上传都会生成新的文件路径 + */ + @Operation(summary = "上传文件") + @PostMapping("api/file/upload") + + public ResultEntity> uploadFile( + @ApiParam("文件对象") @RequestPart MultipartFile file) { + try { + if (file == null || file.isEmpty()) { + return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件不能为空"); + } + + String originalFilename = file.getOriginalFilename(); + if (!StringUtils.hasText(originalFilename)) { + return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件名不能为空"); + } + + String suffix = fileUtil.getSuffix(originalFilename); + if (!StringUtils.hasText(suffix)) { + return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件类型错误,文件必须包含扩展名"); + } + + String relativePath = fileUtil.upload(file); + + Map result = new HashMap<>(); + result.put("relativePath", relativePath); + result.put("originalFilename", originalFilename); + result.put("fileSize", String.valueOf(file.getSize())); + result.put("suffix", suffix); + + log.info("文件上传成功: 原始文件名={}, 保存路径={}, 文件大小={}", + originalFilename, relativePath, file.getSize()); + + return new ResultEntity<>(result); + } catch (IllegalArgumentException e) { + log.error("文件上传失败: {}", e.getMessage()); + return new ResultEntity<>(HttpStatus.BAD_REQUEST, e.getMessage()); + } catch (Exception e) { + log.error("文件上传异常", e); + return new ResultEntity<>(HttpStatus.INTERNAL_SERVER_ERROR, "文件上传失败: " + e.getMessage()); + } + } + + /** + * 下载文件 + * 使用文件原始名称 + */ + @Operation(summary = "下载文件(使用原始文件名)") + @GetMapping("api/file/download") + + public void downloadFile( + @ApiParam("文件相对路径") @RequestParam String relativePath, + HttpServletResponse response) { + try { + if (!StringUtils.hasText(relativePath)) { + throw new IllegalArgumentException("文件路径不能为空"); + } + + log.info("文件下载请求: 路径={}", relativePath); + fileUtil.download(response, relativePath); + + } catch (IllegalArgumentException e) { + log.error("文件下载失败: {}", e.getMessage()); + try { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"message\":\"" + e.getMessage() + "\"}"); + } catch (Exception ex) { + log.error("写入错误响应失败", ex); + } + } catch (Exception e) { + log.error("文件下载异常", e); + try { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"message\":\"文件下载失败: " + e.getMessage() + "\"}"); + } catch (Exception ex) { + log.error("写入错误响应失败", ex); + } + } + } + + /** + * 下载文件 + * 指定自定义文件名 + */ + @Operation(summary = "下载文件(自定义文件名)") + @GetMapping("api/file/download/custom") + + public void downloadFileWithCustomName( + @ApiParam("文件相对路径") @RequestParam String relativePath, + @ApiParam("自定义文件名(不含扩展名)") @RequestParam String fileName, + HttpServletResponse response) { + try { + if (!StringUtils.hasText(relativePath)) { + throw new IllegalArgumentException("文件路径不能为空"); + } + if (!StringUtils.hasText(fileName)) { + throw new IllegalArgumentException("文件名不能为空"); + } + + log.info("文件下载请求: 路径={}, 自定义文件名={}", relativePath, fileName); + fileUtil.download(response, fileName, relativePath); + + } catch (IllegalArgumentException e) { + log.error("文件下载失败: {}", e.getMessage()); + try { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"message\":\"" + e.getMessage() + "\"}"); + } catch (Exception ex) { + log.error("写入错误响应失败", ex); + } + } catch (Exception e) { + log.error("文件下载异常", e); + try { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"message\":\"文件下载失败: " + e.getMessage() + "\"}"); + } catch (Exception ex) { + log.error("写入错误响应失败", ex); + } + } + } + + /** + * 删除文件 + * 支持可重复调用,如果文件不存在也返回成功 + */ + @Operation(summary = "删除文件") + @PostMapping("api/file/delete") + + public ResultEntity> deleteFile( + @ApiParam("文件相对路径") @RequestParam String relativePath) { + try { + if (!StringUtils.hasText(relativePath)) { + return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件路径不能为空"); + } + + boolean deleted = fileUtil.remove(relativePath); + + Map result = new HashMap<>(); + result.put("relativePath", relativePath); + result.put("deleted", deleted); + + if (deleted) { + log.info("文件删除成功: 路径={}", relativePath); + result.put("message", "文件删除成功"); + } else { + log.info("文件不存在或已被删除: 路径={}", relativePath); + result.put("message", "文件不存在或已被删除"); + } + + return new ResultEntity<>(result); + + } catch (Exception e) { + log.error("文件删除异常: 路径={}", relativePath, e); + return new ResultEntity<>(HttpStatus.INTERNAL_SERVER_ERROR, "文件删除失败: " + e.getMessage()); + } + } + + /** + * 获取文件完整路径 + * 用于调试或验证文件路径 + */ + @Operation(summary = "获取文件完整路径") + @GetMapping("api/file/fullPath") + public ResultEntity> getFullPath( + @ApiParam("文件相对路径") @RequestParam String relativePath) { + try { + if (!StringUtils.hasText(relativePath)) { + return new ResultEntity<>(HttpStatus.BAD_REQUEST, "文件路径不能为空"); + } + + String fullPath = fileUtil.getFullPath(relativePath); + + Map result = new HashMap<>(); + result.put("relativePath", relativePath); + result.put("fullPath", fullPath); + + return new ResultEntity<>(result); + + } catch (Exception e) { + log.error("获取文件完整路径失败", e); + return new ResultEntity<>(HttpStatus.INTERNAL_SERVER_ERROR, "获取路径失败: " + e.getMessage()); + } + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 9e80d3f..b7b855b 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -19,3 +19,7 @@ platform-integration: security: jwt: secret: 0123456789012345678901234567890123456789012345678901234567890123 + +file: + type: local + path: D:\新建文件夹 diff --git a/src/main/resources/db/manual/currency_generation_verification_unique_upgrade.sql b/src/main/resources/db/manual/currency_generation_verification_unique_upgrade.sql new file mode 100644 index 0000000..bda6865 --- /dev/null +++ b/src/main/resources/db/manual/currency_generation_verification_unique_upgrade.sql @@ -0,0 +1,9 @@ +-- Execute the duplicate check first. It must return no rows before adding the constraint. +SELECT request_id, COUNT(*) AS duplicate_count +FROM currency_generation_verification +GROUP BY request_id +HAVING COUNT(*) > 1; + +-- MySQL 8+: add the business uniqueness constraint to an existing table. +ALTER TABLE currency_generation_verification + ADD CONSTRAINT uk_currency_verification_request UNIQUE (request_id); diff --git a/src/main/resources/db/manual/institution_identifier_training.sql b/src/main/resources/db/manual/institution_identifier_training.sql index c0b7228..067f78f 100644 --- a/src/main/resources/db/manual/institution_identifier_training.sql +++ b/src/main/resources/db/manual/institution_identifier_training.sql @@ -187,6 +187,7 @@ CREATE TABLE IF NOT EXISTS currency_generation_verification ( created_by VARCHAR(64) NOT NULL, updated_by VARCHAR(64) NOT NULL, deleted BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT uk_currency_verification_request UNIQUE (request_id), CONSTRAINT fk_currency_verification_request FOREIGN KEY (request_id) REFERENCES currency_generation_request(id) ); diff --git a/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java b/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java index 72d02ad..cc603df 100644 --- a/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/institutionidentity/TransactionInformationIdentifierControllerTest.java @@ -273,30 +273,20 @@ class TransactionInformationIdentifierControllerTest { assertThat(transactionMapper.selectList(null).get(0).getId()).isEqualTo(stepFiveRecordId); mvc.perform(post(QUOTA_BASE_PATH + "/steps/package").header("Authorization", authorization)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.length()").value(6)) - .andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty()) - .andExpect(jsonPath("$.data.amount").value(1589.85)) - .andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F")) - .andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_"))) - .andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))) - .andExpect(jsonPath("$.data.signature").isNotEmpty()) - .andExpect(jsonPath("$.data.status").doesNotExist()) - .andExpect(jsonPath("$.data.quotaRequestMessage").doesNotExist()); + .andExpect(jsonPath("$.data.length()").value(1)) + .andExpect(jsonPath("$.data.message").value(org.hamcrest.Matchers.allOf( + org.hamcrest.Matchers.containsString("\"quotaRequestId\""), + org.hamcrest.Matchers.containsString("\"amount\":1589.85"), + org.hamcrest.Matchers.containsString("\"orgId\":\"ORG_3A4B5C6D7E8F\""), + org.hamcrest.Matchers.containsString("\"txnId\":\"TXN_")))); mvc.perform(get(CENTRAL_QUOTA_REQUEST_BASE_PATH).header("Authorization", authorization)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value(1)) .andExpect(jsonPath("$.data.length()").value(7)); mvc.perform(post(QUOTA_BASE_PATH + "/steps/send").header("Authorization", authorization)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.length()").value(6)) - .andExpect(jsonPath("$.data.quotaRequestId").isNotEmpty()) - .andExpect(jsonPath("$.data.amount").value(1589.85)) - .andExpect(jsonPath("$.data.orgId").value("ORG_3A4B5C6D7E8F")) - .andExpect(jsonPath("$.data.txnId").value(org.hamcrest.Matchers.startsWith("TXN_"))) - .andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}"))) - .andExpect(jsonPath("$.data.signature").isNotEmpty()) - .andExpect(jsonPath("$.data.status").doesNotExist()) - .andExpect(jsonPath("$.data.centralBankReceiveStatus").doesNotExist()); + .andExpect(jsonPath("$.data.length()").value(1)) + .andExpect(jsonPath("$.data.status").value("SENT")); mvc.perform(get(QUOTA_BASE_PATH).header("Authorization", authorization)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.length()").value(6)); @@ -469,8 +459,8 @@ class TransactionInformationIdentifierControllerTest { .andExpect(jsonPath("$.data.length()").value(1)); mvc.perform(post(CONTROL_SIGNATURE_BASE_PATH + "/steps/package").header("Authorization", authorization)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.status").value("PACKAGED")) - .andExpect(jsonPath("$.data.responseMessage").value(org.hamcrest.Matchers.containsString( + .andExpect(jsonPath("$.data.length()").value(1)) + .andExpect(jsonPath("$.data.message").value(org.hamcrest.Matchers.containsString( "\"cbCtrlSignature\""))); mvc.perform(get(COMMERCIAL_BANK_CONTROL_SIGNATURE_BASE_PATH).header("Authorization", authorization)) .andExpect(status().isOk()) @@ -619,8 +609,8 @@ class TransactionInformationIdentifierControllerTest { assertThat(quotaControlBitMapper.selectList(null).get(0).getCompleteControlBit()) .isEqualTo(completeControlBit); mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization)) - .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED")) - .andExpect(jsonPath("$.data.requestMessage").value(org.hamcrest.Matchers.containsString("\"quotaId\""))); + .andExpect(status().isOk()).andExpect(jsonPath("$.data.length()").value(1)) + .andExpect(jsonPath("$.data.message").value(org.hamcrest.Matchers.containsString("\"quotaId\""))); mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/static-segment") .header("Authorization", authorization)) .andExpect(status().isOk()) @@ -649,8 +639,8 @@ class TransactionInformationIdentifierControllerTest { mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/package").header("Authorization", authorization)) .andExpect(status().isOk()); mvc.perform(post(QUOTA_CONTROL_BASE_PATH + "/steps/send").header("Authorization", authorization)) - .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("RECEIVED")) - .andExpect(jsonPath("$.data.receiveStatus").value(2)); + .andExpect(status().isOk()).andExpect(jsonPath("$.data.length()").value(1)) + .andExpect(jsonPath("$.data.status").value("RECEIVED")); mvc.perform(get(CENTRAL_QUOTA_CONTROL_BASE_PATH).header("Authorization", authorization)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value(2)) @@ -686,7 +676,7 @@ class TransactionInformationIdentifierControllerTest { mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/decompose") .header("Authorization", authorization)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.status").value("DECOMPOSED")) + .andExpect(jsonPath("$.data.length()").value(2)) .andExpect(jsonPath("$.data.totalCount").value(27)) .andExpect(jsonPath("$.data.breakdown[0].denomination").value(100.0)) .andExpect(jsonPath("$.data.breakdown[0].count").value(15)) @@ -706,16 +696,10 @@ class TransactionInformationIdentifierControllerTest { mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate") .header("Authorization", authorization)) .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.length()").value(2)) .andExpect(jsonPath("$.data.status").value("GENERATED")) - .andExpect(jsonPath("$.data.currencies.length()").value(27)) - .andExpect(jsonPath("$.data.currencies[0].sequenceNumber").value(1)) - .andExpect(jsonPath("$.data.currencies[0].denomination").value(100.0)) - .andExpect(jsonPath("$.data.currencies[0].currencyId").value( - org.hamcrest.Matchers.matchesPattern("DC_[0-9]{8}_[0-9]{3}_001_[0-9A-Z]{6}"))) - .andExpect(jsonPath("$.data.currencies[0].completeCurrency").value( - org.hamcrest.Matchers.containsString("_ORG_3A4B5C6D7E8F_100.00_"))) - .andExpect(jsonPath("$.data.currencies[26].denomination").value(0.05)) - .andExpect(jsonPath("$.data.currencies[26].status").value("待生效")); + .andExpect(jsonPath("$.data.totalCount").value(27)) + .andExpect(jsonPath("$.data.currencies").doesNotExist()); mvc.perform(get(STANDARD_CURRENCY_BASE_PATH + "/currencies") .header("Authorization", authorization)) .andExpect(status().isOk()) @@ -753,7 +737,8 @@ class TransactionInformationIdentifierControllerTest { mvc.perform(post(STANDARD_CURRENCY_BASE_PATH + "/steps/generate").header("Authorization", authorization)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.status").value("GENERATED")) - .andExpect(jsonPath("$.data.currencies.length()").value(27)); + .andExpect(jsonPath("$.data.totalCount").value(27)) + .andExpect(jsonPath("$.data.currencies").doesNotExist()); assertThat(standardCurrencyBatchMapper.selectList(null)).hasSize(1); assertThat(standardCurrencyBatchMapper.selectList(null).get(0).getId()).isEqualTo(batchId); assertThat(standardCurrencyMapper.selectList(null)).hasSize(27);