feat: expose commercial and central issuance APIs

agent/payment-training-progress
chenyuan 4 weeks ago
parent 63960e107c
commit 326366927c

@ -19,6 +19,8 @@
<properties> <properties>
<java.version>1.8</java.version> <java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>
<mybatis-plus.version>3.5.17</mybatis-plus.version> <mybatis-plus.version>3.5.17</mybatis-plus.version>

@ -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;}
}

@ -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<DenominationItemRequest> 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<DenominationItemRequest> getDenominations(){return denominations;} public void setDenominations(List<DenominationItemRequest> v){denominations=v;}
}

@ -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; }
}

@ -0,0 +1,2 @@
package com.yau.digitalrmb.issuance.interfaces.dto;
public class UpdateIssuanceRequest extends CreateIssuanceRequest { }

@ -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<CentralBankIssuanceRequestResponse> get(@PathVariable UUID id){return ApiResponse.success(CentralBankIssuanceRequestResponse.from(service.getCentralBankView(id),mapper),MDC.get(TraceIdFilter.MDC_KEY));}
}

@ -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<CommercialBankInventoryView> inventory(@RequestParam String bankCode){return ok(service.getInventory(bankCode));}
@PostMapping("/requests") @Operation(summary="创建发行申请", description="商业银行端创建数字货币发行申请草稿")
public ApiResponse<IssuanceRequestView> 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<IssuanceRequestView> 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<IssuanceRequestView> get(@PathVariable UUID id){return ok(service.getCommercialBankView(id));}
@PostMapping("/requests/{id}/prepare-message") @Operation(summary="生成待签名原文",description="冻结时间戳并生成发行请求待签名原文") public ApiResponse<IssuanceRequestView> 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<IssuanceRequestView> 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<IssuanceRequestView> sign(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.sign(id,actor(jwt)));}
@PostMapping("/requests/{id}/package") @Operation(summary="封装发行请求报文",description="封装包含摘要与签名的JSON发行请求报文") public ApiResponse<IssuanceRequestView> pack(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.packagePayload(id,actor(jwt)));}
@PostMapping("/requests/{id}/send") @Operation(summary="发送发行请求",description="发送至中央银行端并更新中央银行接收状态") public ApiResponse<IssuanceRequestView> send(@PathVariable UUID id,@AuthenticationPrincipal Jwt jwt){return ok(service.send(id,actor(jwt)));}
private List<DenominationItem> items(List<DenominationItemRequest> 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 <T> ApiResponse<T> ok(T data){return ApiResponse.success(data,MDC.get(TraceIdFilter.MDC_KEY));}
}

@ -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("数字货币发行模块 - 中央银行端");
}
}
Loading…
Cancel
Save