From 326366927c1bdaad97d00448f0ad0fe5793afaf6 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 4 Aug 2026 09:05:32 +0800 Subject: [PATCH] feat: expose commercial and central issuance APIs --- pom.xml | 2 + .../CentralBankIssuanceRequestResponse.java | 20 ++++++ .../interfaces/dto/CreateIssuanceRequest.java | 24 +++++++ .../dto/DenominationItemRequest.java | 15 +++++ .../interfaces/dto/UpdateIssuanceRequest.java | 2 + .../rest/CentralBankIssuanceController.java | 22 +++++++ .../CommercialBankIssuanceController.java | 45 ++++++++++++++ .../rest/IssuanceControllerTest.java | 62 +++++++++++++++++++ 8 files changed, 192 insertions(+) create mode 100644 src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CentralBankIssuanceRequestResponse.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CreateIssuanceRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/DenominationItemRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/UpdateIssuanceRequest.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CentralBankIssuanceController.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java create mode 100644 src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java diff --git a/pom.xml b/pom.xml index 782f04c..a782729 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,8 @@ 1.8 + UTF-8 + UTF-8 1.8 1.8 3.5.17 diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CentralBankIssuanceRequestResponse.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CentralBankIssuanceRequestResponse.java new file mode 100644 index 0000000..3499bb6 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CentralBankIssuanceRequestResponse.java @@ -0,0 +1,20 @@ +package com.yau.digitalrmb.issuance.interfaces.dto; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yau.digitalrmb.issuance.application.query.IssuanceRequestView; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Instant; +import java.util.UUID; + +@Schema(description = "中央银行端发行请求接收结果") +public class CentralBankIssuanceRequestResponse { + @Schema(description = "发行申请标识") private final UUID id; + @Schema(description = "发行请求编号") private final String requestNo; + @Schema(description = "接收状态", example = "RECEIVED") private final String receiveStatus; + @Schema(description = "接收时间") private final Instant receivedAt; + @Schema(description = "数字货币发行请求报文") private final JsonNode payload; + private CentralBankIssuanceRequestResponse(IssuanceRequestView view, JsonNode payload) { id=view.getId(); requestNo=view.getRequestNo(); receiveStatus=view.getCentralBankReceiveStatus(); receivedAt=view.getCentralBankReceivedAt(); this.payload=payload; } + public static CentralBankIssuanceRequestResponse from(IssuanceRequestView view, ObjectMapper mapper) { try { return new CentralBankIssuanceRequestResponse(view, view.getPayloadJson()==null?null:mapper.readTree(view.getPayloadJson())); } catch(Exception e) { throw new IllegalStateException("无法解析发行请求报文",e); } } + public UUID getId(){return id;} public String getRequestNo(){return requestNo;} public String getReceiveStatus(){return receiveStatus;} public Instant getReceivedAt(){return receivedAt;} public JsonNode getPayload(){return payload;} +} diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CreateIssuanceRequest.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CreateIssuanceRequest.java new file mode 100644 index 0000000..3a49f1f --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/CreateIssuanceRequest.java @@ -0,0 +1,24 @@ +package com.yau.digitalrmb.issuance.interfaces.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; +import java.math.BigDecimal; +import java.util.List; + +@Schema(description = "创建数字货币发行申请") +public class CreateIssuanceRequest { + @NotBlank @Schema(description = "商业银行机构代码", example = "BKCHCNBJ00001") private String bankCode; + @NotBlank @Schema(description = "机构标识", example = "ORG_3A4B5C6D7E8F") private String organizationId; + @NotNull @Positive @Schema(description = "发行总金额", example = "50000.00") private BigDecimal totalAmount; + @NotBlank @Schema(description = "币种", example = "DC") private String currency; + @Valid @NotEmpty @Schema(description = "面额明细") private List denominations; + public String getBankCode(){return bankCode;} public void setBankCode(String v){bankCode=v;} + public String getOrganizationId(){return organizationId;} public void setOrganizationId(String v){organizationId=v;} + public BigDecimal getTotalAmount(){return totalAmount;} public void setTotalAmount(BigDecimal v){totalAmount=v;} + public String getCurrency(){return currency;} public void setCurrency(String v){currency=v;} + public List getDenominations(){return denominations;} public void setDenominations(List v){denominations=v;} +} diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/DenominationItemRequest.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/DenominationItemRequest.java new file mode 100644 index 0000000..14b3a0d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/DenominationItemRequest.java @@ -0,0 +1,15 @@ +package com.yau.digitalrmb.issuance.interfaces.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "数字货币面额明细") +public class DenominationItemRequest { + @Schema(description = "面额", example = "100") + private Integer denomination; + @Schema(description = "数量", example = "400") + private Integer quantity; + public Integer getDenomination() { return denomination; } + public void setDenomination(Integer denomination) { this.denomination = denomination; } + public Integer getQuantity() { return quantity; } + public void setQuantity(Integer quantity) { this.quantity = quantity; } +} diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/UpdateIssuanceRequest.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/UpdateIssuanceRequest.java new file mode 100644 index 0000000..f11f5ce --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/dto/UpdateIssuanceRequest.java @@ -0,0 +1,2 @@ +package com.yau.digitalrmb.issuance.interfaces.dto; +public class UpdateIssuanceRequest extends CreateIssuanceRequest { } diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CentralBankIssuanceController.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CentralBankIssuanceController.java new file mode 100644 index 0000000..e20a16a --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CentralBankIssuanceController.java @@ -0,0 +1,22 @@ +package com.yau.digitalrmb.issuance.interfaces.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yau.digitalrmb.issuance.application.service.CentralBankIssuanceQueryService; +import com.yau.digitalrmb.issuance.interfaces.dto.CentralBankIssuanceRequestResponse; +import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.web.TraceIdFilter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.MDC; +import org.springframework.web.bind.annotation.*; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/central-banks/issuance") +@Tag(name="数字货币发行模块 - 中央银行端", description="中央银行查看数字货币发行请求接收结果") +public class CentralBankIssuanceController { + private final CentralBankIssuanceQueryService service; private final ObjectMapper mapper; + public CentralBankIssuanceController(CentralBankIssuanceQueryService service,ObjectMapper mapper){this.service=service;this.mapper=mapper;} + @GetMapping("/requests/{id}") @Operation(summary="查询发行请求接收结果",description="中央银行端查询接收状态、接收时间和完整请求报文") + public ApiResponse get(@PathVariable UUID id){return ApiResponse.success(CentralBankIssuanceRequestResponse.from(service.getCentralBankView(id),mapper),MDC.get(TraceIdFilter.MDC_KEY));} +} diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java new file mode 100644 index 0000000..fc0ec6a --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceController.java @@ -0,0 +1,45 @@ +package com.yau.digitalrmb.issuance.interfaces.rest; + +import com.yau.digitalrmb.issuance.application.command.CreateIssuanceRequestCommand; +import com.yau.digitalrmb.issuance.application.command.UpdateIssuanceRequestCommand; +import com.yau.digitalrmb.issuance.application.query.CommercialBankInventoryView; +import com.yau.digitalrmb.issuance.application.query.IssuanceRequestView; +import com.yau.digitalrmb.issuance.application.service.CommercialBankIssuanceApplicationService; +import com.yau.digitalrmb.issuance.domain.model.DenominationItem; +import com.yau.digitalrmb.issuance.interfaces.dto.CreateIssuanceRequest; +import com.yau.digitalrmb.issuance.interfaces.dto.DenominationItemRequest; +import com.yau.digitalrmb.issuance.interfaces.dto.UpdateIssuanceRequest; +import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.web.TraceIdFilter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.MDC; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; +import java.util.*; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/v1/commercial-banks/issuance") +@Tag(name="数字货币发行模块 - 商业银行端", description="商业银行发起和处理数字货币发行申请") +public class CommercialBankIssuanceController { + private final CommercialBankIssuanceApplicationService service; + public CommercialBankIssuanceController(CommercialBankIssuanceApplicationService service){this.service=service;} + @GetMapping("/inventory") @Operation(summary="查询货币需求信息", description="商业银行端查询库存余额、预警阈值和建议补充金额") + public ApiResponse inventory(@RequestParam String bankCode){return ok(service.getInventory(bankCode));} + @PostMapping("/requests") @Operation(summary="创建发行申请", description="商业银行端创建数字货币发行申请草稿") + public ApiResponse create(@Valid @RequestBody CreateIssuanceRequest request,@AuthenticationPrincipal Jwt jwt){return ok(service.create(new CreateIssuanceRequestCommand(request.getBankCode(),request.getOrganizationId(),request.getTotalAmount(),request.getCurrency(),items(request.getDenominations())),actor(jwt)));} + @PutMapping("/requests/{id}") @Operation(summary="更新发行申请", description="商业银行端仅可更新草稿状态的发行申请") + public ApiResponse update(@PathVariable UUID id,@Valid @RequestBody UpdateIssuanceRequest request,@AuthenticationPrincipal Jwt jwt){return ok(service.update(id,new UpdateIssuanceRequestCommand(request.getTotalAmount(),request.getCurrency(),items(request.getDenominations())),actor(jwt)));} + @GetMapping("/requests/{id}") @Operation(summary="查询发行申请", description="商业银行端查询发行申请及其处理产物") public ApiResponse get(@PathVariable UUID id){return ok(service.getCommercialBankView(id));} + @PostMapping("/requests/{id}/prepare-message") @Operation(summary="生成待签名原文",description="冻结时间戳并生成发行请求待签名原文") public ApiResponse prepare(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.prepareMessage(id,actor(jwt)));} + @PostMapping("/requests/{id}/digest") @Operation(summary="生成SM3摘要",description="对待签名原文生成SM3摘要") public ApiResponse digest(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.digest(id,actor(jwt)));} + @PostMapping("/requests/{id}/sign") @Operation(summary="生成SM2签名",description="使用实验密钥sm2-key-02生成SM2签名") public ApiResponse sign(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.sign(id,actor(jwt)));} + @PostMapping("/requests/{id}/package") @Operation(summary="封装发行请求报文",description="封装包含摘要与签名的JSON发行请求报文") public ApiResponse pack(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.packagePayload(id,actor(jwt)));} + @PostMapping("/requests/{id}/send") @Operation(summary="发送发行请求",description="发送至中央银行端并更新中央银行接收状态") public ApiResponse send(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.send(id,actor(jwt)));} + private List items(List input){return input.stream().map(i->new DenominationItem(i.getDenomination(),i.getQuantity())).collect(Collectors.toList());} + private String actor(Jwt jwt){String account=jwt.getClaimAsString("preferred_username"); return account==null||account.trim().isEmpty()?jwt.getSubject():account;} + private ApiResponse ok(T data){return ApiResponse.success(data,MDC.get(TraceIdFilter.MDC_KEY));} +} diff --git a/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java new file mode 100644 index 0000000..cc3d6a0 --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceControllerTest.java @@ -0,0 +1,62 @@ +package com.yau.digitalrmb.issuance.interfaces.rest; + +import com.jayway.jsonpath.JsonPath; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.nio.charset.StandardCharsets; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class IssuanceControllerTest { + @Autowired + private MockMvc mockMvc; + + @Test + void commercialBankWorkflowIsVisibleToCentralBankEnd() throws Exception { + String requestBody = "{\"bankCode\":\"BKCHCNBJ00001\",\"organizationId\":\"ORG_3A4B5C6D7E8F\",\"totalAmount\":50000.00,\"currency\":\"DC\",\"denominations\":[{\"denomination\":100,\"quantity\":400},{\"denomination\":50,\"quantity\":200}]}"; + String createResponse = mockMvc.perform(post("/api/v1/commercial-banks/issuance/requests") + .with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001"))) + .contentType(MediaType.APPLICATION_JSON).content(requestBody)) + .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DRAFT")) + .andReturn().getResponse().getContentAsString(); + String requestId = JsonPath.read(createResponse, "$.data.id"); + + String base = "/api/v1/commercial-banks/issuance/requests/" + requestId; + mockMvc.perform(post(base + "/prepare-message").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("MESSAGE_PREPARED")); + mockMvc.perform(post(base + "/digest").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("DIGESTED")); + mockMvc.perform(post(base + "/sign").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SIGNED")); + mockMvc.perform(post(base + "/package").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("PACKAGED")); + mockMvc.perform(post(base + "/send").with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + .andExpect(status().isOk()).andExpect(jsonPath("$.data.status").value("SENT")); + + mockMvc.perform(get("/api/v1/central-banks/issuance/requests/{id}", requestId) + .with(jwt().jwt(jwt -> jwt.subject("487").claim("preferred_username", "tzs001")))) + .andExpect(status().isOk()).andExpect(jsonPath("$.data.receiveStatus").value("RECEIVED")); + } + + @Test + void swaggerDocumentsBothChineseIssuanceEnds() throws Exception { + String apiDocs = new String(mockMvc.perform(get("/v3/api-docs")) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsByteArray(), StandardCharsets.UTF_8); + assertThat(apiDocs).contains("数字货币发行模块 - 商业银行端") + .contains("数字货币发行模块 - 中央银行端"); + } +}