|
|
|
|
@ -0,0 +1,500 @@
|
|
|
|
|
# 数字货币发行请求模块 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 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("数字货币发行模块 - 中央银行端")));
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- [ ] **Step 2: Run controller tests to verify they fail**
|
|
|
|
|
|
|
|
|
|
Run: `mvn -Dtest=CommercialBankIssuanceControllerTest,CentralBankIssuanceControllerTest test -B`
|
|
|
|
|
|
|
|
|
|
Expected: compilation fails because the controllers and DTOs 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 Full 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: Write a failing integration workflow test**
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@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 the full workflow test to verify it fails before the missing integration behavior is completed**
|
|
|
|
|
|
|
|
|
|
Run: `mvn -Dtest=IssuanceWorkflowIntegrationTest test -B`
|
|
|
|
|
|
|
|
|
|
Expected: FAIL until all commercial-bank action endpoints and the central-bank projection are connected.
|
|
|
|
|
|
|
|
|
|
- [ ] **Step 3: Add only the missing integration wiring and document the dev workflow**
|
|
|
|
|
|
|
|
|
|
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 4: 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 5: 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 6: 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 1–4 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.
|