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...

491 lines
22 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 数字货币发行请求模块 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
```text
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**
```java
@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**
```java
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**
```bash
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**
```java
@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:
```xml
<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**
```bash
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**
```java
@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`:
```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**
```bash
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**
```java
@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**
```bash
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**
```java
@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:
```java
@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**
```bash
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:
```powershell
$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:
```powershell
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**
```bash
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.