feat: expose commercial and central issuance APIs
parent
63960e107c
commit
326366927c
@ -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…
Reference in New Issue