You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
digital-rmb-backend/docs/superpowers/plans/2026-08-03-issuance-request...

22 KiB

数字货币发行请求模块 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the first-step digital-currency issuance workflow with separate commercial-bank and central-bank APIs, real SM3/SM2 teaching operations, and Chinese Swagger documentation.

Architecture: Extend the existing issuance bounded context around one IssuanceRequest aggregate. Commercial-bank APIs own request creation and state transitions; central-bank APIs are a read-only projection of the same request, with a separate receive-status field. MyBatis-Plus persists the aggregate and denomination children; schema.sql remains the only idempotent database bootstrap mechanism.

Tech Stack: Java 8, Spring Boot 2.7.18, Spring Security 5.7, MyBatis-Plus 3.5.17, Springdoc 1.8.0, MySQL 8, Bouncy Castle bcprov-jdk18on:1.84.

Global Constraints

  • Use Java 8 only; do not introduce records, List.of, text blocks, jakarta.*, or Java 9+ APIs.
  • Keep all issuance code below com.yau.digitalrmb.issuance; do not place issuance business code in shared.
  • All commercial-bank and central-bank APIs require a valid JWT, with no teacher/student role restriction.
  • Do not restore Flyway. Schema and demo data changes belong in the idempotent src/main/resources/schema.sql and must work on MySQL 8 and H2 MySQL mode.
  • Do not accept, log, persist, or return a private key. The only exposed signing-key identifier is sm2-key-02.
  • Swagger labels, summaries, descriptions, field documentation, and root title must be Chinese UTF-8 without garbled characters.
  • All work starts from a clean isolated worktree and ends with JDK 8 mvn test -B, mvn package -DskipTests -B, dev-profile startup, /actuator/health, /v3/api-docs, and Swagger UI verification.

Target File Structure

src/main/java/com/yau/digitalrmb/issuance/
  domain/model/
    DenominationItem.java
    IssuanceRequest.java
    IssuanceRequestStatus.java
    CentralBankReceiveStatus.java
  domain/repository/
    IssuanceRequestRepository.java
  domain/service/
    IssuanceMessageComposer.java
    IssuanceSignatureService.java
    SignedIssuancePayload.java
  application/command/
    CreateIssuanceRequestCommand.java
    UpdateIssuanceRequestCommand.java
  application/query/
    CommercialBankInventoryView.java
    IssuanceRequestView.java
  application/service/
    CommercialBankIssuanceApplicationService.java
    CentralBankIssuanceQueryService.java
  infrastructure/crypto/
    BouncyCastleIssuanceSignatureService.java
    InMemorySm2SigningKeyProvider.java
  infrastructure/persistence/entity/
    IssuanceBankInventoryEntity.java
    IssuanceRequestEntity.java
    IssuanceRequestDenominationEntity.java
  infrastructure/persistence/mapper/
    IssuanceBankInventoryMapper.java
    IssuanceRequestMapper.java
    IssuanceRequestDenominationMapper.java
  infrastructure/persistence/repository/
    MybatisIssuanceRequestRepository.java
  interfaces/dto/
    CreateIssuanceRequestRequest.java
    UpdateIssuanceRequestRequest.java
    DenominationItemRequest.java
    IssuanceRequestResponse.java
    CommercialBankInventoryResponse.java
    CentralBankIssuanceRequestResponse.java
  interfaces/rest/
    CommercialBankIssuanceController.java
    CentralBankIssuanceController.java

Task 1: Model the Issuance Request Aggregate

Files:

  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/model/DenominationItem.java
  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequest.java
  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequestStatus.java
  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/model/CentralBankReceiveStatus.java
  • Create: src/test/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequestTest.java

Interfaces:

  • Consumes: existing IssuanceApplicationId.

  • Produces: aggregate methods updateDraft(...), prepareMessage(...), recordDigest(...), recordSignature(...), packagePayload(...), and sendToCentralBank(Instant).

  • Step 1: Write failing aggregate tests

@Test
void rejectsDenominationsWhoseTotalDoesNotMatchRequestAmount() {
    assertThatThrownBy(() -> IssuanceRequest.create(id, "BKCHCNBJ00001", "ORG_3A4B5C6D7E8F",
            new BigDecimal("50000.00"), "DC", Arrays.asList(new DenominationItem(100, 400))))
        .isInstanceOf(IllegalArgumentException.class);
}

@Test
void sendsOnlyPackagedRequestAndMarksCentralBankAsReceived() {
    IssuanceRequest request = preparedSignedAndPackagedRequest();
    request.sendToCentralBank(Instant.parse("2026-08-03T12:00:00Z"));
    assertThat(request.getStatus()).isEqualTo(IssuanceRequestStatus.SENT);
    assertThat(request.getCentralBankReceiveStatus()).isEqualTo(CentralBankReceiveStatus.RECEIVED);
}
  • Step 2: Run the aggregate test to verify it fails

Run: mvn -Dtest=IssuanceRequestTest test -B

Expected: compilation fails because IssuanceRequest, status enums, and DenominationItem do not yet exist.

  • Step 3: Implement immutable value data and aggregate transitions
public enum IssuanceRequestStatus { DRAFT, MESSAGE_PREPARED, DIGESTED, SIGNED, PACKAGED, SENT }
public enum CentralBankReceiveStatus { NOT_RECEIVED, RECEIVED }

public void sendToCentralBank(Instant receivedAt) {
    requireStatus(IssuanceRequestStatus.PACKAGED);
    this.status = IssuanceRequestStatus.SENT;
    this.centralBankReceiveStatus = CentralBankReceiveStatus.RECEIVED;
    this.centralBankReceivedAt = receivedAt;
}

DenominationItem validates positive denomination and quantity. IssuanceRequest.create(...) validates exact Σ(denomination × quantity) == totalAmount, initializes DRAFT and NOT_RECEIVED, and normalizes monetary values to scale 2.

  • Step 4: Run aggregate tests to verify they pass

Run: mvn -Dtest=IssuanceRequestTest test -B

Expected: PASS, including invalid amount, invalid state, normal state progression, and idempotent repeated send tests.

  • Step 5: Commit the aggregate model
git add src/main/java/com/yau/digitalrmb/issuance/domain/model src/test/java/com/yau/digitalrmb/issuance/domain/model/IssuanceRequestTest.java
git commit -m "feat: add issuance request aggregate"

Task 2: Implement SM3 and SM2 Teaching Services

Files:

  • Modify: pom.xml
  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/service/IssuanceMessageComposer.java
  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/service/IssuanceSignatureService.java
  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/service/SignedIssuancePayload.java
  • Create: src/main/java/com/yau/digitalrmb/issuance/infrastructure/crypto/InMemorySm2SigningKeyProvider.java
  • Create: src/main/java/com/yau/digitalrmb/issuance/infrastructure/crypto/BouncyCastleIssuanceSignatureService.java
  • Create: src/test/java/com/yau/digitalrmb/issuance/infrastructure/crypto/BouncyCastleIssuanceSignatureServiceTest.java

Interfaces:

  • Consumes: IssuanceRequest field values and fixed key reference sm2-key-02.

  • Produces: compose(...) plain text and sign(String keyRef, String plainText) returning a SM3 hex digest plus Base64 SM2 signature.

  • Step 1: Write failing cryptography tests

@Test
void composesPrototypeCompatiblePlainTextInDescendingDenominationOrder() {
    assertThat(composer.compose("BKCHCNBJ00001", "ORG_3A4B5C6D7E8F", new BigDecimal("50000.00"),
            Arrays.asList(new DenominationItem(50, 100), new DenominationItem(100, 400)), "DC", "20260801103218"))
        .isEqualTo("ISSUE|BKCHCNBJ00001|ORG_3A4B5C6D7E8F|50000.00|100:400,50:100|DC|20260801103218");
}

@Test
void signsWithSm3AndSm2UsingTheTeachingKey() {
    SignedIssuancePayload payload = service.sign("sm2-key-02", "ISSUE|...");
    assertThat(payload.getDigest()).matches("[0-9A-F]{64}");
    assertThat(service.verify("sm2-key-02", "ISSUE|...", payload)).isTrue();
}
  • Step 2: Run the cryptography tests to verify they fail

Run: mvn -Dtest=BouncyCastleIssuanceSignatureServiceTest test -B

Expected: compilation fails because the composer, payload, signing service, and Bouncy Castle provider are absent.

  • Step 3: Add the provider and minimal implementations

Add the explicit dependency:

<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk18on</artifactId>
  <version>1.84</version>
</dependency>

Register BouncyCastleProvider once. Generate an in-memory sm2p256v1 key pair for sm2-key-02; use MessageDigest.getInstance("SM3", "BC") for the upper-case hexadecimal digest and Signature.getInstance("SM3withSM2", "BC") for signing and verification. The IssuanceMessageComposer sorts denominations descending and uses toPlainString() for the scale-2 total amount.

  • Step 4: Run the cryptography tests to verify they pass

Run: mvn -Dtest=BouncyCastleIssuanceSignatureServiceTest test -B

Expected: PASS; the digest has 64 uppercase hexadecimal characters and the generated signature verifies with the in-memory public key.

  • Step 5: Commit the cryptography teaching service
git add pom.xml src/main/java/com/yau/digitalrmb/issuance/domain/service src/main/java/com/yau/digitalrmb/issuance/infrastructure/crypto src/test/java/com/yau/digitalrmb/issuance/infrastructure/crypto
git commit -m "feat: add issuance sm3 sm2 teaching service"

Task 3: Add Idempotent Schema and MyBatis Persistence

Files:

  • Modify: src/main/resources/schema.sql
  • Create: src/main/java/com/yau/digitalrmb/issuance/domain/repository/IssuanceRequestRepository.java
  • Create: the six entity, mapper, and repository files listed in Target File Structure under infrastructure/persistence
  • Create: src/test/java/com/yau/digitalrmb/issuance/infrastructure/persistence/MybatisIssuanceRequestRepositoryTest.java

Interfaces:

  • Consumes: IssuanceRequestRepository.save(IssuanceRequest) and findById(IssuanceApplicationId).

  • Produces: persistent requests with all denomination entries and persistent commercial-bank inventory lookups.

  • Step 1: Write failing persistence tests

@Test
void savesAndRestoresTheRequestWithItsDenominationsAndCentralBankStatus() {
    repository.save(sentRequest);
    IssuanceRequest restored = repository.findById(sentRequest.getId()).get();
    assertThat(restored.getDenominations()).containsExactly(new DenominationItem(100, 400), new DenominationItem(50, 100));
    assertThat(restored.getCentralBankReceiveStatus()).isEqualTo(CentralBankReceiveStatus.RECEIVED);
}

@Test
void readsSuggestedAmountAsThresholdMinusCurrentBalance() {
    assertThat(inventoryQuery.findByBankCode("BKCHCNBJ00001").getSuggestedSupplementAmount())
        .isEqualByComparingTo("50000.00");
}
  • Step 2: Run persistence tests to verify they fail

Run: mvn -Dtest=MybatisIssuanceRequestRepositoryTest test -B

Expected: compilation fails because the repository and mappers do not exist.

  • Step 3: Add schema and persistence mapping

Append idempotent MySQL/H2-compatible DDL to schema.sql:

CREATE TABLE IF NOT EXISTS issuance_bank_inventory (
  bank_code VARCHAR(32) PRIMARY KEY,
  current_balance DECIMAL(20, 2) NOT NULL,
  warning_threshold DECIMAL(20, 2) NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

CREATE TABLE IF NOT EXISTS issuance_request (
  id CHAR(36) PRIMARY KEY,
  request_no VARCHAR(64) NOT NULL UNIQUE,
  bank_code VARCHAR(32) NOT NULL,
  organization_id VARCHAR(64) NOT NULL,
  total_amount DECIMAL(20, 2) NOT NULL,
  currency VARCHAR(16) NOT NULL,
  request_timestamp VARCHAR(32),
  message_text TEXT,
  digest CHAR(64),
  signature TEXT,
  signing_key_ref VARCHAR(64),
  payload_json TEXT,
  status VARCHAR(32) NOT NULL,
  central_receive_status VARCHAR(32) NOT NULL,
  central_received_at TIMESTAMP NULL,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL,
  created_by VARCHAR(64) NOT NULL,
  updated_by VARCHAR(64) NOT NULL,
  deleted BOOLEAN NOT NULL DEFAULT FALSE
);

CREATE TABLE IF NOT EXISTS issuance_request_denomination (
  request_id CHAR(36) NOT NULL,
  denomination INT NOT NULL,
  quantity INT NOT NULL,
  PRIMARY KEY (request_id, denomination)
);

Seed BKCHCNBJ00001 with 49950000.00 balance and 50000000.00 threshold using an idempotent upsert. Map the root row and denomination rows in the repository; replacing denomination rows occurs only inside a repository save transaction.

  • Step 4: Run persistence tests to verify they pass

Run: mvn -Dtest=MybatisIssuanceRequestRepositoryTest test -B

Expected: PASS against the existing H2 MySQL-mode test profile.

  • Step 5: Commit schema and persistence
git add src/main/resources/schema.sql src/main/java/com/yau/digitalrmb/issuance/domain/repository src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence src/test/java/com/yau/digitalrmb/issuance/infrastructure/persistence
git commit -m "feat: persist issuance requests and bank inventory"

Task 4: Add Commercial-Bank and Central-Bank Application Services

Files:

  • Create: all command, query, and application-service files listed in Target File Structure
  • Create: src/test/java/com/yau/digitalrmb/issuance/application/service/CommercialBankIssuanceApplicationServiceTest.java
  • Create: src/test/java/com/yau/digitalrmb/issuance/application/service/CentralBankIssuanceQueryServiceTest.java

Interfaces:

  • Consumes: domain aggregate, repository, message composer, signing service, and authenticated account name.

  • Produces: create, update, prepareMessage, digest, sign, packagePayload, send, getCommercialBankView, getInventory, and getCentralBankView application methods.

  • Step 1: Write failing application-service tests

@Test
void processesACommercialBankRequestThroughSendAndMakesItVisibleToCentralBank() {
    UUID id = service.create(createCommand, "tzs001").getId();
    service.prepareMessage(id, "tzs001");
    service.digest(id, "tzs001");
    service.sign(id, "tzs001");
    service.packagePayload(id, "tzs001");
    service.send(id, "tzs001");
    assertThat(centralBankService.get(id).getReceiveStatus()).isEqualTo("RECEIVED");
}

@Test
void rejectsUpdateAfterMessagePreparation() {
    UUID id = createAndPrepareRequest();
    assertThatThrownBy(() -> service.update(id, changedCommand, "tzs001"))
        .isInstanceOf(BusinessException.class);
}
  • Step 2: Run application-service tests to verify they fail

Run: mvn -Dtest=CommercialBankIssuanceApplicationServiceTest,CentralBankIssuanceQueryServiceTest test -B

Expected: compilation fails because the command, query, and application services do not exist.

  • Step 3: Implement application orchestration and error mapping

Generate request numbers using ISSUE_REQ_ plus a UUID-derived upper-case suffix. Wrap writes in @Transactional; read inventory and central-bank data with dedicated query methods. Convert missing aggregate lookups into BusinessException(ErrorCode.RESOURCE_NOT_FOUND, ...); convert domain IllegalStateException and IllegalArgumentException into BusinessException(ErrorCode.VALIDATION_ERROR, ...). Make send return the existing SENT result when called again.

  • Step 4: Run application-service tests to verify they pass

Run: mvn -Dtest=CommercialBankIssuanceApplicationServiceTest,CentralBankIssuanceQueryServiceTest test -B

Expected: PASS; a commercial-bank send produces a central-bank RECEIVED view and draft editing is rejected after preparation.

  • Step 5: Commit application services
git add src/main/java/com/yau/digitalrmb/issuance/application src/test/java/com/yau/digitalrmb/issuance/application
git commit -m "feat: add issuance application workflow"

Task 5: Expose Separate Chinese-Swagger REST Ends

Files:

  • Create: all DTO and controller files listed in Target File Structure
  • Modify: src/main/java/com/yau/digitalrmb/shared/config/OpenApiConfig.java
  • Create: src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/CommercialBankIssuanceControllerTest.java
  • Create: src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/CentralBankIssuanceControllerTest.java

Interfaces:

  • Consumes: application service methods and current authenticated JWT subject.

  • Produces: the ten REST paths specified in the approved design, wrapped in ApiResponse<T>.

  • Step 1: Write failing MVC, end-to-end workflow, and OpenAPI documentation tests

@Test
void commercialBankCreateRequiresJwtAndReturnsDraftRequest() throws Exception {
    mockMvc.perform(post("/api/v1/commercial-banks/issuance/requests")
            .with(jwt().jwt(jwt -> jwt.subject("tzs001")))
            .contentType(MediaType.APPLICATION_JSON)
            .content(validCreateBody))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.data.status").value("DRAFT"));
}

@Test
void apiDocsContainChineseCommercialAndCentralBankTags() throws Exception {
    mockMvc.perform(get("/v3/api-docs"))
        .andExpect(status().isOk())
        .andExpect(content().string(containsString("数字货币发行模块 - 商业银行端")))
        .andExpect(content().string(containsString("数字货币发行模块 - 中央银行端")));
}

@Test
void completeTeachingWorkflowReturnsReceivedPayloadToCentralBankEnd() throws Exception {
    UUID requestId = createPrepareDigestSignPackageAndSendThroughCommercialBankApi();
    mockMvc.perform(get("/api/v1/central-banks/issuance/requests/{id}", requestId)
            .with(jwt().jwt(jwt -> jwt.subject("tzs001"))))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.data.receiveStatus").value("RECEIVED"))
        .andExpect(jsonPath("$.data.payload.requestId").exists());
}
  • Step 2: Run controller tests to verify they fail

Run: mvn -Dtest=CommercialBankIssuanceControllerTest,CentralBankIssuanceControllerTest test -B

Expected: compilation fails because the controllers, DTOs, and commercial-bank-to-central-bank HTTP workflow do not exist.

  • Step 3: Implement controllers, DTO validation, and Chinese Swagger metadata

Use the two class-level tags and paths exactly:

@Tag(name = "数字货币发行模块 - 商业银行端", description = "商业银行发起和处理数字货币发行申请")
@RequestMapping("/api/v1/commercial-banks/issuance")

@Tag(name = "数字货币发行模块 - 中央银行端", description = "中央银行查看数字货币发行请求接收结果")
@RequestMapping("/api/v1/central-banks/issuance")

Annotate each endpoint with Chinese @Operation(summary = ..., description = ...) and each DTO property with Chinese @Schema(description = ..., example = ...). Set the root OpenAPI title to 数字人民币教学仿真后端 using UTF-8 source text. Commercial-bank controllers expose inventory, CRUD-read/update, process actions, and send; the central-bank controller exposes only GET /requests/{id}.

  • Step 4: Run controller tests to verify they pass

Run: mvn -Dtest=CommercialBankIssuanceControllerTest,CentralBankIssuanceControllerTest test -B

Expected: PASS; all commercial-bank write operations require JWT, central-bank query is isolated to its prefix, and /v3/api-docs has both readable Chinese tags.

  • Step 5: Commit REST and Swagger work
git add src/main/java/com/yau/digitalrmb/issuance/interfaces src/main/java/com/yau/digitalrmb/shared/config/OpenApiConfig.java src/test/java/com/yau/digitalrmb/issuance/interfaces
git commit -m "feat: expose commercial and central issuance APIs"

Task 6: Verify the Completed Workflow on the Dev Database

Files:

  • Modify: README.md
  • Test: existing and new src/test/java/com/yau/digitalrmb/issuance/** tests

Interfaces:

  • Consumes: completed application JAR and dev profile.

  • Produces: documented startup and verified commercial-bank-to-central-bank workflow.

  • Step 1: Add the dev workflow documentation

Add a README section with the dev profile startup command, the Chinese Swagger URL, the commercial-bank inventory endpoint, and the central-bank request query endpoint. Do not document or expose any database password or private key.

  • Step 2: Run all automated verification

Run:

$env:JAVA_HOME='C:\Users\Acer\.jdks\temurin-8\jdk8u502-b07'
$env:Path="$env:JAVA_HOME\bin;$env:Path"
mvn test -B
mvn package -DskipTests -B

Expected: Maven exits 0 with all tests passing and the JAR built for Java 8.

  • Step 3: Run the dev-profile smoke test against the 118 test database

Run:

java -jar target\digital-rmb-backend-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev --server.port=8081
Invoke-RestMethod http://localhost:8081/actuator/health
Invoke-WebRequest http://localhost:8081/swagger-ui/index.html -UseBasicParsing
Invoke-WebRequest http://localhost:8081/v3/api-docs -UseBasicParsing

Expected: health status is UP, Swagger returns HTTP 200, and OpenAPI JSON contains both readable Chinese tag names without replacement characters.

  • Step 4: Commit verification and documentation
git add README.md src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceWorkflowIntegrationTest.java
git commit -m "test: verify issuance teaching workflow"

Plan Self-Review

  • Spec coverage: Tasks 14 implement the aggregate, two-end state separation, inventory, persistence, message construction, SM3/SM2, and send simulation. Task 5 implements all approved API paths and Chinese Swagger grouping. Task 6 covers full workflow, JDK 8 build, dev database startup, and non-garbled OpenAPI verification.
  • Placeholder scan: no tasks defer behavior; each has concrete paths, interfaces, test commands, expected results, and implementation details.
  • Type consistency: all request IDs are UUID-backed IssuanceApplicationId; persistence and HTTP boundaries expose UUID values, while application services accept UUID and adapt to the value object internally. The status names are consistently DRAFT, MESSAGE_PREPARED, DIGESTED, SIGNED, PACKAGED, SENT, NOT_RECEIVED, and RECEIVED.

Execution Handoff

Plan complete and saved to docs/superpowers/plans/2026-08-03-issuance-request-implementation.md.

  1. Subagent-Driven (recommended) — dispatch a fresh subagent per task and review between tasks.
  2. Inline Execution — execute tasks in this session in batches with review checkpoints.