Compare commits

..

No commits in common. 'master' and 'agent/payment-training-progress' have entirely different histories.

@ -0,0 +1,645 @@
# Downstream Wallet Prerequisite Projection 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:** Make modules 4 and 5 pull completed module 3 wallet-opening results on first access and initialize shared wallet runtime state without module 3 writing downstream process data.
**Architecture:** A shared transactional `WalletPrerequisiteProjectionService` reads completed module 3 fact tables with full user/school/class isolation and inserts only missing shared wallet runtime rows. Exchange and payment repositories invoke it before their existing runtime-state queries; existing rows are validated and never overwritten, so real balances and limit counters remain authoritative.
**Tech Stack:** Java 8-compatible code, Spring Boot 2.7, `JdbcTemplate`, Spring transactions, JUnit 5, MockMvc, AssertJ, H2/MySQL-compatible SQL.
**Spec:** `docs/superpowers/specs/2026-08-18-downstream-wallet-prerequisite-projection-design.md`
## Global Constraints
- Module 3 fact tables remain read-only to modules 4 and 5.
- Every module 1 and module 3 lookup is scoped by `user_id`, `school_id`, and `class_id`.
- Projection is pull-based, transactional, idempotent, and insert-only for existing shared runtime rows.
- Entering a context endpoint must not create exchange orders, payment orders, or step logs.
- A module 5 payer needs module 4 wallet-owned currency; a payee only needs a completed module 3 wallet.
- Do not address unrelated full-suite error-code assertion failures.
---
### Task 1: Put wallet private-key encryption in the shared boundary
**Files:**
- Create: `src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrivateKeyCipher.java`
- Delete: `src/main/java/com/yau/digitalrmb/exchange/infrastructure/crypto/WalletPrivateKeyCipher.java`
- Modify: `src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java`
- Modify: `src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java`
- Modify: `src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java`
- Modify: `src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java`
**Interfaces:**
- Consumes: `SecurityProperties.getJwt().getSecret()`.
- Produces: `WalletPrivateKeyCipher.encrypt(String)` and `WalletPrivateKeyCipher.decrypt(String)` in `com.yau.digitalrmb.shared.wallet`.
- [ ] **Step 1: Move the component without changing its implementation**
Create the class under the shared package with the existing constructor and methods:
```java
package com.yau.digitalrmb.shared.wallet;
import com.yau.digitalrmb.security.config.SecurityProperties;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
@Component
public class WalletPrivateKeyCipher {
private static final int IV_LENGTH = 12;
private final SecretKeySpec key;
public WalletPrivateKeyCipher(SecurityProperties properties) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256")
.digest(properties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8));
this.key = new SecretKeySpec(digest, "AES");
} catch (Exception exception) {
throw new IllegalStateException("钱包私钥加密组件初始化失败", exception);
}
}
public String encrypt(String privateKey) {
try {
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] encrypted = cipher.doFinal(privateKey.getBytes(StandardCharsets.US_ASCII));
byte[] output = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, output, 0, iv.length);
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(output);
} catch (Exception exception) {
throw new IllegalStateException("钱包私钥加密失败", exception);
}
}
public String decrypt(String value) {
try {
byte[] input = Base64.getDecoder().decode(value);
byte[] iv = new byte[IV_LENGTH];
byte[] encrypted = new byte[input.length - IV_LENGTH];
System.arraycopy(input, 0, iv, 0, iv.length);
System.arraycopy(input, iv.length, encrypted, 0, encrypted.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
return new String(cipher.doFinal(encrypted), StandardCharsets.US_ASCII);
} catch (Exception exception) {
throw new IllegalStateException("钱包私钥解密失败", exception);
}
}
}
```
Delete the old class and replace all four imports with:
```java
import com.yau.digitalrmb.shared.wallet.WalletPrivateKeyCipher;
```
- [ ] **Step 2: Compile and run the existing exchange/payment tests**
Run:
```bash
mvn -DskipTests compile
mvn "-Dtest=ExchangeControllerTest,PaymentControllerTest" test
```
Expected: compilation succeeds; 3 tests pass with 0 failures.
- [ ] **Step 3: Commit the boundary-only refactor**
```bash
git add src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrivateKeyCipher.java src/main/java/com/yau/digitalrmb/exchange/infrastructure/crypto/WalletPrivateKeyCipher.java src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java
git commit -m "refactor: share wallet private key cipher"
```
---
### Task 2: Project completed module 3 facts into shared wallet runtime state
**Files:**
- Create: `src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteReference.java`
- Create: `src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java`
- Create: `src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java`
- Create: `src/test/java/com/yau/digitalrmb/testsupport/WalletOpeningTestData.java`
- Modify: `src/test/resources/schema.sql`
**Interfaces:**
- Consumes: `InstitutionKeySubject`, module 3 fact rows, module 1 institution identifier, `WalletPrivateKeyCipher`.
- Produces:
```java
public final class WalletPrerequisiteReference {
public String getUserId();
public long getSchoolId();
public long getClassId();
public String getWalletId();
public String getBankCode();
public String getOrganizationId();
}
public class WalletPrerequisiteProjectionService {
public WalletPrerequisiteReference ensureForSubject(InstitutionKeySubject subject);
public WalletPrerequisiteReference ensureForWallet(String walletId);
}
```
- [ ] **Step 1: Add module 3 source tables to the H2 test schema**
Append H2-compatible definitions matching the production columns used by the projector. Define all columns below, including audit/scope columns, so production SQL is exercised unchanged:
```sql
CREATE TABLE IF NOT EXISTS wallet_application (
id BIGINT PRIMARY KEY, user_id VARCHAR(36) NOT NULL, school_id BIGINT NOT NULL, class_id BIGINT NOT NULL,
account_bank VARCHAR(100), bank_card_number VARCHAR(30), account_balance DECIMAL(20,2), selected_bank VARCHAR(100),
wallet_type VARCHAR(20), application_id VARCHAR(64), status VARCHAR(32) NOT 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 wallet_identifier_generation (
id BIGINT PRIMARY KEY, user_id VARCHAR(36) NOT NULL, school_id BIGINT NOT NULL, class_id BIGINT NOT NULL,
wallet_identifier VARCHAR(128), cert_private_key VARCHAR(512), cert_public_key VARCHAR(512),
cert_serial_number VARCHAR(64), cert_issued_time VARCHAR(20), status VARCHAR(32) NOT 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 central_wallet_registration (
id BIGINT PRIMARY KEY, user_id VARCHAR(36) NOT NULL, school_id BIGINT NOT NULL, class_id BIGINT NOT NULL,
wallet_id VARCHAR(128), cb_root_signature VARCHAR(512), wallet_registered BOOLEAN NOT NULL DEFAULT FALSE,
sent BOOLEAN NOT NULL DEFAULT FALSE, status VARCHAR(32) NOT 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 smart_contract_generation (
id BIGINT PRIMARY KEY, user_id VARCHAR(36) NOT NULL, school_id BIGINT NOT NULL, class_id BIGINT NOT NULL,
wallet_id VARCHAR(128), wallet_type VARCHAR(30), single_payment_limit VARCHAR(30), daily_payment_limit VARCHAR(30),
annual_payment_limit VARCHAR(30), balance_ceiling VARCHAR(30), validity VARCHAR(20), contract_plaintext VARCHAR(1000),
contract_digest VARCHAR(128), contract_id VARCHAR(64), contract_effective_time VARCHAR(20), sent BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(32) NOT 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 central_wallet_activation (
id BIGINT PRIMARY KEY, user_id VARCHAR(36) NOT NULL, school_id BIGINT NOT NULL, class_id BIGINT NOT NULL,
wallet_id VARCHAR(128), wallet_activated BOOLEAN NOT NULL DEFAULT FALSE, wallet_register_status VARCHAR(32),
cb_final_signature VARCHAR(512), final_time VARCHAR(20), final_sent BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(32) NOT 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
);
```
- [ ] **Step 2: Write failing integration tests for incomplete, complete, idempotent, and isolated projection**
Create a shared test-data utility so all three integration test classes seed the same module 3 completion contract:
```java
public final class WalletOpeningTestData {
private WalletOpeningTestData() {}
public static void insertCompletedWallet(JdbcTemplate jdbc, long baseId, String userId,
long schoolId, long classId, String walletId, String privateKey, String publicKey,
String bankName, String cardNumber, BigDecimal bankBalance) {
jdbc.update("INSERT INTO wallet_application (id,user_id,school_id,class_id,account_bank,bank_card_number," +
"account_balance,selected_bank,wallet_type,application_id,status,created_at,updated_at,created_by,updated_by,deleted) " +
"VALUES (?,?,?,?,?,?,?,?,? ,?,'SUBMITTED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
baseId, userId, schoolId, classId, bankName, cardNumber, bankBalance, bankName, "TYPE_II", "APP_" + baseId);
jdbc.update("INSERT INTO wallet_identifier_generation (id,user_id,school_id,class_id,wallet_identifier," +
"cert_private_key,cert_public_key,cert_serial_number,cert_issued_time,status,created_at,updated_at,created_by,updated_by,deleted) " +
"VALUES (?,?,?,?,?,?,?,?,?,'CERT_ISSUED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
baseId + 1, userId, schoolId, classId, walletId, privateKey, publicKey, "CERT_" + baseId, "20260818090000");
jdbc.update("INSERT INTO central_wallet_registration (id,user_id,school_id,class_id,wallet_id,cb_root_signature," +
"wallet_registered,sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
"VALUES (?,?,?,?,?,'CB_ROOT_TEST',TRUE,TRUE,'SENT',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
baseId + 2, userId, schoolId, classId, walletId);
jdbc.update("INSERT INTO smart_contract_generation (id,user_id,school_id,class_id,wallet_id,wallet_type," +
"single_payment_limit,daily_payment_limit,annual_payment_limit,balance_ceiling,validity,contract_plaintext," +
"contract_digest,contract_id,contract_effective_time,sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
"VALUES (?,?,?,?,?,'TYPE_II','50000.00','100000.00','500000.00','500000.00','长期有效'," +
"'CONTRACT_TEXT','0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',?," +
"'20260818090000',TRUE,'SENT',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
baseId + 3, userId, schoolId, classId, walletId, "CONTRACT_" + baseId);
jdbc.update("INSERT INTO central_wallet_activation (id,user_id,school_id,class_id,wallet_id,wallet_activated," +
"wallet_register_status,cb_final_signature,final_time,final_sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
"VALUES (?,?,?,?,?,TRUE,'AVAILABLE','CB_FINAL_TEST','20260818090000',TRUE,'FINAL_SENT'," +
"CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
baseId + 4, userId, schoolId, classId, walletId);
}
public static void deleteWalletFacts(JdbcTemplate jdbc, String userId) {
jdbc.update("DELETE FROM central_wallet_activation WHERE user_id=?", userId);
jdbc.update("DELETE FROM smart_contract_generation WHERE user_id=?", userId);
jdbc.update("DELETE FROM central_wallet_registration WHERE user_id=?", userId);
jdbc.update("DELETE FROM wallet_identifier_generation WHERE user_id=?", userId);
jdbc.update("DELETE FROM wallet_application WHERE user_id=?", userId);
}
}
```
Create `@SpringBootTest` tests using real `JdbcTemplate` and the real cipher. Seed unique IDs and clean them in `@AfterEach`.
```java
@Test
void incompleteModuleThreeCreatesNoSharedRuntimeRows() {
jdbc.update("INSERT INTO central_wallet_activation (id,user_id,school_id,class_id,wallet_id,wallet_activated," +
"final_sent,status,created_at,updated_at,created_by,updated_by,deleted) " +
"VALUES (?,?,?,?,?,FALSE,FALSE,'PENDING',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test',FALSE)",
ACTIVATION_ID, SUBJECT_USER, SCHOOL_ID, CLASS_ID, WALLET_ID);
assertThatThrownBy(() -> service.ensureForSubject(subject()))
.isInstanceOf(BusinessException.class)
.hasMessageContaining("个人数字钱包开通");
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE user_id=?", Integer.class, SUBJECT_USER)).isZero();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM simulated_bank_account WHERE user_id=?", Integer.class, SUBJECT_USER)).isZero();
}
@Test
void completedModuleThreeCreatesAllSharedRuntimeRows() {
WalletOpeningTestData.insertCompletedWallet(jdbc, 980000L, SUBJECT_USER, SCHOOL_ID, CLASS_ID,
WALLET_ID, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行", "6216610100001234567", new BigDecimal("50000.00"));
seedInstitutionIdentifier(SUBJECT_USER, SCHOOL_ID, CLASS_ID, BANK_CODE, "ORG_EXPECTED", true);
WalletPrerequisiteReference reference = service.ensureForSubject(subject());
assertThat(reference.getWalletId()).isEqualTo(WALLET_ID);
assertThat(reference.getOrganizationId()).isEqualTo("ORG_EXPECTED");
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", Integer.class, WALLET_ID)).isEqualTo(1);
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id=?", Integer.class, WALLET_ID)).isEqualTo(1);
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_contract WHERE wallet_id=?", Integer.class, WALLET_ID)).isEqualTo(1);
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_bank_binding WHERE wallet_id=?", Integer.class, WALLET_ID)).isEqualTo(1);
}
@Test
void repeatedProjectionDoesNotOverwriteMutableRuntimeStateOrModuleThreeFacts() {
WalletOpeningTestData.insertCompletedWallet(jdbc, 980000L, SUBJECT_USER, SCHOOL_ID, CLASS_ID,
WALLET_ID, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行", "6216610100001234567", new BigDecimal("50000.00"));
seedInstitutionIdentifier(SUBJECT_USER, SCHOOL_ID, CLASS_ID, BANK_CODE, "ORG_EXPECTED", true);
service.ensureForSubject(subject());
jdbc.update("UPDATE digital_wallet SET balance=321.00,frozen_amount=20.00 WHERE wallet_id=?", WALLET_ID);
jdbc.update("UPDATE wallet_contract SET daily_used_amount=45.00 WHERE wallet_id=?", WALLET_ID);
String activationUpdatedAt = jdbc.queryForObject("SELECT CAST(updated_at AS VARCHAR) FROM central_wallet_activation WHERE id=?", String.class, ACTIVATION_ID);
service.ensureForSubject(subject());
assertThat(jdbc.queryForObject("SELECT balance FROM digital_wallet WHERE wallet_id=?", BigDecimal.class, WALLET_ID)).isEqualByComparingTo("321.00");
assertThat(jdbc.queryForObject("SELECT daily_used_amount FROM wallet_contract WHERE wallet_id=?", BigDecimal.class, WALLET_ID)).isEqualByComparingTo("45.00");
assertThat(jdbc.queryForObject("SELECT CAST(updated_at AS VARCHAR) FROM central_wallet_activation WHERE id=?", String.class, ACTIVATION_ID)).isEqualTo(activationUpdatedAt);
}
@Test
void sameBankCodeFromAnotherScopeCannotSupplyOrganizationId() {
WalletOpeningTestData.insertCompletedWallet(jdbc, 980000L, SUBJECT_USER, SCHOOL_ID, CLASS_ID,
WALLET_ID, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行", "6216610100001234567", new BigDecimal("50000.00"));
seedInstitutionIdentifier(SUBJECT_USER, SCHOOL_ID, CLASS_ID, BANK_CODE, "ORG_EXPECTED", true);
seedInstitutionIdentifier("other-user", SCHOOL_ID + 1, CLASS_ID + 1, BANK_CODE, "ORG_WRONG", true);
assertThat(service.ensureForWallet(WALLET_ID).getOrganizationId()).isEqualTo("ORG_EXPECTED");
}
```
- [ ] **Step 3: Run the new test and verify RED**
Run:
```bash
mvn "-Dtest=WalletPrerequisiteProjectionServiceTest" test
```
Expected: test compilation fails because `WalletPrerequisiteProjectionService` and `WalletPrerequisiteReference` do not exist. This is the intended RED condition.
- [ ] **Step 4: Implement the immutable reference**
```java
public final class WalletPrerequisiteReference {
private final String userId;
private final long schoolId;
private final long classId;
private final String walletId;
private final String bankCode;
private final String organizationId;
public WalletPrerequisiteReference(String userId, long schoolId, long classId,
String walletId, String bankCode, String organizationId) {
this.userId = userId;
this.schoolId = schoolId;
this.classId = classId;
this.walletId = walletId;
this.bankCode = bankCode;
this.organizationId = organizationId;
}
public String getUserId() { return userId; }
public long getSchoolId() { return schoolId; }
public long getClassId() { return classId; }
public String getWalletId() { return walletId; }
public String getBankCode() { return bankCode; }
public String getOrganizationId() { return organizationId; }
}
```
- [ ] **Step 5: Implement transactional source validation and lookup**
Annotate both public methods with `@Transactional`. `ensureForSubject` must query each module 3 table using the same three scope columns, `deleted=FALSE`, expected completion state, descending `created_at`, and `LIMIT 1`. Link identifier, registration, and contract rows to the activation wallet ID. Query module 1 with all scope columns:
```sql
SELECT bank_code,institution_identifier
FROM institution_identifier_application
WHERE user_id=? AND school_id=? AND class_id=?
AND status IN ('ISSUED','FEEDBACKED') AND deleted=FALSE
AND bank_code IS NOT NULL AND institution_identifier IS NOT NULL
ORDER BY created_at DESC LIMIT 1
```
`ensureForWallet` must locate exactly one completed activation by `wallet_id`, `wallet_activated=TRUE`, `final_sent=TRUE`, non-null final signature, and `deleted=FALSE`, then delegate with the located subject. Zero matches returns the same prerequisite validation error; more than one scope returns a data-conflict error.
- [ ] **Step 6: Implement insert-only runtime projection**
Use deterministic account ID `ACCOUNT_` plus `UUID.nameUUIDFromBytes((userId + "|" + bankCode).getBytes(UTF_8))`. Parse `yyyyMMddHHmmss` timestamps when possible, otherwise use `CURRENT_TIMESTAMP`. Insert in this order:
```sql
INSERT INTO simulated_bank_account
(account_id,user_id,bank_code,bank_name,card_number,card_last4,balance,frozen_amount,status,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,0,'ACTIVE',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
INSERT INTO digital_wallet
(wallet_id,user_id,wallet_type,status,balance,frozen_amount,central_bank_confirmation_signature,opened_at,created_at,updated_at)
VALUES (?,?,?,'ACTIVE',0,0,?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
INSERT INTO wallet_certificate
(certificate_serial,wallet_id,public_key,encrypted_private_key,filing_status,central_bank_root_signature,status,issued_at)
VALUES (?,?,?,?, 'REGISTERED',?,'VALID',?)
INSERT INTO wallet_contract
(contract_id,wallet_id,wallet_type,single_payment_limit,daily_payment_limit,annual_payment_limit,balance_limit,
valid_until,original_text,digest,status,daily_used_amount,daily_counter_date,annual_used_amount,annual_counter_year,created_at,updated_at)
VALUES (?,?,?,?,?,?,?,NULL,?,?,'ACTIVE',0,NULL,0,NULL,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
INSERT INTO wallet_bank_binding (wallet_id,bank_account_id,status,bound_at)
VALUES (?,?,'BOUND',CURRENT_TIMESTAMP)
```
Before each insert, query by its primary/unique business key. If a row exists, validate immutable ownership fields and skip the insert. Catch `DuplicateKeyException`, re-read, and run the same validation; never use an updating upsert. Encrypt the module 3 certificate private key with `WalletPrivateKeyCipher.encrypt` only when inserting a missing certificate.
- [ ] **Step 7: Run the new test and verify GREEN**
Run:
```bash
mvn "-Dtest=WalletPrerequisiteProjectionServiceTest" test
```
Expected: 4 tests pass with 0 failures and 0 errors.
- [ ] **Step 8: Commit the shared projection**
```bash
git add src/main/java/com/yau/digitalrmb/shared/wallet src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java src/test/java/com/yau/digitalrmb/testsupport/WalletOpeningTestData.java src/test/resources/schema.sql
git commit -m "feat: project completed wallet prerequisites on demand"
```
---
### Task 3: Make module 4 pull module 3 prerequisites
**Files:**
- Modify: `src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java`
- Modify: `src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java`
**Interfaces:**
- Consumes: `WalletPrerequisiteProjectionService.ensureForSubject(InstitutionKeySubject)`.
- Produces: unchanged `ExchangeResourceRepository.loadContext(ExchangeActor)` API.
- [ ] **Step 1: Rewrite exchange test setup to seed module 3 facts, not shared runtime rows**
Keep module 1 identifiers, keys, module 2 bank currency, and ownership setup. Replace direct inserts into the five shared wallet tables with:
```java
WalletOpeningTestData.insertCompletedWallet(jdbc, 990100L, USER_ID, 1999L, 2999L,
"WALLET_EXCHANGE_TEST", walletKey.getPrivateKey(), walletKey.getPublicKey(),
"中国银行北京分行", "6216610100001234567", new BigDecimal("50000.00"));
```
Add assertions to the successful context test:
```java
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM currency_exchange_order", Integer.class)).isZero();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM exchange_step_log", Integer.class)).isZero();
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE user_id=?", Integer.class, USER_ID)).isEqualTo(1);
```
Change `doesNotCreateDemoWalletWhenWalletOpeningOutputIsMissing` so it deletes module 3 fact rows as well as shared rows and retains the assertion that no wallet is created.
- [ ] **Step 2: Run exchange tests and verify RED**
Run:
```bash
mvn "-Dtest=ExchangeControllerTest" test
```
Expected: the successful context test returns 400 because the repository still reads shared rows without invoking the projector.
- [ ] **Step 3: Inject and invoke the projector**
Add `WalletPrerequisiteProjectionService` to the repository constructor and start `loadContext` with:
```java
InstitutionKeySubject subject = new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId());
walletPrerequisiteProjectionService.ensureForSubject(subject);
Institution institution = loadIssuedInstitution(actor);
```
Do not invoke the projector from module 3 services or controllers.
- [ ] **Step 4: Run exchange tests and verify GREEN**
Run:
```bash
mvn "-Dtest=ExchangeControllerTest" test
```
Expected: both exchange tests pass. The full five-step exchange still updates shared balances and ownership, while the missing-prerequisite test creates no shared rows.
- [ ] **Step 5: Commit module 4 integration**
```bash
git add src/main/java/com/yau/digitalrmb/exchange/infrastructure/persistence/JdbcExchangeResourceRepository.java src/test/java/com/yau/digitalrmb/exchange/interfaces/rest/ExchangeControllerTest.java
git commit -m "feat: load module three wallet data for exchange"
```
---
### Task 4: Make module 5 pull both parties and enforce scoped identifiers
**Files:**
- Modify: `src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java`
- Modify: `src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java`
- Modify: `src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java`
- Modify: `src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java`
**Interfaces:**
- Consumes: `ensureForSubject` for payer and `ensureForWallet` for payee.
- Changes repository API to:
```java
PaymentContext loadContext(PaymentActor actor, String payeeWalletId);
```
- [ ] **Step 1: Add failing payment association tests**
Seed completed module 3 facts for both users. For the payer, retain an already-existing shared wallet with balance `200.00` and module 4 `WALLET/AVAILABLE` coins; this represents completed module 4. Do not seed payee shared rows. Assert context access projects only the payee and creates no payment order:
```java
@Test
void loadsPayeeFromCompletedModuleThreeWithoutCreatingPaymentOrder() throws Exception {
jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id=?", PAYEE_WALLET);
jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id=?", PAYEE_WALLET);
jdbc.update("DELETE FROM simulated_bank_account WHERE user_id=?", PAYEE_USER);
jdbc.update("DELETE FROM digital_wallet WHERE wallet_id=?", PAYEE_WALLET);
mockMvc.perform(get("/api/v1/payments/context")
.param("payeeWalletId", PAYEE_WALLET).with(payer()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.payee.walletId").value(PAYEE_WALLET));
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id=?", Integer.class, PAYEE_WALLET)).isEqualTo(1);
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM payment_order", Integer.class)).isZero();
}
```
Add a second test with the same bank code in another school/class and assert payer/payee `organizationId` values come from their own scoped module 1 records. Add a third test where the payer has completed module 3 but has no `WALLET/AVAILABLE` coins; create and sign the request, then assert `/payer-bank-process` returns 400 and no ownership transfer is created.
- [ ] **Step 2: Run payment tests and verify RED**
Run:
```bash
mvn "-Dtest=PaymentControllerTest" test
```
Expected: payee context returns 400 because no shared payee row exists, or the scope assertion returns the other user's organization identifier.
- [ ] **Step 3: Pass the complete actor through the payment repository boundary**
Change both service call sites:
```java
return resourceRepository.loadContext(actor, payeeWalletId);
PaymentContext context = resourceRepository.loadContext(actor, command.getPayeeWalletId());
```
Update the interface and implementation signature to use `PaymentActor`.
- [ ] **Step 4: Pull payer and payee prerequisites before participant queries**
Implement:
```java
WalletPrerequisiteReference payerRef = projector.ensureForSubject(
new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId()));
WalletPrerequisiteReference payeeRef = projector.ensureForWallet(payeeWalletId);
PaymentParticipant payer = loadParticipant(payerRef);
PaymentParticipant payee = loadParticipant(payeeRef);
```
Replace the dynamic `where` SQL and unscoped `organizationId(bankCode)` method with a fixed wallet query:
```java
private PaymentParticipant loadParticipant(WalletPrerequisiteReference reference) {
List<PaymentParticipant> values = jdbc.query(
"SELECT w.user_id,w.wallet_id,w.wallet_type,cert.certificate_serial,cert.public_key," +
"a.bank_code,a.bank_name,w.balance,COALESCE(w.frozen_amount,0) frozen_amount " +
"FROM digital_wallet w JOIN wallet_certificate cert ON cert.wallet_id=w.wallet_id " +
"AND cert.status='VALID' AND cert.filing_status='REGISTERED' " +
"JOIN wallet_bank_binding binding ON binding.wallet_id=w.wallet_id AND binding.status='BOUND' " +
"JOIN simulated_bank_account a ON a.account_id=binding.bank_account_id AND a.status='ACTIVE' " +
"WHERE w.wallet_id=? AND w.user_id=? AND w.status='ACTIVE' " +
"AND w.central_bank_confirmation_signature IS NOT NULL",
(rs, row) -> new PaymentParticipant(rs.getString("user_id"), rs.getString("wallet_id"),
rs.getString("wallet_type"), rs.getString("certificate_serial"), rs.getString("public_key"),
rs.getString("bank_code"), rs.getString("bank_name"), reference.getOrganizationId(),
rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")),
reference.getWalletId(), reference.getUserId());
if (values.isEmpty()) throw validation("付款方或收款方钱包运行态不存在或状态异常");
return values.get(0);
}
```
Delete the query that selects `institution_identifier_application` by `bank_code` only. The scoped organization ID must come from the projection reference.
- [ ] **Step 5: Run payment tests and verify GREEN**
Run:
```bash
mvn "-Dtest=PaymentControllerTest" test
```
Expected: all payment tests pass; payee-only module 3 data is readable, scoped organization IDs are correct, and a payer without module 4 coins cannot proceed through payer-bank processing.
- [ ] **Step 6: Commit module 5 integration**
```bash
git add src/main/java/com/yau/digitalrmb/payment/domain/repository/PaymentResourceRepository.java src/main/java/com/yau/digitalrmb/payment/application/service/PaymentApplicationService.java src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java
git commit -m "feat: load scoped wallet prerequisites for payment"
```
---
### Task 5: Verify the complete module chain and document actual failures
**Files:**
- Modify only if verification exposes a regression caused by Tasks 1-4.
**Interfaces:**
- Consumes: completed implementation from Tasks 1-4.
- Produces: reproducible compile/test evidence and a clean tracked worktree.
- [ ] **Step 1: Check formatting, conflicts, and production compilation**
Run:
```bash
git diff --check
rg -n "^(<<<<<<<|=======|>>>>>>>)" src
mvn -DskipTests compile
```
Expected: `git diff --check` and compile exit 0; `rg` finds no conflict markers.
- [ ] **Step 2: Run the focused module-chain tests**
Run:
```bash
mvn "-Dtest=WalletPrerequisiteProjectionServiceTest,DigitalCurrencyGenerationStockIntegrationTest,ExchangeControllerTest,PaymentControllerTest" test
```
Expected: all focused tests pass with 0 failures and 0 errors.
- [ ] **Step 3: Run the full suite without hiding existing failures**
Run:
```bash
mvn test
```
Expected: report the exact total, failure count, and test names. Fix only failures introduced by Tasks 1-4; retain and explicitly report unrelated existing failures such as string-versus-numeric API error-code assertions.
- [ ] **Step 4: Verify repository state and module 3 write isolation**
Run:
```bash
rg -n "UPDATE (wallet_application|bank_received_wallet_application|wallet_verification_record|wallet_identifier_generation|wallet_registration_request|central_wallet_registration|smart_contract_generation|central_wallet_activation)" src/main/java/com/yau/digitalrmb/exchange src/main/java/com/yau/digitalrmb/payment src/main/java/com/yau/digitalrmb/shared/wallet
git status --short --branch
git log --oneline -6
```
Expected: no downstream update statements target module 3 fact tables. Only user-owned pre-existing untracked documents may remain outside the committed implementation.

@ -0,0 +1,127 @@
# 模块 4/5 下游按需读取钱包前置数据设计
## 目标
建立模块 1 至模块 5 的真实业务数据关联,同时保持模块边界:模块 3 只记录钱包开通实验事实,不提前创建模块 4 或模块 5 的操作数据;模块 4、模块 5 在用户实际进入时主动读取前置模块结果。
## 已确认的业务边界
- 模块 3 完成钱包申请、证书生成、央行备案、合约生成、钱包激活和最终确认,只写 `walletopening` 模块自己的结果表。
- 模块 4 的付款用户必须完成模块 1、模块 2、模块 3模块 4 从模块 2 读取商业银行币串库存,从模块 3 读取已开通钱包资料。
- 模块 5 的付款方必须已经通过模块 4 取得数字货币币串;模块 5 的收款方只需要完成模块 3 并拥有已激活钱包。
- 下游读取必须按 `user_id + school_id + class_id` 隔离。按银行编码查询机构标识时也必须保留该隔离范围。
- 进入模块页面只能加载或初始化前置资源,不得创建兑换订单、支付订单或对应步骤记录。
## 数据所有权
### 模块 3 实验事实
以下表继续由模块 3 独占写入:
- `wallet_application`
- `bank_received_wallet_application`
- `wallet_verification_record`
- `wallet_identifier_generation`
- `wallet_registration_request`
- `central_wallet_registration`
- `smart_contract_generation`
- `central_wallet_activation`
模块 4、模块 5 对这些表只读,不反向修改。
### 共享钱包运行态
以下表定义为模块间共享的、可交易的钱包运行态,不属于模块 4 或模块 5 的步骤数据:
- `digital_wallet`
- `wallet_certificate`
- `wallet_contract`
- `simulated_bank_account`
- `wallet_bank_binding`
共享运行态只在模块 4 或模块 5 首次需要该钱包时,由下游按模块 3 最终结果幂等初始化。后续余额、冻结金额和额度累计值由真实兑换、支付业务更新,初始化逻辑不得覆盖已有值。
## 读取与初始化流程
新增一个共享的前置钱包读取服务,供模块 4 和模块 5 调用。
1. 接收当前主体 `userId`、`schoolId`、`classId`;按钱包标识读取收款人时,先定位钱包所属主体,再使用其完整主体范围校验。
2. 查询模块 3 同一主体的最新一轮结果,所有组成数据必须来自同一用户、学校和班级。
3. 验证以下完成条件:
- `central_wallet_activation.wallet_activated = true`
- `central_wallet_activation.final_sent = true`
- `central_wallet_activation.cb_final_signature` 非空;
- `central_wallet_registration.cb_root_signature` 非空且备案流程已完成;
- `wallet_identifier_generation.status = 'CERT_ISSUED'`,证书公钥、私钥和序列号完整;
- `smart_contract_generation.status = 'SENT'`,合约标识和额度字段完整;
- `wallet_application.status = 'SUBMITTED'`,银行卡、开户行、钱包类型和账户余额完整。
4. 任一条件不满足时返回业务校验错误,错误信息指出尚未完成的钱包开通前置步骤,不创建任何共享运行态记录。
5. 条件全部满足时,在一个事务内按依赖顺序插入缺失的共享记录:银行卡账户、钱包、证书、合约、绑定关系。
6. 如果共享钱包已经存在,只验证其归属与模块 3 结果一致,然后返回;不得更新余额、冻结金额、额度累计值、证书密钥或绑定关系。
7. 初始化完成后,模块 4、模块 5 继续使用现有共享运行态仓储完成签名、冻结、扣款、限额累计和余额更新。
## 字段来源
| 共享字段 | 模块 3 来源 |
|---|---|
| 钱包标识 | `central_wallet_activation.wallet_id` |
| 钱包类型 | `smart_contract_generation.wallet_type` |
| 央行最终确认签名 | `central_wallet_activation.cb_final_signature` |
| 钱包开户时间 | `central_wallet_activation.final_time`,无法解析时使用共享记录初始化时间 |
| 证书序列号、公私钥 | `wallet_identifier_generation` 对应字段 |
| 央行根签名 | `central_wallet_registration.cb_root_signature` |
| 合约标识、额度、摘要 | `smart_contract_generation` 对应字段 |
| 银行编码 | 当前主体对应的 `institution_identifier_application.bank_code` |
| 银行名称、银行卡号、银行账户初始余额 | `wallet_application` 对应字段 |
| 钱包初始余额、冻结金额 | `0.00` |
| 银行账户冻结金额 | `0.00` |
共享银行账户标识使用稳定、可重复生成的值,保证重复初始化不会产生多条账户记录。银行卡末四位由完整卡号计算。
## 模块调用关系
### 模块 4
`GET /api/v1/exchanges/context` 在读取兑换上下文前调用共享前置钱包读取服务。成功后继续通过现有 `JdbcExchangeResourceRepository` 读取钱包、证书、合约、银行卡账户及模块 2 币串库存。只有创建兑换请求时才写 `currency_exchange_order` 等模块 4 表。
### 模块 5
支付上下文加载时分别处理双方:
- 付款方:读取或初始化其模块 3 钱包运行态,并继续校验模块 4 形成的 `WALLET/AVAILABLE` 币串;没有币串时拒绝支付。
- 收款方:按收款钱包标识读取模块 3 最终结果并初始化运行态,不要求其完成模块 4。
模块 5 查询机构标识时使用钱包所属用户、学校、班级和银行编码共同限定,禁止只按 `bank_code` 取最新记录。
## 并发与幂等
- 初始化方法使用事务。
- 共享表已有唯一键继续作为并发保护;出现并发插入时重新读取并验证归属,不覆盖先写入的数据。
- 初始化只执行“缺失则插入”,不能使用会更新已有余额或额度字段的 upsert。
- 如果发现共享记录与模块 3 的钱包标识、用户或银行卡绑定不一致,返回冲突错误,不自动修正。
## 错误处理
- 模块 3 未完成:返回业务校验错误,并指出需要先完成个人数字钱包开通实验。
- 模块 1 机构标识缺失或主体不匹配:返回业务校验错误,不允许跨用户或跨班级回退。
- 模块 4 币串库存不足:沿用模块 4 现有库存错误。
- 模块 5 付款方没有模块 4 形成的钱包币串:返回余额或币串不足错误。
- 模块 3 数据与已存在共享运行态冲突:返回数据冲突错误,保留双方数据供排查。
## 测试与验收
1. 模块 3 未完成时访问模块 4接口拒绝并且五张共享运行态表没有新增记录。
2. 模块 3 完成后访问模块 4能够读取钱包、合约、证书、银行卡和模块 2 库存;模块 4 订单仍为空。
3. 重复访问模块 4 不覆盖已发生变化的钱包余额、银行卡余额和额度累计值。
4. 模块 5 付款方完成模块 3 但未取得模块 4 币串时,支付被拒绝。
5. 模块 5 收款方只完成模块 3 时,可以作为合法收款钱包被读取。
6. 相同银行编码下存在其他用户、学校或班级数据时,不会读取到错误机构标识或钱包结果。
7. 模块 4/5 读取和交易后,模块 3 的八张实验事实表内容不被修改。
8. 现有模块 2→4 币串权属和模块 4→5 支付币串链路继续通过定向集成测试。
## 不在本次范围
- 不修改模块 3 页面步骤、接口顺序或评分逻辑。
- 不把模块 3 的历史轮次迁移到共享运行态。
- 不重构模块 4/5 的交易流水、签名或央行权属模型。
- 不处理当前全量测试中与本关联改动无关的错误码断言差异。

@ -1,34 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "企业法人提交申请输出结果")
public class CorporateApplicationResult {
@Schema(description = "申请编号", example = "CORP_APP_20260801_001")
private final String applicationId;
@Schema(description = "企业名称", example = "创新科技公司")
private final String corpName;
@Schema(description = "统一社会信用代码", example = "91440101MA5XXXXXX")
private final String creditCode;
@Schema(description = "法人代表", example = "张三")
private final String legalPerson;
@Schema(description = "注册资本", example = "10000000.00")
private final String capital;
@Schema(description = "申请时间", example = "20260801100000")
private final String applyTime;
@Schema(description = "申请状态", example = "已提交")
private final String status;
public CorporateApplicationResult(String applicationId, String corpName, String creditCode,
String legalPerson, String capital, String applyTime, String status) {
this.applicationId = applicationId;
this.corpName = corpName;
this.creditCode = creditCode;
this.legalPerson = legalPerson;
this.capital = capital;
this.applyTime = applyTime;
this.status = status;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "拼接原文结果")
public class CorporateConcatenateResult {
@Schema(description = "拼接后的原文", example = "CORP_OPEN|创新科技公司|91440101MA5XXXXXX|张三|10000000.00|软件开发、技术服务|20260801100000")
private final String concatenatedMessage;
public CorporateConcatenateResult(String concatenatedMessage) {
this.concatenatedMessage = concatenatedMessage;
}
}

@ -1,18 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "SM3摘要运算结果")
public class CorporateDigestResult {
@Schema(description = "哈希算法", example = "SM3")
private final String algorithm;
@Schema(description = "摘要值(十六进制)", example = "3A4B5C6D7E8F9A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4")
private final String digestValue;
public CorporateDigestResult(String algorithm, String digestValue) {
this.algorithm = algorithm;
this.digestValue = digestValue;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "提取摘要原文结果")
public class CorporateExtractResult {
@Schema(description = "提取的摘要原文", example = "91440101MA5XXXXXX|20260801101000")
private final String extractMessage;
public CorporateExtractResult(String extractMessage) {
this.extractMessage = extractMessage;
}
}

@ -1,28 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "银行端审核结果")
public class CorporateVerifyResult {
@Schema(description = "审核编号", example = "VERIFY_20260801_001")
private final String verifyId;
@Schema(description = "申请编号", example = "CORP_APP_20260801_001")
private final String applicationId;
@Schema(description = "审核状态", example = "APPROVED")
private final String status;
@Schema(description = "审核人", example = "李经理")
private final String reviewer;
@Schema(description = "审核时间", example = "20260801101000")
private final String verifyTime;
public CorporateVerifyResult(String verifyId, String applicationId, String status,
String reviewer, String verifyTime) {
this.verifyId = verifyId;
this.applicationId = applicationId;
this.status = status;
this.reviewer = reviewer;
this.verifyTime = verifyTime;
}
}

@ -1,36 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "密钥信息")
public class CorporateWalletKeyResult {
@Schema(description = "密钥类型(如:央行第一私钥、央行第一公钥、商业银行第二私钥、企业法人私钥等)")
private final String keyType;
@Schema(description = "持有方(如:中国人民银行、中国银行、企业法人等)")
private final String holder;
@Schema(description = "密码算法SM2")
private final String keyAlgorithm;
@Schema(description = "密钥值(十六进制)")
private final String keyValue;
@Schema(description = "是否为私钥")
private final boolean privateKey;
@Schema(description = "密钥来源REUSED-复用已有, NEW-新生成")
private final String source;
public CorporateWalletKeyResult(String keyType, String holder, String keyAlgorithm,
String keyValue, boolean privateKey, String source) {
this.keyType = keyType;
this.holder = holder;
this.keyAlgorithm = keyAlgorithm;
this.keyValue = keyValue;
this.privateKey = privateKey;
this.source = source;
}
public String getKeyType() { return keyType; }
public String getHolder() { return holder; }
public String getKeyAlgorithm() { return keyAlgorithm; }
public String getKeyValue() { return keyValue; }
public boolean isPrivateKey() { return privateKey; }
public String getSource() { return source; }
}

@ -1,149 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoMapper;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyPairResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
public class CorporateWalletKeyService {
@Resource
private InstitutionKeyService institutionKeyService;
@Resource
private InstitutionIdentityCryptography cryptography;
@Resource
private CorporateWalletKeyInfoMapper keyInfoMapper;
/**
*
*
*
*/
@Transactional
public List<CorporateWalletKeyResult> getAllKeys(InstitutionKeySubject subject, String operator) {
List<CorporateWalletKeyResult> results = new ArrayList<>();
// 1. 央行第一密钥(复用 institution_sm2_key
InstitutionKeyPairResult centralKey = institutionKeyService.centralBankKey(subject, operator);
results.add(findOrCreate(subject, "央行第一私钥", "中国人民银行",
centralKey.getPrivateKey(), true, "REUSED", operator));
results.add(findOrCreate(subject, "央行第一公钥", "中国人民银行",
centralKey.getPublicKey(), false, "REUSED", operator));
// 2. 商业银行第二密钥(复用 institution_sm2_key
InstitutionKeyPairResult bankKey = institutionKeyService.commercialBankKey(subject, operator);
results.add(findOrCreate(subject, "商业银行第二私钥", "中国银行",
bankKey.getPrivateKey(), true, "REUSED", operator));
results.add(findOrCreate(subject, "商业银行第二公钥", "中国银行",
bankKey.getPublicKey(), false, "REUSED", operator));
// 3. 企业法人密钥(新增)
String legalPersonName = subject.getUserName() != null ? subject.getUserName() : "企业法人";
String[] legalPersonPair = findOrCreateNewKeyPair(subject, "企业法人私钥", "企业法人公钥",
legalPersonName, operator);
results.add(buildResult("企业法人私钥", legalPersonName, legalPersonPair[0], true, "NEW"));
results.add(buildResult("企业法人公钥", legalPersonName, legalPersonPair[1], false, "NEW"));
// 4. 数币操作员密钥(新增)
String[] operatorPair = findOrCreateNewKeyPair(subject, "数币操作员私钥", "数币操作员公钥",
"王五", operator);
results.add(buildResult("数币操作员私钥", "王五", operatorPair[0], true, "NEW"));
results.add(buildResult("数币操作员公钥", "王五", operatorPair[1], false, "NEW"));
// 5. 数币复核员密钥(新增)
String[] reviewerPair = findOrCreateNewKeyPair(subject, "数币复核员私钥", "数币复核员公钥",
"赵六", operator);
results.add(buildResult("数币复核员私钥", "赵六", reviewerPair[0], true, "NEW"));
results.add(buildResult("数币复核员公钥", "赵六", reviewerPair[1], false, "NEW"));
// 6. 监管机构密钥(新增)
String[] regulatorPair = findOrCreateNewKeyPair(subject, "监管机构私钥", "监管机构公钥",
"民政部公益监管中心", operator);
results.add(buildResult("监管机构私钥", "民政部公益监管中心", regulatorPair[0], true, "NEW"));
results.add(buildResult("监管机构公钥", "民政部公益监管中心", regulatorPair[1], false, "NEW"));
return results;
}
/**
* corporate_wallet_key_info
*/
private CorporateWalletKeyResult findOrCreate(InstitutionKeySubject subject, String keyType,
String holder, String keyValue, boolean isPrivateKey,
String source, String operator) {
CorporateWalletKeyInfoEntity existing = find(subject, keyType);
if (existing != null) {
return buildResult(existing.getKeyType(), existing.getHolder(), existing.getKeyValue(),
existing.getIsPrivateKey(), existing.getSource());
}
saveKeyInfo(subject, keyType, holder, keyValue, isPrivateKey, source, operator);
return buildResult(keyType, holder, keyValue, isPrivateKey, source);
}
/**
* SM2 corporate_wallet_key_info
* [privateKey, publicKey]
*/
private String[] findOrCreateNewKeyPair(InstitutionKeySubject subject, String privateKeyType,
String publicKeyType, String holder, String operator) {
CorporateWalletKeyInfoEntity existingPrivate = find(subject, privateKeyType);
if (existingPrivate != null) {
CorporateWalletKeyInfoEntity existingPublic = find(subject, publicKeyType);
return new String[]{existingPrivate.getKeyValue(),
existingPublic != null ? existingPublic.getKeyValue() : ""};
}
InstitutionSm2KeyPair pair = cryptography.generateSm2KeyPair();
saveKeyInfo(subject, privateKeyType, holder, pair.getPrivateKey(), true, "NEW", operator);
saveKeyInfo(subject, publicKeyType, holder, pair.getPublicKey(), false, "NEW", operator);
return new String[]{pair.getPrivateKey(), pair.getPublicKey()};
}
private CorporateWalletKeyInfoEntity find(InstitutionKeySubject subject, String keyType) {
return keyInfoMapper.selectOne(new LambdaQueryWrapper<CorporateWalletKeyInfoEntity>()
.eq(CorporateWalletKeyInfoEntity::getUserId, subject.getUserId())
.eq(CorporateWalletKeyInfoEntity::getSchoolId, subject.getSchoolId())
.eq(CorporateWalletKeyInfoEntity::getClassId, subject.getClassId())
.eq(CorporateWalletKeyInfoEntity::getKeyType, keyType)
.eq(CorporateWalletKeyInfoEntity::getDeleted, false)
.orderByDesc(CorporateWalletKeyInfoEntity::getCreatedAt)
.last("LIMIT 1"));
}
private void saveKeyInfo(InstitutionKeySubject subject, String keyType, String holder,
String keyValue, boolean isPrivateKey, String source, String operator) {
CorporateWalletKeyInfoEntity entity = new CorporateWalletKeyInfoEntity();
entity.setUserId(subject.getUserId());
entity.setSchoolId(subject.getSchoolId());
entity.setClassId(subject.getClassId());
entity.setKeyType(keyType);
entity.setHolder(holder);
entity.setKeyAlgorithm("SM2");
entity.setKeyValue(keyValue);
entity.setIsPrivateKey(isPrivateKey);
entity.setSource(source);
LocalDateTime now = LocalDateTime.now();
entity.setCreatedAt(now);
entity.setUpdatedAt(now);
entity.setCreatedBy(operator);
entity.setUpdatedBy(operator);
entity.setDeleted(false);
keyInfoMapper.insert(entity);
}
private CorporateWalletKeyResult buildResult(String keyType, String holder, String keyValue,
boolean isPrivateKey, String source) {
return new CorporateWalletKeyResult(keyType, holder, "SM2", keyValue, isPrivateKey, source);
}
}

@ -1,39 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "生成对公钱包输出结果")
public class CorporateWalletResult {
@Schema(description = "钱包ID", example = "CORP_3A4B5C6D7E8F9A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4")
private final String walletId;
@Schema(description = "企业名称", example = "创新科技公司")
private final String corpName;
@Schema(description = "统一社会信用代码", example = "91440101MA5XXXXXX")
private final String creditCode;
@Schema(description = "钱包类型", example = "对公钱包")
private final String walletType;
@Schema(description = "钱包状态", example = "ACTIVATED")
private final String status;
@Schema(description = "激活时间", example = "20260801101000")
private final String activateTime;
@Schema(description = "开立渠道:临柜开立 / 远程开立(复用时标注原渠道)")
private final String channel;
public CorporateWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime) {
this(walletId, corpName, creditCode, walletType, status, activateTime, "临柜开立");
}
public CorporateWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime, String channel) {
this.walletId = walletId;
this.corpName = corpName;
this.creditCode = creditCode;
this.walletType = walletType;
this.status = status;
this.activateTime = activateTime;
this.channel = channel;
}
}

@ -1,220 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.CorporateWalletApplicationRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.infrastructure.SmInstitutionIdentityCryptography;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Service
public class CorporateWalletService {
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
@Resource
private CorporateWalletApplicationRepository repository;
@Resource
private RemoteWalletApplicationRepository remoteRepository;
@Resource
private SmInstitutionIdentityCryptography cryptography;
/**
*
*/
@Transactional
public CorporateConcatenateResult concatenate(InstitutionKeySubject subject,
CorporateWalletApplicationRequest request) {
CorporateWalletApplication app = findOrCreate(subject);
app.fillForm(request.getCorpName(), request.getCreditCode(), request.getLegalPerson(),
request.getCapital(), request.getBusinessScope(), request.getApplyTime());
String concatenatedMessage = buildConcatenatedMessage(
request.getCorpName(), request.getCreditCode(), request.getLegalPerson(),
request.getCapital(), request.getBusinessScope(), request.getApplyTime());
app.concatenate(concatenatedMessage);
repository.update(app);
return new CorporateConcatenateResult(concatenatedMessage);
}
/**
*
*/
@Transactional
public CorporateApplicationResult outputApplication(InstitutionKeySubject subject,
CorporateWalletApplicationRequest request) {
CorporateWalletApplication app = findOrCreate(subject);
if (app.getStatus() != null
&& app.getStatus().ordinal() >= CorporateWalletApplication.Status.SUBMITTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "申请已提交,请勿重复操作");
}
app.fillForm(request.getCorpName(), request.getCreditCode(), request.getLegalPerson(),
request.getCapital(), request.getBusinessScope(), request.getApplyTime());
String applyTime = request.getApplyTime();
String applicationId = "CORP_APP_" + applyTime.substring(0, 8) + "_001";
// 拼接报文
String concatenatedMessage = buildConcatenatedMessage(
request.getCorpName(), request.getCreditCode(), request.getLegalPerson(),
request.getCapital(), request.getBusinessScope(), applyTime);
app.concatenate(concatenatedMessage);
// 构建申请报文JSON
String applicationMessage = buildApplicationMessage(
applicationId, request.getCorpName(), request.getCreditCode(),
request.getLegalPerson(), request.getCapital(), applyTime);
app.outputApplication(applicationId, applicationMessage);
repository.update(app);
return new CorporateApplicationResult(
applicationId, request.getCorpName(), request.getCreditCode(),
request.getLegalPerson(), request.getCapital(), applyTime, "已提交");
}
/**
*
*/
@Transactional
public CorporateVerifyResult verify(InstitutionKeySubject subject) {
CorporateWalletApplication app = findRequired(subject);
if (app.getStatus() == null
|| app.getStatus().ordinal() < CorporateWalletApplication.Status.SUBMITTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提交申请");
}
if (app.getStatus().ordinal() >= CorporateWalletApplication.Status.APPROVED.ordinal()) {
return new CorporateVerifyResult(
app.getVerifyId(), app.getApplicationId(), "APPROVED",
app.getReviewer(), app.getVerifyTime());
}
String verifyTime = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String verifyId = "VERIFY_" + LocalDateTime.now().format(DATE_FORMAT) + "_001";
String reviewer = "李经理";
app.approve(verifyId, reviewer, verifyTime);
repository.update(app);
return new CorporateVerifyResult(verifyId, app.getApplicationId(), "APPROVED", reviewer, verifyTime);
}
/**
*
*/
@Transactional
public CorporateExtractResult extract(InstitutionKeySubject subject) {
CorporateWalletApplication app = findRequired(subject);
if (app.getStatus() == null
|| app.getStatus().ordinal() < CorporateWalletApplication.Status.APPROVED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成审核");
}
// 提取:信用代码|审核时间
String extractMessage = app.getCreditCode() + "|" + app.getVerifyTime();
app.extract(extractMessage);
repository.update(app);
return new CorporateExtractResult(extractMessage);
}
/**
* SM3
*/
@Transactional
public CorporateDigestResult computeDigest(InstitutionKeySubject subject) {
CorporateWalletApplication app = findRequired(subject);
if (app.getExtractMessage() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提取摘要原文");
}
if (app.getDigestValue() != null) {
return new CorporateDigestResult(app.getDigestAlgorithm(), app.getDigestValue());
}
String digest = cryptography.sm3(app.getExtractMessage());
app.computeDigest("SM3", digest);
repository.update(app);
return new CorporateDigestResult("SM3", digest);
}
/**
*
*
* ID
*/
@Transactional
public CorporateWalletResult outputWallet(InstitutionKeySubject subject) {
CorporateWalletApplication app = findRequired(subject);
if (app.getDigestValue() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM3摘要运算");
}
if (app.getWalletId() != null) {
return new CorporateWalletResult(
app.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", app.getActivateTime(), "临柜开立");
}
// 跨渠道检查:远程开立是否已激活同信用代码的钱包
RemoteWalletApplication remoteActivated = remoteRepository
.findActivatedByCreditCode(app.getCreditCode(), subject.getUserId(),
subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (remoteActivated != null) {
// 复用远程开立的钱包ID不再生成新钱包
app.activateWallet(remoteActivated.getWalletId(), remoteActivated.getActivateTime());
repository.update(app);
return new CorporateWalletResult(
remoteActivated.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", remoteActivated.getActivateTime(), "远程开立");
}
String walletId = "CORP_" + app.getDigestValue();
String activateTime = app.getVerifyTime() != null ? app.getVerifyTime()
: LocalDateTime.now().format(TIMESTAMP_FORMAT);
app.activateWallet(walletId, activateTime);
repository.update(app);
return new CorporateWalletResult(
walletId, app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", activateTime, "临柜开立");
}
private CorporateWalletApplication findOrCreate(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseGet(() -> repository.save(
CorporateWalletApplication.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId())));
}
private CorporateWalletApplication findRequired(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "对公钱包申请不存在,请先提交申请"));
}
private String buildConcatenatedMessage(String corpName, String creditCode, String legalPerson,
String capital, String businessScope, String applyTime) {
return "CORP_OPEN|" + corpName + "|" + creditCode + "|" + legalPerson + "|"
+ capital + "|" + businessScope + "|" + applyTime;
}
private String buildApplicationMessage(String applicationId, String corpName, String creditCode,
String legalPerson, String capital, String applyTime) {
return "{\"applicationId\":\"" + applicationId + "\","
+ "\"corpName\":\"" + corpName + "\","
+ "\"creditCode\":\"" + creditCode + "\","
+ "\"legalPerson\":\"" + legalPerson + "\","
+ "\"capital\":\"" + capital + "\","
+ "\"applyTime\":\"" + applyTime + "\","
+ "\"status\":\"已提交\"}";
}
}

@ -1,59 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-步骤一输出结果")
public class RemoteApplicationResult {
@Schema(description = "申请编号", example = "REMOTE_APP_20260801_001")
private final String applicationId;
@Schema(description = "企业名称", example = "阳光公益基金")
private final String corpName;
@Schema(description = "统一社会信用代码", example = "91440101MA6XXXXXX")
private final String creditCode;
@Schema(description = "法人代表", example = "李四")
private final String legalPerson;
@Schema(description = "手机号码", example = "13900139000")
private final String phone;
@Schema(description = "申请时间", example = "20260801110000")
private final String applyTime;
@Schema(description = "人脸识别结果")
private final FaceRecognitionData faceRecognition;
@Schema(description = "申请状态", example = "人脸识别已通过,等待电子签约")
private final String status;
public RemoteApplicationResult(String applicationId, String corpName, String creditCode,
String legalPerson, String phone, String applyTime,
String faceStatus, String liveDetection, String similarityScore,
String verifyTime, String status) {
this.applicationId = applicationId;
this.corpName = corpName;
this.creditCode = creditCode;
this.legalPerson = legalPerson;
this.phone = phone;
this.applyTime = applyTime;
this.faceRecognition = new FaceRecognitionData(faceStatus, liveDetection, similarityScore, verifyTime);
this.status = status;
}
@Getter
@Schema(description = "人脸识别详情")
public static class FaceRecognitionData {
@Schema(description = "人脸识别状态", example = "PASSED")
private final String status;
@Schema(description = "活体检测状态", example = "PASSED")
private final String liveDetection;
@Schema(description = "相似度", example = "98.7%")
private final String similarityScore;
@Schema(description = "验证时间", example = "20260801110230")
private final String verifyTime;
public FaceRecognitionData(String status, String liveDetection, String similarityScore, String verifyTime) {
this.status = status;
this.liveDetection = liveDetection;
this.similarityScore = similarityScore;
this.verifyTime = verifyTime;
}
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-拼接原文结果")
public class RemoteConcatenateResult {
@Schema(description = "拼接后的原文", example = "REMOTE_OPEN|阳光公益基金|91440101MA6XXXXXX|李四|13900139000|20260801110000")
private final String concatenatedMessage;
public RemoteConcatenateResult(String concatenatedMessage) {
this.concatenatedMessage = concatenatedMessage;
}
}

@ -1,35 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-步骤二电子签约输出结果")
public class RemoteContractResult {
@Schema(description = "合约编号", example = "E_SIGN_20260801_001")
private final String contractId;
@Schema(description = "申请编号", example = "REMOTE_APP_20260801_001")
private final String applicationId;
@Schema(description = "签约人", example = "李四")
private final String signer;
@Schema(description = "人脸识别确认", example = "PASSED")
private final String faceRecognitionConfirm;
@Schema(description = "SM2签名值")
private final String signature;
@Schema(description = "签约时间", example = "20260801110500")
private final String signTime;
@Schema(description = "签约状态", example = "SIGNED")
private final String status;
public RemoteContractResult(String contractId, String applicationId, String signer,
String faceRecognitionConfirm, String signature,
String signTime, String status) {
this.contractId = contractId;
this.applicationId = applicationId;
this.signer = signer;
this.faceRecognitionConfirm = faceRecognitionConfirm;
this.signature = signature;
this.signTime = signTime;
this.status = status;
}
}

@ -1,18 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-SM3摘要运算结果")
public class RemoteDigestResult {
@Schema(description = "哈希算法", example = "SM3")
private final String algorithm;
@Schema(description = "摘要值(十六进制)")
private final String digestValue;
public RemoteDigestResult(String algorithm, String digestValue) {
this.algorithm = algorithm;
this.digestValue = digestValue;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-提取摘要原文结果")
public class RemoteExtractResult {
@Schema(description = "提取的摘要原文")
private final String extractMessage;
public RemoteExtractResult(String extractMessage) {
this.extractMessage = extractMessage;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-人脸识别状态结果")
public class RemoteFaceRecognitionResult {
@Schema(description = "人脸识别状态1=未成功2=已成功", example = "2")
private final Integer faceRecognitionStatus;
public RemoteFaceRecognitionResult(Integer faceRecognitionStatus) {
this.faceRecognitionStatus = faceRecognitionStatus;
}
}

@ -1,21 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-SM2签名结果")
public class RemoteSignatureResult {
@Schema(description = "签名私钥(十六进制)")
private final String privateKey;
@Schema(description = "签名算法", example = "SM2")
private final String algorithm;
@Schema(description = "签名值(十六进制)")
private final String signature;
public RemoteSignatureResult(String privateKey, String algorithm, String signature) {
this.privateKey = privateKey;
this.algorithm = algorithm;
this.signature = signature;
}
}

@ -1,44 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "远程开立-步骤三公钱包输出结果")
public class RemoteWalletResult {
@Schema(description = "钱包ID", example = "CORP_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")
private final String walletId;
@Schema(description = "企业名称", example = "阳光公益基金")
private final String corpName;
@Schema(description = "统一社会信用代码", example = "91440101MA6XXXXXX")
private final String creditCode;
@Schema(description = "钱包类型", example = "对公钱包")
private final String walletType;
@Schema(description = "钱包状态", example = "ACTIVATED")
private final String status;
@Schema(description = "激活时间", example = "20260801110500")
private final String activateTime;
@Schema(description = "人脸识别记录", example = "已验证")
private final String faceRecognitionLog;
@Schema(description = "开立渠道:远程开立 / 临柜开立(复用时标注原渠道)")
private final String channel;
public RemoteWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime,
String faceRecognitionLog) {
this(walletId, corpName, creditCode, walletType, status, activateTime, faceRecognitionLog, "远程开立");
}
public RemoteWalletResult(String walletId, String corpName, String creditCode,
String walletType, String status, String activateTime,
String faceRecognitionLog, String channel) {
this.walletId = walletId;
this.corpName = corpName;
this.creditCode = creditCode;
this.walletType = walletType;
this.status = status;
this.activateTime = activateTime;
this.faceRecognitionLog = faceRecognitionLog;
this.channel = channel;
}
}

@ -1,263 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteWalletApplicationRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionSm2KeyPair;
import com.yau.digitalrmb.institutionidentity.infrastructure.SmInstitutionIdentityCryptography;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Service
public class RemoteWalletService {
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
@Resource
private RemoteWalletApplicationRepository repository;
@Resource
private CorporateWalletApplicationRepository corporateRepository;
@Resource
private SmInstitutionIdentityCryptography cryptography;
// ==================== 步骤一:企业法人在线填写信息 ====================
@Transactional
public RemoteConcatenateResult concatenate(InstitutionKeySubject subject,
RemoteWalletApplicationRequest request) {
RemoteWalletApplication app = findOrCreate(subject);
app.fillForm(request.getCorpName(), request.getCreditCode(),
request.getLegalPerson(), request.getPhone(), request.getApplyTime());
String message = buildConcatenatedMessage(
request.getCorpName(), request.getCreditCode(),
request.getLegalPerson(), request.getPhone(), request.getApplyTime());
app.concatenate(message);
repository.update(app);
return new RemoteConcatenateResult(message);
}
@Transactional
public RemoteFaceRecognitionResult updateFaceRecognition(InstitutionKeySubject subject, int status) {
RemoteWalletApplication app = findOrCreate(subject);
app.updateFaceRecognition(status);
repository.update(app);
return new RemoteFaceRecognitionResult(status);
}
@Transactional
public RemoteApplicationResult outputApplication(InstitutionKeySubject subject,
RemoteWalletApplicationRequest request) {
RemoteWalletApplication app = findOrCreate(subject);
if (app.getStatus() != null
&& app.getStatus().ordinal() >= RemoteWalletApplication.Status.FACE_RECOGNIZED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "申请已输出,请勿重复操作");
}
if (app.getFaceRecognitionStatus() == null || app.getFaceRecognitionStatus() != 2) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成人脸识别");
}
app.fillForm(request.getCorpName(), request.getCreditCode(),
request.getLegalPerson(), request.getPhone(), request.getApplyTime());
String applyTime = request.getApplyTime();
String applicationId = "REMOTE_APP_" + applyTime.substring(0, 8) + "_001";
String concatenatedMessage = buildConcatenatedMessage(
request.getCorpName(), request.getCreditCode(),
request.getLegalPerson(), request.getPhone(), applyTime);
app.concatenate(concatenatedMessage);
String faceVerifyTime = LocalDateTime.now().format(TIMESTAMP_FORMAT);
app.outputApplication(applicationId, faceVerifyTime);
repository.update(app);
return new RemoteApplicationResult(
applicationId, request.getCorpName(), request.getCreditCode(),
request.getLegalPerson(), request.getPhone(), applyTime,
"PASSED", "PASSED", "98.7%", faceVerifyTime,
"人脸识别已通过,等待电子签约");
}
// ==================== 步骤二:法人人脸识别与电子签约 ====================
@Transactional
public RemoteExtractResult extractSigning(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
if (app.getStatus() == null
|| app.getStatus().ordinal() < RemoteWalletApplication.Status.FACE_RECOGNIZED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤一的人脸识别和输出");
}
String signTime = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String message = "E_SIGN|" + app.getApplicationId() + "|" + app.getLegalPerson() + "|" + signTime;
app.extractSigning(message);
repository.update(app);
return new RemoteExtractResult(message);
}
@Transactional
public RemoteDigestResult computeSigningDigest(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
if (app.getSigningExtractMessage() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提取电子签约摘要原文");
}
if (app.getSigningDigestValue() != null) {
return new RemoteDigestResult(app.getSigningDigestAlgorithm(), app.getSigningDigestValue());
}
String digest = cryptography.sm3(app.getSigningExtractMessage());
app.computeSigningDigest("SM3", digest);
repository.update(app);
return new RemoteDigestResult("SM3", digest);
}
@Transactional
public RemoteSignatureResult computeSignature(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
if (app.getSigningDigestValue() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM3摘要运算");
}
if (app.getSignature() != null) {
return new RemoteSignatureResult(app.getSignPrivateKey(), "SM2", app.getSignature());
}
InstitutionSm2KeyPair keyPair = cryptography.generateSm2KeyPair();
String privateKey = keyPair.getPrivateKey();
String signature = cryptography.sign(privateKey, app.getSigningExtractMessage());
app.setSignPrivateKey(privateKey);
app.setSignature(signature);
repository.update(app);
return new RemoteSignatureResult(privateKey, "SM2", signature);
}
@Transactional
public RemoteContractResult outputContract(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
if (app.getSignature() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名计算");
}
if (app.getStatus().ordinal() >= RemoteWalletApplication.Status.SIGNED.ordinal()) {
return new RemoteContractResult(
app.getContractId(), app.getApplicationId(), app.getLegalPerson(),
"PASSED", app.getSignature(), app.getSignTime(), "SIGNED");
}
String signTime = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String contractId = "E_SIGN_" + LocalDateTime.now().format(DATE_FORMAT) + "_001";
app.sign(app.getSignPrivateKey(), app.getSignature(), contractId, signTime);
repository.update(app);
return new RemoteContractResult(
contractId, app.getApplicationId(), app.getLegalPerson(),
"PASSED", app.getSignature(), signTime, "SIGNED");
}
// ==================== 步骤三:自动审核与钱包生成 ====================
@Transactional
public RemoteExtractResult extractWallet(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
if (app.getStatus() == null
|| app.getStatus().ordinal() < RemoteWalletApplication.Status.SIGNED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成步骤二的电子签约");
}
String extractTime = app.getSignTime() != null ? app.getSignTime()
: LocalDateTime.now().format(TIMESTAMP_FORMAT);
String message = app.getCreditCode() + "|" + extractTime;
app.extractWallet(message);
repository.update(app);
return new RemoteExtractResult(message);
}
@Transactional
public RemoteDigestResult computeWalletDigest(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
if (app.getWalletExtractMessage() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提取钱包摘要原文");
}
if (app.getWalletDigestValue() != null) {
return new RemoteDigestResult(app.getWalletDigestAlgorithm(), app.getWalletDigestValue());
}
String digest = cryptography.sm3(app.getWalletExtractMessage());
app.computeWalletDigest("SM3", digest);
repository.update(app);
return new RemoteDigestResult("SM3", digest);
}
/**
*
*
* ID
*/
@Transactional
public RemoteWalletResult outputWallet(InstitutionKeySubject subject) {
RemoteWalletApplication app = findRequired(subject);
if (app.getWalletDigestValue() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成钱包SM3摘要运算");
}
if (app.getWalletId() != null) {
return new RemoteWalletResult(
app.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", app.getActivateTime(), "已验证", "远程开立");
}
// 跨渠道检查:临柜开立是否已激活同信用代码的钱包
CorporateWalletApplication corpActivated = corporateRepository
.findActivatedByCreditCode(app.getCreditCode(), subject.getUserId(),
subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (corpActivated != null) {
// 复用临柜开立的钱包ID不再生成新钱包
app.activateWallet(corpActivated.getWalletId(), corpActivated.getActivateTime());
repository.update(app);
return new RemoteWalletResult(
corpActivated.getWalletId(), app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", corpActivated.getActivateTime(), "已验证", "临柜开立");
}
String walletId = "CORP_" + app.getWalletDigestValue();
String activateTime = app.getSignTime() != null ? app.getSignTime()
: LocalDateTime.now().format(TIMESTAMP_FORMAT);
app.activateWallet(walletId, activateTime);
repository.update(app);
return new RemoteWalletResult(
walletId, app.getCorpName(), app.getCreditCode(),
app.getWalletType(), "ACTIVATED", activateTime, "已验证", "远程开立");
}
// ==================== 内部方法 ====================
private RemoteWalletApplication findOrCreate(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseGet(() -> repository.save(
RemoteWalletApplication.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId())));
}
private RemoteWalletApplication findRequired(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"远程开立申请不存在,请先填写企业信息"));
}
private String buildConcatenatedMessage(String corpName, String creditCode,
String legalPerson, String phone, String applyTime) {
return "REMOTE_OPEN|" + corpName + "|" + creditCode + "|" + legalPerson + "|"
+ phone + "|" + applyTime;
}
}

@ -1,722 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoEntity;
import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletKeyInfoMapper;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatchRepository;
import com.yau.digitalrmb.shared.api.ErrorCode;
import com.yau.digitalrmb.shared.exception.BusinessException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class SalaryBatchService {
private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final String OPERATOR_NAME = "王五";
private static final String OPERATOR_KEY_TYPE = "数币操作员私钥";
private static final String REVIEWER_NAME = "赵六";
private static final String REVIEWER_KEY_TYPE = "数币复核员私钥";
// 权限限额(元)
private static final BigDecimal SINGLE_LIMIT = new BigDecimal("50000.00");
private static final BigDecimal BATCH_LIMIT = new BigDecimal("500000.00");
private static final BigDecimal DAILY_LIMIT = new BigDecimal("1000000.00");
// 审核状态1-通过, 2-未通过
private static final int STATUS_PASSED = 1;
private static final int STATUS_FAILED = 2;
// 内置工资明细数据
private static final String[][] EMPLOYEE_DATA = {
{"张明", "WALLET_EMP_001", "技术部", "8500.00"},
{"李二", "WALLET_EMP_002", "市场部", "7200.00"},
{"王小", "WALLET_EMP_003", "财务部", "6800.00"},
{"赵六", "WALLET_EMP_004", "人事部", "5600.00"},
{"钱七", "WALLET_EMP_005", "技术部", "9200.00"}
};
@Resource
private SalaryBatchRepository repository;
@Resource
private CorporateWalletApplicationRepository corporateWalletRepository;
@Resource
private RemoteWalletApplicationRepository remoteWalletRepository;
@Resource
private CorporateWalletKeyInfoMapper keyInfoMapper;
@Resource
private InstitutionIdentityCryptography cryptography;
@Resource
private SalaryScoreService scoreService;
@Resource
private InstitutionKeyService institutionKeyService;
// ==================== 步骤一:调取区块信息 ====================
@Transactional
public SalaryBlockchainResult fetchBlockchain(InstitutionKeySubject subject, String operator) {
SalaryBatch batch = findOrCreate(subject);
// 付款钱包从步骤一(对公钱包开立)获取
String corpWalletId = findCorporateWalletId(subject);
if (corpWalletId == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成对公钱包开立(步骤一)");
}
// 批次号随机生成
String batchId = generateBatchId();
// 时间戳
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
batch.fetchBlockchain(batchId, corpWalletId, OPERATOR_NAME, timestamp);
repository.update(batch);
return new SalaryBlockchainResult(corpWalletId, batchId, OPERATOR_NAME, timestamp);
}
// ==================== 步骤二:拼接工资发放申请原文 ====================
@Transactional
public SalaryConcatenateResult concatenate(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.BLOCKCHAIN_FETCHED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先调取区块信息");
}
String message = buildConcatenatedMessage(batch);
batch.concatenate(message);
repository.update(batch);
return new SalaryConcatenateResult(message);
}
// ==================== 步骤三:生成摘要 ====================
@Transactional
public SalaryDigestResult computeDigest(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONCATENATED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先拼接工资发放申请原文");
}
if (batch.getDigestValue() != null) {
return new SalaryDigestResult(batch.getDigestValue());
}
String digest = cryptography.sm3(batch.getConcatenatedMessage());
batch.computeDigest("SM3", digest);
repository.update(batch);
return new SalaryDigestResult(digest);
}
// ==================== 步骤四SM2签名 ====================
@Transactional
public SalarySignatureResult sign(InstitutionKeySubject subject, String providedPrivateKey) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.DIGESTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成摘要");
}
if (batch.getSignature() != null) {
return new SalarySignatureResult(batch.getSignature());
}
// 从 corporate_wallet_key_info 表获取数币操作员私钥
String storedPrivateKey = findOperatorPrivateKey(subject);
if (storedPrivateKey == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "数币操作员私钥不存在,请先获取密钥");
}
// 验证传入的私钥是否正确
if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey != null ? providedPrivateKey.trim() : "")) {
// 错误数加1
scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数币操作员私钥不正确,错误次数+1");
}
// 使用数币操作员私钥对摘要进行SM2签名
String signature = cryptography.sign(storedPrivateKey, batch.getConcatenatedMessage());
batch.sign(signature);
repository.update(batch);
return new SalarySignatureResult(signature);
}
// ==================== 步骤五:输出 ====================
@Transactional
public SalaryOutputResult output(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.SIGNED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名");
}
if (batch.getOutputJson() != null) {
return new SalaryOutputResult(batch.getOutputJson());
}
String json = buildOutputJson(batch);
batch.output(json);
repository.update(batch);
return new SalaryOutputResult(json);
}
// ==================== 步骤六:发送 ====================
@Transactional
public SalarySendResult send(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.OUTPUT.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成输出");
}
if (batch.getStatus() == SalaryBatch.Status.SENT) {
return new SalarySendResult("待审核", batch.getSubmitTime());
}
String submitTime = LocalDateTime.now().format(TIMESTAMP_FORMAT);
batch.send(submitTime);
repository.update(batch);
return new SalarySendResult("待审核", submitTime);
}
// ==================== 步骤七:提取审核原文 ====================
@Transactional
public SalaryReviewExtractResult extractReviewText(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.SENT.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成数币操作员发送步骤");
}
if (batch.getReviewConcatenatedMessage() != null) {
return new SalaryReviewExtractResult(batch.getReviewConcatenatedMessage(),
batch.getBatchId(), batch.getReviewerName(),
calculateTotalAmount(), EMPLOYEE_DATA.length, batch.getReviewTimestamp());
}
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String totalAmount = calculateTotalAmount();
int employeeCount = EMPLOYEE_DATA.length;
// 拼接审核原文REVIEW|批次号|复核员|总金额|人数|时间戳
String reviewMessage = "REVIEW|" + batch.getBatchId() + "|" + REVIEWER_NAME
+ "|" + totalAmount + "|" + employeeCount + "|" + timestamp;
batch.extractReviewText(REVIEWER_NAME, timestamp, reviewMessage);
repository.update(batch);
return new SalaryReviewExtractResult(reviewMessage, batch.getBatchId(),
REVIEWER_NAME, totalAmount, employeeCount, timestamp);
}
// ==================== 步骤八:审核与权限校验 ====================
@Transactional
public SalaryReviewCheckResult checkPermission(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_EXTRACTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提取审核原文");
}
if (batch.getSingleLimitStatus() != null) {
// 已校验过,直接返回
return buildCheckResult(batch);
}
// 计算单笔最高金额
BigDecimal maxSingleAmount = BigDecimal.ZERO;
for (String[] emp : EMPLOYEE_DATA) {
BigDecimal amount = new BigDecimal(emp[3]);
if (amount.compareTo(maxSingleAmount) > 0) {
maxSingleAmount = amount;
}
}
// 批次总金额
BigDecimal batchTotal = new BigDecimal(calculateTotalAmount());
// 当日累计发放金额(查询当天所有已发送批次的总金额)
BigDecimal dailyTotal = calculateDailyTotal(subject, batchTotal);
// 单笔限额校验
int singleStatus = maxSingleAmount.compareTo(SINGLE_LIMIT) <= 0 ? STATUS_PASSED : STATUS_FAILED;
String singleDesc = "单笔最高金额 " + maxSingleAmount.setScale(2).toPlainString() + " 元 "
+ (singleStatus == STATUS_PASSED ? "≤" : ">") + " " + SINGLE_LIMIT.setScale(2).toPlainString() + " 元";
// 批次限额校验
int batchStatus = batchTotal.compareTo(BATCH_LIMIT) <= 0 ? STATUS_PASSED : STATUS_FAILED;
String batchDesc = "批次总金额 " + batchTotal.setScale(2).toPlainString() + " 元 "
+ (batchStatus == STATUS_PASSED ? "≤" : ">") + " " + BATCH_LIMIT.setScale(2).toPlainString() + " 元";
// 日累计限额校验
int dailyStatus = dailyTotal.compareTo(DAILY_LIMIT) <= 0 ? STATUS_PASSED : STATUS_FAILED;
String dailyDesc = "当日累计发放金额 " + dailyTotal.setScale(2).toPlainString() + " 元 "
+ (dailyStatus == STATUS_PASSED ? "≤" : ">") + " " + DAILY_LIMIT.setScale(2).toPlainString() + " 元";
// 审核结果:全部通过才批准
String reviewResult = (singleStatus == STATUS_PASSED && batchStatus == STATUS_PASSED && dailyStatus == STATUS_PASSED)
? "APPROVED" : "REJECTED";
batch.checkPermission(singleStatus, batchStatus, dailyStatus, reviewResult);
repository.update(batch);
return new SalaryReviewCheckResult(
singleStatus, SINGLE_LIMIT.setScale(2).toPlainString(), maxSingleAmount.setScale(2).toPlainString(), singleDesc,
batchStatus, BATCH_LIMIT.setScale(2).toPlainString(), batchTotal.setScale(2).toPlainString(), batchDesc,
dailyStatus, DAILY_LIMIT.setScale(2).toPlainString(), dailyTotal.setScale(2).toPlainString(), dailyDesc,
reviewResult);
}
// ==================== 步骤九:生成审核摘要 ====================
@Transactional
public SalaryReviewDigestResult computeReviewDigest(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_CHECKED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成审核与权限校验");
}
if (batch.getReviewDigestValue() != null) {
return new SalaryReviewDigestResult(batch.getReviewDigestValue());
}
String digest = cryptography.sm3(batch.getReviewConcatenatedMessage());
batch.computeReviewDigest("SM3", digest);
repository.update(batch);
return new SalaryReviewDigestResult(digest);
}
// ==================== 步骤十SM2签名复核员私钥 ====================
@Transactional
public SalaryReviewSignatureResult signReview(InstitutionKeySubject subject, String providedPrivateKey) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_DIGESTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成审核摘要");
}
if (batch.getReviewSignature() != null) {
return new SalaryReviewSignatureResult(batch.getReviewSignature());
}
// 从 corporate_wallet_key_info 表获取数币复核员私钥
String storedPrivateKey = findReviewerPrivateKey(subject);
if (storedPrivateKey == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "数币复核员私钥不存在,请先获取密钥");
}
// 验证传入的私钥是否正确
if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey != null ? providedPrivateKey.trim() : "")) {
scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数币复核员私钥不正确,错误次数+1");
}
// 使用数币复核员私钥对审核摘要原文进行SM2签名
String signature = cryptography.sign(storedPrivateKey, batch.getReviewConcatenatedMessage());
batch.signReview(signature);
repository.update(batch);
return new SalaryReviewSignatureResult(signature);
}
// ==================== 步骤十一:输出审核结果 ====================
@Transactional
public SalaryReviewOutputResult outputReview(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEW_SIGNED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM2签名");
}
if (batch.getReviewOutputJson() != null) {
return new SalaryReviewOutputResult(batch.getReviewOutputJson());
}
String json = buildReviewOutputJson(batch);
batch.outputReview(json);
repository.update(batch);
return new SalaryReviewOutputResult(json);
}
@Transactional(readOnly = true)
public SalaryPageResult pageQuery(InstitutionKeySubject subject) {
// 始终返回内置工资明细
List<SalaryPageResult.EmployeeDetail> payroll = new ArrayList<>();
for (int i = 0; i < EMPLOYEE_DATA.length; i++) {
payroll.add(new SalaryPageResult.EmployeeDetail(
i + 1, EMPLOYEE_DATA[i][0], EMPLOYEE_DATA[i][1],
EMPLOYEE_DATA[i][2], EMPLOYEE_DATA[i][3]));
}
String totalAmount = calculateTotalAmount();
int employeeCount = EMPLOYEE_DATA.length;
// 查询已有批次数据,若不存在则返回空字段(不报错)
SalaryBatch batch = repository
.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (batch == null) {
return new SalaryPageResult(payroll, totalAmount, employeeCount,
null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null,
null, null, null, null, null);
}
return new SalaryPageResult(payroll, totalAmount, employeeCount,
batch.getBatchId(), batch.getCorpWalletId(), batch.getOperatorName(),
batch.getOperatorTimestamp(), batch.getConcatenatedMessage(),
batch.getDigestValue(), batch.getSignature(), batch.getOutputJson(),
batch.getStatus() != null ? batch.getStatus().name() : null,
batch.getReviewConcatenatedMessage(), batch.getReviewerName(),
batch.getSingleLimitStatus(), batch.getBatchLimitStatus(), batch.getDailyLimitStatus(),
batch.getReviewResult(), batch.getReviewDigestValue(),
batch.getReviewSignature(), batch.getReviewOutputJson(),
batch.getConfirmMessage(), batch.getConfirmTimestamp(),
batch.getConfirmDigestValue(), batch.getCbSignature(),
batch.getConfirmOutputJson());
}
// ==================== 步骤十二:提取交易确认原文 ====================
@Transactional
public SalaryConfirmExtractResult extractConfirmText(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.REVIEWED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成复核员审核步骤");
}
if (batch.getConfirmMessage() != null) {
return new SalaryConfirmExtractResult(batch.getConfirmMessage(),
batch.getBatchId(), calculateTotalAmount(), EMPLOYEE_DATA.length,
batch.getConfirmTimestamp());
}
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMAT);
String totalAmount = calculateTotalAmount();
int employeeCount = EMPLOYEE_DATA.length;
// 拼接确认原文CONFIRM|批次号|总金额|人数|时间戳
String confirmMessage = "CONFIRM|" + batch.getBatchId() + "|"
+ totalAmount + "|" + employeeCount + "|" + timestamp;
batch.extractConfirmText(timestamp, confirmMessage);
repository.update(batch);
return new SalaryConfirmExtractResult(confirmMessage, batch.getBatchId(),
totalAmount, employeeCount, timestamp);
}
// ==================== 步骤十三:生成交易确认摘要 ====================
@Transactional
public SalaryConfirmDigestResult computeConfirmDigest(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONFIRM_EXTRACTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先提取交易确认原文");
}
if (batch.getConfirmDigestValue() != null) {
return new SalaryConfirmDigestResult(batch.getConfirmDigestValue());
}
String digest = cryptography.sm3(batch.getConfirmMessage());
batch.computeConfirmDigest("SM3", digest);
repository.update(batch);
return new SalaryConfirmDigestResult(digest);
}
// ==================== 步骤十四:央行第一私钥签名 ====================
@Transactional
public SalaryConfirmSignatureResult signConfirm(InstitutionKeySubject subject, String providedPrivateKey) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONFIRM_DIGESTED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先生成交易确认摘要");
}
if (batch.getCbSignature() != null) {
return new SalaryConfirmSignatureResult(batch.getCbSignature());
}
// 从 institution_key_service 获取央行第一私钥
String storedPrivateKey = institutionKeyService.centralBankFirstPrivateKeyOrNull(subject);
if (storedPrivateKey == null) {
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "央行第一私钥不存在,请先生成央行密钥对");
}
// 验证传入的私钥是否正确
if (!storedPrivateKey.equalsIgnoreCase(providedPrivateKey != null ? providedPrivateKey.trim() : "")) {
scoreService.recordError(subject.getUserId());
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "央行第一私钥不正确,错误次数+1");
}
// 使用央行第一私钥对确认原文进行SM2签名并添加CB_FINAL_前缀
String signature = cryptography.sign(storedPrivateKey, batch.getConfirmMessage());
String cbSignature = "CB_FINAL_A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0" + signature;
batch.signConfirm(cbSignature);
repository.update(batch);
return new SalaryConfirmSignatureResult(cbSignature);
}
// ==================== 步骤十五:输出交易确认结果 ====================
@Transactional
public SalaryConfirmOutputResult outputConfirm(InstitutionKeySubject subject) {
SalaryBatch batch = findRequired(subject);
if (batch.getStatus().ordinal() < SalaryBatch.Status.CONFIRM_SIGNED.ordinal()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成央行签名");
}
if (batch.getConfirmOutputJson() != null) {
return new SalaryConfirmOutputResult(batch.getConfirmOutputJson());
}
String json = buildConfirmOutputJson(batch);
batch.outputConfirm(json);
repository.update(batch);
return new SalaryConfirmOutputResult(json);
}
// ==================== 内部方法 ====================
private SalaryBatch findOrCreate(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseGet(() -> repository.save(
SalaryBatch.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId())));
}
private SalaryBatch findRequired(InstitutionKeySubject subject) {
return repository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"工资批次不存在,请先调取区块信息"));
}
/**
* /ID
*/
private String findCorporateWalletId(InstitutionKeySubject subject) {
// 先查临柜开立
CorporateWalletApplication corpApp = corporateWalletRepository
.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (corpApp != null && corpApp.getWalletId() != null && !corpApp.getWalletId().isEmpty()) {
return corpApp.getWalletId();
}
// 再查远程开立
RemoteWalletApplication remoteApp = remoteWalletRepository
.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
if (remoteApp != null && remoteApp.getWalletId() != null && !remoteApp.getWalletId().isEmpty()) {
return remoteApp.getWalletId();
}
return null;
}
/**
* corporate_wallet_key_info
*/
private String findOperatorPrivateKey(InstitutionKeySubject subject) {
CorporateWalletKeyInfoEntity entity = keyInfoMapper.selectOne(
new LambdaQueryWrapper<CorporateWalletKeyInfoEntity>()
.eq(CorporateWalletKeyInfoEntity::getUserId, subject.getUserId())
.eq(CorporateWalletKeyInfoEntity::getSchoolId, subject.getSchoolId())
.eq(CorporateWalletKeyInfoEntity::getClassId, subject.getClassId())
.eq(CorporateWalletKeyInfoEntity::getKeyType, OPERATOR_KEY_TYPE)
.eq(CorporateWalletKeyInfoEntity::getDeleted, false)
.orderByDesc(CorporateWalletKeyInfoEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? null : entity.getKeyValue();
}
/**
* corporate_wallet_key_info
*/
private String findReviewerPrivateKey(InstitutionKeySubject subject) {
CorporateWalletKeyInfoEntity entity = keyInfoMapper.selectOne(
new LambdaQueryWrapper<CorporateWalletKeyInfoEntity>()
.eq(CorporateWalletKeyInfoEntity::getUserId, subject.getUserId())
.eq(CorporateWalletKeyInfoEntity::getSchoolId, subject.getSchoolId())
.eq(CorporateWalletKeyInfoEntity::getClassId, subject.getClassId())
.eq(CorporateWalletKeyInfoEntity::getKeyType, REVIEWER_KEY_TYPE)
.eq(CorporateWalletKeyInfoEntity::getDeleted, false)
.orderByDesc(CorporateWalletKeyInfoEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? null : entity.getKeyValue();
}
/**
* +
*/
private BigDecimal calculateDailyTotal(InstitutionKeySubject subject, BigDecimal currentBatchTotal) {
// 简化处理:当前批次金额即为当日累计(教学仿真场景)
// 如需精确统计,可扩展 repository 添加按日期查询方法
return currentBatchTotal;
}
/**
*
*/
private SalaryReviewCheckResult buildCheckResult(SalaryBatch batch) {
BigDecimal maxSingleAmount = BigDecimal.ZERO;
for (String[] emp : EMPLOYEE_DATA) {
BigDecimal amount = new BigDecimal(emp[3]);
if (amount.compareTo(maxSingleAmount) > 0) {
maxSingleAmount = amount;
}
}
BigDecimal batchTotal = new BigDecimal(calculateTotalAmount());
BigDecimal dailyTotal = calculateDailyTotal(
new InstitutionKeySubject(batch.getUserId(), batch.getSchoolId(), batch.getClassId()), batchTotal);
String singleDesc = "单笔最高金额 " + maxSingleAmount.setScale(2).toPlainString() + " 元 "
+ (batch.getSingleLimitStatus() == STATUS_PASSED ? "≤" : ">")
+ " " + SINGLE_LIMIT.setScale(2).toPlainString() + " 元";
String batchDesc = "批次总金额 " + batchTotal.setScale(2).toPlainString() + " 元 "
+ (batch.getBatchLimitStatus() == STATUS_PASSED ? "≤" : ">")
+ " " + BATCH_LIMIT.setScale(2).toPlainString() + " 元";
String dailyDesc = "当日累计发放金额 " + dailyTotal.setScale(2).toPlainString() + " 元 "
+ (batch.getDailyLimitStatus() == STATUS_PASSED ? "≤" : ">")
+ " " + DAILY_LIMIT.setScale(2).toPlainString() + " 元";
return new SalaryReviewCheckResult(
batch.getSingleLimitStatus(), SINGLE_LIMIT.setScale(2).toPlainString(),
maxSingleAmount.setScale(2).toPlainString(), singleDesc,
batch.getBatchLimitStatus(), BATCH_LIMIT.setScale(2).toPlainString(),
batchTotal.setScale(2).toPlainString(), batchDesc,
batch.getDailyLimitStatus(), DAILY_LIMIT.setScale(2).toPlainString(),
dailyTotal.setScale(2).toPlainString(), dailyDesc,
batch.getReviewResult());
}
/**
* JSON
*/
private String buildReviewOutputJson(SalaryBatch batch) {
BigDecimal maxSingleAmount = BigDecimal.ZERO;
for (String[] emp : EMPLOYEE_DATA) {
BigDecimal amount = new BigDecimal(emp[3]);
if (amount.compareTo(maxSingleAmount) > 0) {
maxSingleAmount = amount;
}
}
BigDecimal batchTotal = new BigDecimal(calculateTotalAmount());
BigDecimal dailyTotal = calculateDailyTotal(
new InstitutionKeySubject(batch.getUserId(), batch.getSchoolId(), batch.getClassId()), batchTotal);
String singleResult = batch.getSingleLimitStatus() == STATUS_PASSED ? "PASSED" : "FAILED";
String batchResult = batch.getBatchLimitStatus() == STATUS_PASSED ? "PASSED" : "FAILED";
String dailyResult = batch.getDailyLimitStatus() == STATUS_PASSED ? "PASSED" : "FAILED";
String statusText = "APPROVED".equals(batch.getReviewResult()) ? "已批准" : "已拒绝";
StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append(" \"batchId\": \"").append(batch.getBatchId()).append("\",\n");
sb.append(" \"reviewer\": \"").append(batch.getReviewerName()).append("\",\n");
sb.append(" \"reviewResult\": \"").append(batch.getReviewResult()).append("\",\n");
sb.append(" \"permissionCheck\": {\n");
sb.append(" \"singleLimit\": {\n");
sb.append(" \"limit\": \"").append(SINGLE_LIMIT.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"actual\": \"").append(maxSingleAmount.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"result\": \"").append(singleResult).append("\"\n");
sb.append(" },\n");
sb.append(" \"batchLimit\": {\n");
sb.append(" \"limit\": \"").append(BATCH_LIMIT.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"actual\": \"").append(batchTotal.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"result\": \"").append(batchResult).append("\"\n");
sb.append(" },\n");
sb.append(" \"dailyLimit\": {\n");
sb.append(" \"limit\": \"").append(DAILY_LIMIT.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"actual\": \"").append(dailyTotal.setScale(2).toPlainString()).append("\",\n");
sb.append(" \"result\": \"").append(dailyResult).append("\"\n");
sb.append(" }\n");
sb.append(" },\n");
sb.append(" \"reviewerSignature\": \"").append(batch.getReviewSignature()).append("\",\n");
sb.append(" \"reviewTime\": \"").append(batch.getReviewTimestamp()).append("\",\n");
sb.append(" \"status\": \"").append(statusText).append("\"\n");
sb.append("}");
return sb.toString();
}
private String generateBatchId() {
String datePart = LocalDateTime.now().format(DATE_FORMAT);
int seq = ThreadLocalRandom.current().nextInt(100, 1000);
return "BATCH_" + datePart + "_" + String.format("%03d", seq);
}
private String buildConcatenatedMessage(SalaryBatch batch) {
StringBuilder sb = new StringBuilder();
sb.append(batch.getBatchId());
sb.append("|").append(batch.getCorpWalletId());
sb.append("|").append(batch.getOperatorName());
for (String[] emp : EMPLOYEE_DATA) {
sb.append("|").append(emp[0]); // 姓名
sb.append("|").append(emp[1]); // 钱包ID
sb.append("|").append(emp[3]); // 金额
}
sb.append("|").append(batch.getOperatorTimestamp());
return sb.toString();
}
private String buildOutputJson(SalaryBatch batch) {
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("\"batchId\":\"").append(batch.getBatchId()).append("\",");
sb.append("\"corpWalletId\":\"").append(batch.getCorpWalletId()).append("\",");
sb.append("\"operator\":\"").append(batch.getOperatorName()).append("\",");
sb.append("\"totalAmount\":\"").append(calculateTotalAmount()).append("\",");
sb.append("\"employeeCount\":").append(EMPLOYEE_DATA.length).append(",");
sb.append("\"payroll\":[");
for (int i = 0; i < EMPLOYEE_DATA.length; i++) {
if (i > 0) sb.append(",");
sb.append("{\"name\":\"").append(EMPLOYEE_DATA[i][0]).append("\",");
sb.append("\"walletId\":\"").append(EMPLOYEE_DATA[i][1]).append("\",");
sb.append("\"amount\":\"").append(EMPLOYEE_DATA[i][3]).append("\"}");
}
sb.append("],");
sb.append("\"digest\":\"").append(batch.getDigestValue()).append("\",");
sb.append("\"operatorSignature\":\"").append(batch.getSignature()).append("\",");
sb.append("\"status\":\"待审核\",");
sb.append("\"submitTime\":\"").append(batch.getOperatorTimestamp()).append("\"");
sb.append("}");
return sb.toString();
}
private String calculateTotalAmount() {
double total = 0;
for (String[] emp : EMPLOYEE_DATA) {
total += Double.parseDouble(emp[3]);
}
return String.format("%.2f", total);
}
/**
* JSON
*/
private String buildConfirmOutputJson(SalaryBatch batch) {
StringBuilder sb = new StringBuilder();
sb.append("{\n");
sb.append(" \"batchId\": \"").append(batch.getBatchId()).append("\",\n");
sb.append(" \"totalAmount\": \"").append(calculateTotalAmount()).append("\",\n");
sb.append(" \"employeeCount\": ").append(EMPLOYEE_DATA.length).append(",\n");
sb.append(" \"digest\": \"").append(batch.getConfirmDigestValue()).append("\",\n");
sb.append(" \"cbSignature\": \"").append(batch.getCbSignature()).append("\",\n");
sb.append(" \"confirmTime\": \"").append(batch.getConfirmTimestamp()).append("\",\n");
sb.append(" \"status\": \"COMPLETED\"\n");
sb.append("}");
return sb.toString();
}
}

@ -1,24 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "调取区块信息结果")
public class SalaryBlockchainResult {
@Schema(description = "付款钱包ID从步骤一对公钱包开立获取")
private final String corpWalletId;
@Schema(description = "批次号(随机生成)")
private final String batchId;
@Schema(description = "操作员(数币操作员)")
private final String operatorName;
@Schema(description = "时间戳")
private final String timestamp;
public SalaryBlockchainResult(String corpWalletId, String batchId, String operatorName, String timestamp) {
this.corpWalletId = corpWalletId;
this.batchId = batchId;
this.operatorName = operatorName;
this.timestamp = timestamp;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "拼接工资发放申请原文结果")
public class SalaryConcatenateResult {
@Schema(description = "拼接原文(管道分隔)")
private final String concatenatedMessage;
public SalaryConcatenateResult(String concatenatedMessage) {
this.concatenatedMessage = concatenatedMessage;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "交易确认SM3摘要结果")
public class SalaryConfirmDigestResult {
@Schema(description = "摘要值(十六进制大写)")
private final String digestValue;
public SalaryConfirmDigestResult(String digestValue) {
this.digestValue = digestValue;
}
}

@ -1,28 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "提取交易确认原文结果")
public class SalaryConfirmExtractResult {
@Schema(description = "确认原文管道分隔格式CONFIRM|批次号|总金额|人数|时间戳")
private final String confirmMessage;
@Schema(description = "批次号")
private final String batchId;
@Schema(description = "工资金额汇总(元)")
private final String totalAmount;
@Schema(description = "员工人数")
private final int employeeCount;
@Schema(description = "确认时间戳")
private final String timestamp;
public SalaryConfirmExtractResult(String confirmMessage, String batchId,
String totalAmount, int employeeCount, String timestamp) {
this.confirmMessage = confirmMessage;
this.batchId = batchId;
this.totalAmount = totalAmount;
this.employeeCount = employeeCount;
this.timestamp = timestamp;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "交易确认输出JSON结果")
public class SalaryConfirmOutputResult {
@Schema(description = "输出JSON报文")
private final String outputJson;
public SalaryConfirmOutputResult(String outputJson) {
this.outputJson = outputJson;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "交易确认央行签名结果")
public class SalaryConfirmSignatureResult {
@Schema(description = "央行签名值CB_FINAL_前缀+十六进制)")
private final String cbSignature;
public SalaryConfirmSignatureResult(String cbSignature) {
this.cbSignature = cbSignature;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "SM3摘要结果只返回摘要值")
public class SalaryDigestResult {
@Schema(description = "摘要值")
private final String digestValue;
public SalaryDigestResult(String digestValue) {
this.digestValue = digestValue;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "输出JSON结果")
public class SalaryOutputResult {
@Schema(description = "输出JSON报文")
private final String outputJson;
public SalaryOutputResult(String outputJson) {
this.outputJson = outputJson;
}
}

@ -1,124 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import java.util.List;
@Getter
@Schema(description = "工资明细页面查询结果")
public class SalaryPageResult {
@Schema(description = "工资明细列表")
private final List<EmployeeDetail> payroll;
@Schema(description = "合计金额")
private final String totalAmount;
@Schema(description = "员工数量")
private final int employeeCount;
@Schema(description = "批次号(若已调取区块信息)")
private final String batchId;
@Schema(description = "付款钱包ID若已调取区块信息")
private final String corpWalletId;
@Schema(description = "操作员(若已调取区块信息)")
private final String operatorName;
@Schema(description = "时间戳(若已调取区块信息)")
private final String timestamp;
@Schema(description = "拼接原文(若已拼接)")
private final String concatenatedMessage;
@Schema(description = "摘要值(若已生成摘要)")
private final String digestValue;
@Schema(description = "签名值(若已签名)")
private final String signature;
@Schema(description = "输出JSON若已输出")
private final String outputJson;
@Schema(description = "状态(若已发送)")
private final String status;
@Schema(description = "审核原文(若已提取)")
private final String reviewMessage;
@Schema(description = "复核员姓名(若已提取)")
private final String reviewerName;
@Schema(description = "单笔限额状态1-通过, 2-未通过(若已校验)")
private final Integer singleLimitStatus;
@Schema(description = "批次限额状态1-通过, 2-未通过(若已校验)")
private final Integer batchLimitStatus;
@Schema(description = "日累计限额状态1-通过, 2-未通过(若已校验)")
private final Integer dailyLimitStatus;
@Schema(description = "审核结果APPROVED-已批准, REJECTED-已拒绝(若已校验)")
private final String reviewResult;
@Schema(description = "审核摘要值(若已生成)")
private final String reviewDigestValue;
@Schema(description = "审核签名值(若已签名)")
private final String reviewSignature;
@Schema(description = "审核输出JSON若已输出")
private final String reviewOutputJson;
@Schema(description = "交易确认原文(若已提取)")
private final String confirmMessage;
@Schema(description = "确认时间戳(若已提取)")
private final String confirmTimestamp;
@Schema(description = "交易确认摘要值(若已生成)")
private final String confirmDigestValue;
@Schema(description = "央行签名值(若已签名)")
private final String cbSignature;
@Schema(description = "交易确认输出JSON若已输出")
private final String confirmOutputJson;
public SalaryPageResult(List<EmployeeDetail> payroll, String totalAmount, int employeeCount,
String batchId, String corpWalletId, String operatorName, String timestamp,
String concatenatedMessage, String digestValue, String signature,
String outputJson, String status,
String reviewMessage, String reviewerName,
Integer singleLimitStatus, Integer batchLimitStatus, Integer dailyLimitStatus,
String reviewResult, String reviewDigestValue, String reviewSignature,
String reviewOutputJson,
String confirmMessage, String confirmTimestamp,
String confirmDigestValue, String cbSignature, String confirmOutputJson) {
this.payroll = payroll;
this.totalAmount = totalAmount;
this.employeeCount = employeeCount;
this.batchId = batchId;
this.corpWalletId = corpWalletId;
this.operatorName = operatorName;
this.timestamp = timestamp;
this.concatenatedMessage = concatenatedMessage;
this.digestValue = digestValue;
this.signature = signature;
this.outputJson = outputJson;
this.status = status;
this.reviewMessage = reviewMessage;
this.reviewerName = reviewerName;
this.singleLimitStatus = singleLimitStatus;
this.batchLimitStatus = batchLimitStatus;
this.dailyLimitStatus = dailyLimitStatus;
this.reviewResult = reviewResult;
this.reviewDigestValue = reviewDigestValue;
this.reviewSignature = reviewSignature;
this.reviewOutputJson = reviewOutputJson;
this.confirmMessage = confirmMessage;
this.confirmTimestamp = confirmTimestamp;
this.confirmDigestValue = confirmDigestValue;
this.cbSignature = cbSignature;
this.confirmOutputJson = confirmOutputJson;
}
@Getter
@Schema(description = "员工工资明细")
public static class EmployeeDetail {
@Schema(description = "序号")
private final int seqNo;
@Schema(description = "姓名")
private final String name;
@Schema(description = "钱包ID")
private final String walletId;
@Schema(description = "部门")
private final String department;
@Schema(description = "工资金额(元)")
private final String amount;
public EmployeeDetail(int seqNo, String name, String walletId, String department, String amount) {
this.seqNo = seqNo;
this.name = name;
this.walletId = walletId;
this.department = department;
this.amount = amount;
}
}
}

@ -1,57 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "审核与权限校验结果")
public class SalaryReviewCheckResult {
@Schema(description = "单笔限额校验1-通过, 2-未通过")
private final int singleLimitStatus;
@Schema(description = "单笔限额上限(元)")
private final String singleLimit;
@Schema(description = "单笔最高金额(元)")
private final String singleActual;
@Schema(description = "单笔限额说明")
private final String singleLimitDesc;
@Schema(description = "批次限额校验1-通过, 2-未通过")
private final int batchLimitStatus;
@Schema(description = "批次限额上限(元)")
private final String batchLimit;
@Schema(description = "批次总金额(元)")
private final String batchActual;
@Schema(description = "批次限额说明")
private final String batchLimitDesc;
@Schema(description = "日累计限额校验1-通过, 2-未通过")
private final int dailyLimitStatus;
@Schema(description = "日累计限额上限(元)")
private final String dailyLimit;
@Schema(description = "当日累计发放金额(元)")
private final String dailyActual;
@Schema(description = "日累计限额说明")
private final String dailyLimitDesc;
@Schema(description = "审核结果APPROVED-已批准, REJECTED-已拒绝")
private final String reviewResult;
public SalaryReviewCheckResult(int singleLimitStatus, String singleLimit, String singleActual, String singleLimitDesc,
int batchLimitStatus, String batchLimit, String batchActual, String batchLimitDesc,
int dailyLimitStatus, String dailyLimit, String dailyActual, String dailyLimitDesc,
String reviewResult) {
this.singleLimitStatus = singleLimitStatus;
this.singleLimit = singleLimit;
this.singleActual = singleActual;
this.singleLimitDesc = singleLimitDesc;
this.batchLimitStatus = batchLimitStatus;
this.batchLimit = batchLimit;
this.batchActual = batchActual;
this.batchLimitDesc = batchLimitDesc;
this.dailyLimitStatus = dailyLimitStatus;
this.dailyLimit = dailyLimit;
this.dailyActual = dailyActual;
this.dailyLimitDesc = dailyLimitDesc;
this.reviewResult = reviewResult;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "审核SM3摘要结果只返回摘要值")
public class SalaryReviewDigestResult {
@Schema(description = "摘要值(十六进制大写)")
private final String digestValue;
public SalaryReviewDigestResult(String digestValue) {
this.digestValue = digestValue;
}
}

@ -1,31 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "提取审核原文结果")
public class SalaryReviewExtractResult {
@Schema(description = "审核原文管道分隔格式REVIEW|批次号|复核员|总金额|人数|时间戳")
private final String reviewMessage;
@Schema(description = "批次号")
private final String batchId;
@Schema(description = "复核员姓名")
private final String reviewer;
@Schema(description = "工资金额汇总(元)")
private final String totalAmount;
@Schema(description = "员工人数")
private final int employeeCount;
@Schema(description = "审核时间戳")
private final String timestamp;
public SalaryReviewExtractResult(String reviewMessage, String batchId, String reviewer,
String totalAmount, int employeeCount, String timestamp) {
this.reviewMessage = reviewMessage;
this.batchId = batchId;
this.reviewer = reviewer;
this.totalAmount = totalAmount;
this.employeeCount = employeeCount;
this.timestamp = timestamp;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "审核输出JSON结果")
public class SalaryReviewOutputResult {
@Schema(description = "输出JSON报文")
private final String outputJson;
public SalaryReviewOutputResult(String outputJson) {
this.outputJson = outputJson;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "审核SM2签名结果只返回签名值")
public class SalaryReviewSignatureResult {
@Schema(description = "签名值(十六进制)")
private final String signature;
public SalaryReviewSignatureResult(String signature) {
this.signature = signature;
}
}

@ -1,94 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.UUID;
/**
*
*
*/
@Service
public class SalaryScoreService {
public static final String MODULE_NAME = "企业发放数字人民币工资";
private static final int MODULE_SERIAL_NUMBER = 6;
private static final double TOTAL_SCORE = 100.0;
@Resource
private StuModuleScoreDetailsMapper scoreDetailsMapper;
@Value("${salary.score.wrong-deduct:0.50}")
private BigDecimal wrongDeduct = new BigDecimal("0.50");
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int recordError(String userId) {
if (userId == null || userId.trim().isEmpty()) {
return 0;
}
StuModuleScoreDetailsEntity detail = getOrCreate(userId);
int accumulatedErrorCount = errorCount(detail.getCompletionStatus()) + 1;
detail.setCompletionStatus(String.valueOf(accumulatedErrorCount));
detail.setScoreProject(score(accumulatedErrorCount, progress(detail.getSchedule())));
scoreDetailsMapper.updateById(detail);
return accumulatedErrorCount;
}
private StuModuleScoreDetailsEntity getOrCreate(String userId) {
StuModuleScoreDetailsEntity existing = scoreDetailsMapper.selectOne(
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
.eq(StuModuleScoreDetailsEntity::getUserId, userId)
.eq(StuModuleScoreDetailsEntity::getMoudule, MODULE_NAME)
.eq(StuModuleScoreDetailsEntity::getSerialNumber, MODULE_SERIAL_NUMBER)
.last("LIMIT 1"));
if (existing != null) {
return existing;
}
StuModuleScoreDetailsEntity created = new StuModuleScoreDetailsEntity();
created.setId(UUID.randomUUID().toString());
created.setMoudule(MODULE_NAME);
created.setLearningProjects("实验实训");
created.setAssessmentItems("工资发放流程错误次数");
created.setScoringCriteria(wrongDeduct.stripTrailingZeros().toPlainString());
created.setCompletionStatus("0");
created.setScoreProject(0.0);
created.setUserId(userId);
created.setTotalScore(TOTAL_SCORE);
created.setSerialNumber(MODULE_SERIAL_NUMBER);
created.setSchedule(0.0);
scoreDetailsMapper.insert(created);
return created;
}
private double score(int errorCount, double schedule) {
BigDecimal base = BigDecimal.valueOf(TOTAL_SCORE)
.subtract(wrongDeduct.multiply(BigDecimal.valueOf(Math.max(0, errorCount))))
.max(BigDecimal.ZERO);
return base.multiply(BigDecimal.valueOf(schedule))
.divide(BigDecimal.valueOf(TOTAL_SCORE), 2, RoundingMode.HALF_UP)
.doubleValue();
}
private double progress(Double schedule) {
return schedule == null ? 0.0 : schedule;
}
private int errorCount(String completionStatus) {
if (completionStatus == null || completionStatus.trim().isEmpty()) {
return 0;
}
try {
return Math.max(0, Integer.parseInt(completionStatus));
} catch (NumberFormatException exception) {
return 0;
}
}
}

@ -1,18 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "发送结果")
public class SalarySendResult {
@Schema(description = "状态:待审核")
private final String status;
@Schema(description = "发送时间")
private final String submitTime;
public SalarySendResult(String status, String submitTime) {
this.status = status;
this.submitTime = submitTime;
}
}

@ -1,15 +0,0 @@
package com.yau.digitalrmb.corporatewallet.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
@Getter
@Schema(description = "SM2签名结果只返回签名值")
public class SalarySignatureResult {
@Schema(description = "签名值")
private final String signature;
public SalarySignatureResult(String signature) {
this.signature = signature;
}
}

@ -1,145 +0,0 @@
package com.yau.digitalrmb.corporatewallet.domain;
public class CorporateWalletApplication {
public enum Status {
DRAFT, SUBMITTED, APPROVED, ACTIVATED
}
private Long id;
private String userId;
private long schoolId;
private long classId;
// 企业申请信息
private String corpName;
private String creditCode;
private String legalPerson;
private String capital;
private String businessScope;
private String applyTime;
// 拼接报文
private String concatenatedMessage;
// 申请输出
private String applicationId;
private String applicationMessage;
private Status status;
// 审核信息
private String verifyId;
private String reviewer;
private String verifyTime;
// 提取摘要原文
private String extractMessage;
// SM3摘要
private String digestAlgorithm;
private String digestValue;
// 对公钱包
private String walletId;
private String walletType;
private String activateTime;
public static CorporateWalletApplication create(String userId, long schoolId, long classId) {
CorporateWalletApplication app = new CorporateWalletApplication();
app.userId = userId;
app.schoolId = schoolId;
app.classId = classId;
app.status = Status.DRAFT;
app.walletType = "对公钱包";
return app;
}
public void fillForm(String corpName, String creditCode, String legalPerson,
String capital, String businessScope, String applyTime) {
this.corpName = corpName;
this.creditCode = creditCode;
this.legalPerson = legalPerson;
this.capital = capital;
this.businessScope = businessScope;
this.applyTime = applyTime;
}
public void concatenate(String concatenatedMessage) {
this.concatenatedMessage = concatenatedMessage;
}
public void outputApplication(String applicationId, String applicationMessage) {
this.applicationId = applicationId;
this.applicationMessage = applicationMessage;
this.status = Status.SUBMITTED;
}
public void approve(String verifyId, String reviewer, String verifyTime) {
this.verifyId = verifyId;
this.reviewer = reviewer;
this.verifyTime = verifyTime;
this.status = Status.APPROVED;
}
public void extract(String extractMessage) {
this.extractMessage = extractMessage;
}
public void computeDigest(String algorithm, String digestValue) {
this.digestAlgorithm = algorithm;
this.digestValue = digestValue;
}
public void activateWallet(String walletId, String activateTime) {
this.walletId = walletId;
this.activateTime = activateTime;
this.status = Status.ACTIVATED;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public long getSchoolId() { return schoolId; }
public void setSchoolId(long schoolId) { this.schoolId = schoolId; }
public long getClassId() { return classId; }
public void setClassId(long classId) { this.classId = classId; }
public String getCorpName() { return corpName; }
public void setCorpName(String corpName) { this.corpName = corpName; }
public String getCreditCode() { return creditCode; }
public void setCreditCode(String creditCode) { this.creditCode = creditCode; }
public String getLegalPerson() { return legalPerson; }
public void setLegalPerson(String legalPerson) { this.legalPerson = legalPerson; }
public String getCapital() { return capital; }
public void setCapital(String capital) { this.capital = capital; }
public String getBusinessScope() { return businessScope; }
public void setBusinessScope(String businessScope) { this.businessScope = businessScope; }
public String getApplyTime() { return applyTime; }
public void setApplyTime(String applyTime) { this.applyTime = applyTime; }
public String getConcatenatedMessage() { return concatenatedMessage; }
public void setConcatenatedMessage(String concatenatedMessage) { this.concatenatedMessage = concatenatedMessage; }
public String getApplicationId() { return applicationId; }
public void setApplicationId(String applicationId) { this.applicationId = applicationId; }
public String getApplicationMessage() { return applicationMessage; }
public void setApplicationMessage(String applicationMessage) { this.applicationMessage = applicationMessage; }
public Status getStatus() { return status; }
public void setStatus(Status status) { this.status = status; }
public String getVerifyId() { return verifyId; }
public void setVerifyId(String verifyId) { this.verifyId = verifyId; }
public String getReviewer() { return reviewer; }
public void setReviewer(String reviewer) { this.reviewer = reviewer; }
public String getVerifyTime() { return verifyTime; }
public void setVerifyTime(String verifyTime) { this.verifyTime = verifyTime; }
public String getExtractMessage() { return extractMessage; }
public void setExtractMessage(String extractMessage) { this.extractMessage = extractMessage; }
public String getDigestAlgorithm() { return digestAlgorithm; }
public void setDigestAlgorithm(String digestAlgorithm) { this.digestAlgorithm = digestAlgorithm; }
public String getDigestValue() { return digestValue; }
public void setDigestValue(String digestValue) { this.digestValue = digestValue; }
public String getWalletId() { return walletId; }
public void setWalletId(String walletId) { this.walletId = walletId; }
public String getWalletType() { return walletType; }
public void setWalletType(String walletType) { this.walletType = walletType; }
public String getActivateTime() { return activateTime; }
public void setActivateTime(String activateTime) { this.activateTime = activateTime; }
}

@ -1,11 +0,0 @@
package com.yau.digitalrmb.corporatewallet.domain;
import java.util.Optional;
public interface CorporateWalletApplicationRepository {
CorporateWalletApplication save(CorporateWalletApplication application);
Optional<CorporateWalletApplication> findLatest(String userId, long schoolId, long classId);
Optional<CorporateWalletApplication> findByApplicationId(String applicationId);
Optional<CorporateWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId);
void update(CorporateWalletApplication application);
}

@ -1,166 +0,0 @@
package com.yau.digitalrmb.corporatewallet.domain;
public class RemoteWalletApplication {
public enum Status {
DRAFT, FACE_RECOGNIZED, SIGNED, ACTIVATED
}
private Long id;
private String userId;
private long schoolId;
private long classId;
// 步骤一:企业填写信息
private String corpName;
private String creditCode;
private String legalPerson;
private String phone;
private String applyTime;
private String concatenatedMessage;
private String applicationId;
// 人脸识别状态1=未成功2=已成功
private Integer faceRecognitionStatus;
private String faceVerifyTime;
// 步骤一输出状态
private Status status;
// 步骤二:电子签约
private String signingExtractMessage;
private String signingDigestAlgorithm;
private String signingDigestValue;
private String signPrivateKey;
private String signature;
private String contractId;
private String signTime;
// 步骤三:钱包生成
private String walletExtractMessage;
private String walletDigestAlgorithm;
private String walletDigestValue;
private String walletId;
private String walletType;
private String activateTime;
public static RemoteWalletApplication create(String userId, long schoolId, long classId) {
RemoteWalletApplication app = new RemoteWalletApplication();
app.userId = userId;
app.schoolId = schoolId;
app.classId = classId;
app.status = Status.DRAFT;
app.walletType = "对公钱包";
return app;
}
public void fillForm(String corpName, String creditCode, String legalPerson,
String phone, String applyTime) {
this.corpName = corpName;
this.creditCode = creditCode;
this.legalPerson = legalPerson;
this.phone = phone;
this.applyTime = applyTime;
}
public void concatenate(String message) {
this.concatenatedMessage = message;
}
public void updateFaceRecognition(int status) {
this.faceRecognitionStatus = status;
}
public void outputApplication(String applicationId, String faceVerifyTime) {
this.applicationId = applicationId;
this.faceVerifyTime = faceVerifyTime;
this.status = Status.FACE_RECOGNIZED;
}
public void extractSigning(String message) {
this.signingExtractMessage = message;
}
public void computeSigningDigest(String algorithm, String digest) {
this.signingDigestAlgorithm = algorithm;
this.signingDigestValue = digest;
}
public void sign(String privateKey, String signature, String contractId, String signTime) {
this.signPrivateKey = privateKey;
this.signature = signature;
this.contractId = contractId;
this.signTime = signTime;
this.status = Status.SIGNED;
}
public void extractWallet(String message) {
this.walletExtractMessage = message;
}
public void computeWalletDigest(String algorithm, String digest) {
this.walletDigestAlgorithm = algorithm;
this.walletDigestValue = digest;
}
public void activateWallet(String walletId, String activateTime) {
this.walletId = walletId;
this.activateTime = activateTime;
this.status = Status.ACTIVATED;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public long getSchoolId() { return schoolId; }
public void setSchoolId(long schoolId) { this.schoolId = schoolId; }
public long getClassId() { return classId; }
public void setClassId(long classId) { this.classId = classId; }
public String getCorpName() { return corpName; }
public void setCorpName(String corpName) { this.corpName = corpName; }
public String getCreditCode() { return creditCode; }
public void setCreditCode(String creditCode) { this.creditCode = creditCode; }
public String getLegalPerson() { return legalPerson; }
public void setLegalPerson(String legalPerson) { this.legalPerson = legalPerson; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getApplyTime() { return applyTime; }
public void setApplyTime(String applyTime) { this.applyTime = applyTime; }
public String getConcatenatedMessage() { return concatenatedMessage; }
public void setConcatenatedMessage(String concatenatedMessage) { this.concatenatedMessage = concatenatedMessage; }
public String getApplicationId() { return applicationId; }
public void setApplicationId(String applicationId) { this.applicationId = applicationId; }
public Integer getFaceRecognitionStatus() { return faceRecognitionStatus; }
public void setFaceRecognitionStatus(Integer faceRecognitionStatus) { this.faceRecognitionStatus = faceRecognitionStatus; }
public String getFaceVerifyTime() { return faceVerifyTime; }
public void setFaceVerifyTime(String faceVerifyTime) { this.faceVerifyTime = faceVerifyTime; }
public Status getStatus() { return status; }
public void setStatus(Status status) { this.status = status; }
public String getSigningExtractMessage() { return signingExtractMessage; }
public void setSigningExtractMessage(String signingExtractMessage) { this.signingExtractMessage = signingExtractMessage; }
public String getSigningDigestAlgorithm() { return signingDigestAlgorithm; }
public void setSigningDigestAlgorithm(String signingDigestAlgorithm) { this.signingDigestAlgorithm = signingDigestAlgorithm; }
public String getSigningDigestValue() { return signingDigestValue; }
public void setSigningDigestValue(String signingDigestValue) { this.signingDigestValue = signingDigestValue; }
public String getSignPrivateKey() { return signPrivateKey; }
public void setSignPrivateKey(String signPrivateKey) { this.signPrivateKey = signPrivateKey; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public String getContractId() { return contractId; }
public void setContractId(String contractId) { this.contractId = contractId; }
public String getSignTime() { return signTime; }
public void setSignTime(String signTime) { this.signTime = signTime; }
public String getWalletExtractMessage() { return walletExtractMessage; }
public void setWalletExtractMessage(String walletExtractMessage) { this.walletExtractMessage = walletExtractMessage; }
public String getWalletDigestAlgorithm() { return walletDigestAlgorithm; }
public void setWalletDigestAlgorithm(String walletDigestAlgorithm) { this.walletDigestAlgorithm = walletDigestAlgorithm; }
public String getWalletDigestValue() { return walletDigestValue; }
public void setWalletDigestValue(String walletDigestValue) { this.walletDigestValue = walletDigestValue; }
public String getWalletId() { return walletId; }
public void setWalletId(String walletId) { this.walletId = walletId; }
public String getWalletType() { return walletType; }
public void setWalletType(String walletType) { this.walletType = walletType; }
public String getActivateTime() { return activateTime; }
public void setActivateTime(String activateTime) { this.activateTime = activateTime; }
}

@ -1,10 +0,0 @@
package com.yau.digitalrmb.corporatewallet.domain;
import java.util.Optional;
public interface RemoteWalletApplicationRepository {
RemoteWalletApplication save(RemoteWalletApplication application);
Optional<RemoteWalletApplication> findLatest(String userId, long schoolId, long classId);
Optional<RemoteWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId);
void update(RemoteWalletApplication application);
}

@ -1,220 +0,0 @@
package com.yau.digitalrmb.corporatewallet.domain;
public class SalaryBatch {
public enum Status {
DRAFT, BLOCKCHAIN_FETCHED, CONCATENATED, DIGESTED, SIGNED, OUTPUT, SENT,
REVIEW_EXTRACTED, REVIEW_CHECKED, REVIEW_DIGESTED, REVIEW_SIGNED, REVIEWED,
CONFIRM_EXTRACTED, CONFIRM_DIGESTED, CONFIRM_SIGNED, CONFIRM_COMPLETED
}
private Long id;
private String userId;
private long schoolId;
private long classId;
// 调取区块信息
private String batchId;
private String corpWalletId;
private String operatorName;
private String operatorTimestamp;
// 拼接原文
private String concatenatedMessage;
// SM3摘要
private String digestAlgorithm;
private String digestValue;
// SM2签名
private String signature;
// 输出JSON
private String outputJson;
// 状态
private Status status;
private String submitTime;
// 复核员审核相关
private String reviewerName;
private String reviewTimestamp;
private String reviewConcatenatedMessage;
private String reviewDigestAlgorithm;
private String reviewDigestValue;
private String reviewSignature;
private String reviewOutputJson;
private Integer singleLimitStatus;
private Integer batchLimitStatus;
private Integer dailyLimitStatus;
private String reviewResult;
// 交易确认相关
private String confirmMessage;
private String confirmTimestamp;
private String confirmDigestAlgorithm;
private String confirmDigestValue;
private String cbSignature;
private String confirmOutputJson;
public static SalaryBatch create(String userId, long schoolId, long classId) {
SalaryBatch batch = new SalaryBatch();
batch.userId = userId;
batch.schoolId = schoolId;
batch.classId = classId;
batch.status = Status.DRAFT;
return batch;
}
public void fetchBlockchain(String batchId, String corpWalletId, String operatorName, String timestamp) {
this.batchId = batchId;
this.corpWalletId = corpWalletId;
this.operatorName = operatorName;
this.operatorTimestamp = timestamp;
this.status = Status.BLOCKCHAIN_FETCHED;
}
public void concatenate(String message) {
this.concatenatedMessage = message;
this.status = Status.CONCATENATED;
}
public void computeDigest(String algorithm, String digest) {
this.digestAlgorithm = algorithm;
this.digestValue = digest;
this.status = Status.DIGESTED;
}
public void sign(String signature) {
this.signature = signature;
this.status = Status.SIGNED;
}
public void output(String outputJson) {
this.outputJson = outputJson;
this.status = Status.OUTPUT;
}
public void send(String submitTime) {
this.submitTime = submitTime;
this.status = Status.SENT;
}
public void extractReviewText(String reviewerName, String reviewTimestamp, String reviewMessage) {
this.reviewerName = reviewerName;
this.reviewTimestamp = reviewTimestamp;
this.reviewConcatenatedMessage = reviewMessage;
this.status = Status.REVIEW_EXTRACTED;
}
public void checkPermission(Integer singleLimitStatus, Integer batchLimitStatus, Integer dailyLimitStatus, String reviewResult) {
this.singleLimitStatus = singleLimitStatus;
this.batchLimitStatus = batchLimitStatus;
this.dailyLimitStatus = dailyLimitStatus;
this.reviewResult = reviewResult;
this.status = Status.REVIEW_CHECKED;
}
public void computeReviewDigest(String algorithm, String digest) {
this.reviewDigestAlgorithm = algorithm;
this.reviewDigestValue = digest;
this.status = Status.REVIEW_DIGESTED;
}
public void signReview(String signature) {
this.reviewSignature = signature;
this.status = Status.REVIEW_SIGNED;
}
public void outputReview(String outputJson) {
this.reviewOutputJson = outputJson;
this.status = Status.REVIEWED;
}
public void extractConfirmText(String confirmTimestamp, String confirmMessage) {
this.confirmTimestamp = confirmTimestamp;
this.confirmMessage = confirmMessage;
this.status = Status.CONFIRM_EXTRACTED;
}
public void computeConfirmDigest(String algorithm, String digest) {
this.confirmDigestAlgorithm = algorithm;
this.confirmDigestValue = digest;
this.status = Status.CONFIRM_DIGESTED;
}
public void signConfirm(String cbSignature) {
this.cbSignature = cbSignature;
this.status = Status.CONFIRM_SIGNED;
}
public void outputConfirm(String outputJson) {
this.confirmOutputJson = outputJson;
this.status = Status.CONFIRM_COMPLETED;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public long getSchoolId() { return schoolId; }
public void setSchoolId(long schoolId) { this.schoolId = schoolId; }
public long getClassId() { return classId; }
public void setClassId(long classId) { this.classId = classId; }
public String getBatchId() { return batchId; }
public void setBatchId(String batchId) { this.batchId = batchId; }
public String getCorpWalletId() { return corpWalletId; }
public void setCorpWalletId(String corpWalletId) { this.corpWalletId = corpWalletId; }
public String getOperatorName() { return operatorName; }
public void setOperatorName(String operatorName) { this.operatorName = operatorName; }
public String getOperatorTimestamp() { return operatorTimestamp; }
public void setOperatorTimestamp(String operatorTimestamp) { this.operatorTimestamp = operatorTimestamp; }
public String getConcatenatedMessage() { return concatenatedMessage; }
public void setConcatenatedMessage(String concatenatedMessage) { this.concatenatedMessage = concatenatedMessage; }
public String getDigestAlgorithm() { return digestAlgorithm; }
public void setDigestAlgorithm(String digestAlgorithm) { this.digestAlgorithm = digestAlgorithm; }
public String getDigestValue() { return digestValue; }
public void setDigestValue(String digestValue) { this.digestValue = digestValue; }
public String getSignature() { return signature; }
public void setSignature(String signature) { this.signature = signature; }
public String getOutputJson() { return outputJson; }
public void setOutputJson(String outputJson) { this.outputJson = outputJson; }
public Status getStatus() { return status; }
public void setStatus(Status status) { this.status = status; }
public String getSubmitTime() { return submitTime; }
public void setSubmitTime(String submitTime) { this.submitTime = submitTime; }
public String getReviewerName() { return reviewerName; }
public void setReviewerName(String reviewerName) { this.reviewerName = reviewerName; }
public String getReviewTimestamp() { return reviewTimestamp; }
public void setReviewTimestamp(String reviewTimestamp) { this.reviewTimestamp = reviewTimestamp; }
public String getReviewConcatenatedMessage() { return reviewConcatenatedMessage; }
public void setReviewConcatenatedMessage(String reviewConcatenatedMessage) { this.reviewConcatenatedMessage = reviewConcatenatedMessage; }
public String getReviewDigestAlgorithm() { return reviewDigestAlgorithm; }
public void setReviewDigestAlgorithm(String reviewDigestAlgorithm) { this.reviewDigestAlgorithm = reviewDigestAlgorithm; }
public String getReviewDigestValue() { return reviewDigestValue; }
public void setReviewDigestValue(String reviewDigestValue) { this.reviewDigestValue = reviewDigestValue; }
public String getReviewSignature() { return reviewSignature; }
public void setReviewSignature(String reviewSignature) { this.reviewSignature = reviewSignature; }
public String getReviewOutputJson() { return reviewOutputJson; }
public void setReviewOutputJson(String reviewOutputJson) { this.reviewOutputJson = reviewOutputJson; }
public Integer getSingleLimitStatus() { return singleLimitStatus; }
public void setSingleLimitStatus(Integer singleLimitStatus) { this.singleLimitStatus = singleLimitStatus; }
public Integer getBatchLimitStatus() { return batchLimitStatus; }
public void setBatchLimitStatus(Integer batchLimitStatus) { this.batchLimitStatus = batchLimitStatus; }
public Integer getDailyLimitStatus() { return dailyLimitStatus; }
public void setDailyLimitStatus(Integer dailyLimitStatus) { this.dailyLimitStatus = dailyLimitStatus; }
public String getReviewResult() { return reviewResult; }
public void setReviewResult(String reviewResult) { this.reviewResult = reviewResult; }
public String getConfirmMessage() { return confirmMessage; }
public void setConfirmMessage(String confirmMessage) { this.confirmMessage = confirmMessage; }
public String getConfirmTimestamp() { return confirmTimestamp; }
public void setConfirmTimestamp(String confirmTimestamp) { this.confirmTimestamp = confirmTimestamp; }
public String getConfirmDigestAlgorithm() { return confirmDigestAlgorithm; }
public void setConfirmDigestAlgorithm(String confirmDigestAlgorithm) { this.confirmDigestAlgorithm = confirmDigestAlgorithm; }
public String getConfirmDigestValue() { return confirmDigestValue; }
public void setConfirmDigestValue(String confirmDigestValue) { this.confirmDigestValue = confirmDigestValue; }
public String getCbSignature() { return cbSignature; }
public void setCbSignature(String cbSignature) { this.cbSignature = cbSignature; }
public String getConfirmOutputJson() { return confirmOutputJson; }
public void setConfirmOutputJson(String confirmOutputJson) { this.confirmOutputJson = confirmOutputJson; }
}

@ -1,9 +0,0 @@
package com.yau.digitalrmb.corporatewallet.domain;
import java.util.Optional;
public interface SalaryBatchRepository {
SalaryBatch save(SalaryBatch batch);
Optional<SalaryBatch> findLatest(String userId, long schoolId, long classId);
void update(SalaryBatch batch);
}

@ -1,61 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("corporate_wallet_application")
public class CorporateWalletApplicationEntity extends AuditableEntity {
@TableField("user_id")
private String userId;
@TableField("school_id")
private Long schoolId;
@TableField("class_id")
private Long classId;
@TableField("corp_name")
private String corpName;
@TableField("credit_code")
private String creditCode;
@TableField("legal_person")
private String legalPerson;
private String capital;
@TableField("business_scope")
private String businessScope;
@TableField("apply_time")
private String applyTime;
@TableField("concatenated_message")
private String concatenatedMessage;
@TableField("application_id")
private String applicationId;
@TableField("application_message")
private String applicationMessage;
private String status;
@TableField("verify_id")
private String verifyId;
private String reviewer;
@TableField("verify_time")
private String verifyTime;
@TableField("extract_message")
private String extractMessage;
@TableField("digest_algorithm")
private String digestAlgorithm;
@TableField("digest_value")
private String digestValue;
@TableField("wallet_id")
private String walletId;
@TableField("wallet_type")
private String walletType;
@TableField("activate_time")
private String activateTime;
}

@ -1,8 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CorporateWalletApplicationMapper extends BaseMapper<CorporateWalletApplicationEntity> {
}

@ -1,31 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("corporate_wallet_key_info")
public class CorporateWalletKeyInfoEntity extends AuditableEntity {
@TableField("user_id")
private String userId;
@TableField("school_id")
private Long schoolId;
@TableField("class_id")
private Long classId;
@TableField("key_type")
private String keyType;
@TableField("holder")
private String holder;
@TableField("key_algorithm")
private String keyAlgorithm;
@TableField("key_value")
private String keyValue;
@TableField("is_private_key")
private Boolean isPrivateKey;
@TableField("source")
private String source;
}

@ -1,8 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface CorporateWalletKeyInfoMapper extends BaseMapper<CorporateWalletKeyInfoEntity> {
}

@ -1,123 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.CorporateWalletApplicationRepository;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.Optional;
@Repository
public class MybatisCorporateWalletApplicationRepository implements CorporateWalletApplicationRepository {
@Resource
private CorporateWalletApplicationMapper mapper;
@Override
public CorporateWalletApplication save(CorporateWalletApplication application) {
CorporateWalletApplicationEntity entity = toEntity(application);
mapper.insert(entity);
application.setId(entity.getId());
return application;
}
@Override
public Optional<CorporateWalletApplication> findLatest(String userId, long schoolId, long classId) {
CorporateWalletApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<CorporateWalletApplicationEntity>()
.eq(CorporateWalletApplicationEntity::getUserId, userId)
.eq(CorporateWalletApplicationEntity::getSchoolId, schoolId)
.eq(CorporateWalletApplicationEntity::getClassId, classId)
.eq(CorporateWalletApplicationEntity::getDeleted, false)
.orderByDesc(CorporateWalletApplicationEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public Optional<CorporateWalletApplication> findByApplicationId(String applicationId) {
CorporateWalletApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<CorporateWalletApplicationEntity>()
.eq(CorporateWalletApplicationEntity::getApplicationId, applicationId)
.eq(CorporateWalletApplicationEntity::getDeleted, false)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public Optional<CorporateWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId) {
CorporateWalletApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<CorporateWalletApplicationEntity>()
.eq(CorporateWalletApplicationEntity::getCreditCode, creditCode)
.eq(CorporateWalletApplicationEntity::getUserId, userId)
.eq(CorporateWalletApplicationEntity::getSchoolId, schoolId)
.eq(CorporateWalletApplicationEntity::getClassId, classId)
.eq(CorporateWalletApplicationEntity::getDeleted, false)
.isNotNull(CorporateWalletApplicationEntity::getWalletId)
.ne(CorporateWalletApplicationEntity::getWalletId, "")
.orderByDesc(CorporateWalletApplicationEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public void update(CorporateWalletApplication application) {
mapper.updateById(toEntity(application));
}
private CorporateWalletApplicationEntity toEntity(CorporateWalletApplication app) {
CorporateWalletApplicationEntity entity = new CorporateWalletApplicationEntity();
entity.setId(app.getId());
entity.setUserId(app.getUserId());
entity.setSchoolId(app.getSchoolId());
entity.setClassId(app.getClassId());
entity.setCorpName(app.getCorpName());
entity.setCreditCode(app.getCreditCode());
entity.setLegalPerson(app.getLegalPerson());
entity.setCapital(app.getCapital());
entity.setBusinessScope(app.getBusinessScope());
entity.setApplyTime(app.getApplyTime());
entity.setConcatenatedMessage(app.getConcatenatedMessage());
entity.setApplicationId(app.getApplicationId());
entity.setApplicationMessage(app.getApplicationMessage());
entity.setStatus(app.getStatus() == null ? null : app.getStatus().name());
entity.setVerifyId(app.getVerifyId());
entity.setReviewer(app.getReviewer());
entity.setVerifyTime(app.getVerifyTime());
entity.setExtractMessage(app.getExtractMessage());
entity.setDigestAlgorithm(app.getDigestAlgorithm());
entity.setDigestValue(app.getDigestValue());
entity.setWalletId(app.getWalletId());
entity.setWalletType(app.getWalletType());
entity.setActivateTime(app.getActivateTime());
return entity;
}
private CorporateWalletApplication toDomain(CorporateWalletApplicationEntity entity) {
CorporateWalletApplication app = new CorporateWalletApplication();
app.setId(entity.getId());
app.setUserId(entity.getUserId());
app.setSchoolId(entity.getSchoolId());
app.setClassId(entity.getClassId());
app.setCorpName(entity.getCorpName());
app.setCreditCode(entity.getCreditCode());
app.setLegalPerson(entity.getLegalPerson());
app.setCapital(entity.getCapital());
app.setBusinessScope(entity.getBusinessScope());
app.setApplyTime(entity.getApplyTime());
app.setConcatenatedMessage(entity.getConcatenatedMessage());
app.setApplicationId(entity.getApplicationId());
app.setApplicationMessage(entity.getApplicationMessage());
app.setStatus(entity.getStatus() == null ? null : CorporateWalletApplication.Status.valueOf(entity.getStatus()));
app.setVerifyId(entity.getVerifyId());
app.setReviewer(entity.getReviewer());
app.setVerifyTime(entity.getVerifyTime());
app.setExtractMessage(entity.getExtractMessage());
app.setDigestAlgorithm(entity.getDigestAlgorithm());
app.setDigestValue(entity.getDigestValue());
app.setWalletId(entity.getWalletId());
app.setWalletType(entity.getWalletType());
app.setActivateTime(entity.getActivateTime());
return app;
}
}

@ -1,121 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplication;
import com.yau.digitalrmb.corporatewallet.domain.RemoteWalletApplicationRepository;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.Optional;
@Repository
public class MybatisRemoteWalletApplicationRepository implements RemoteWalletApplicationRepository {
@Resource
private RemoteWalletApplicationMapper mapper;
@Override
public RemoteWalletApplication save(RemoteWalletApplication application) {
RemoteWalletApplicationEntity entity = toEntity(application);
mapper.insert(entity);
application.setId(entity.getId());
return application;
}
@Override
public Optional<RemoteWalletApplication> findLatest(String userId, long schoolId, long classId) {
RemoteWalletApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<RemoteWalletApplicationEntity>()
.eq(RemoteWalletApplicationEntity::getUserId, userId)
.eq(RemoteWalletApplicationEntity::getSchoolId, schoolId)
.eq(RemoteWalletApplicationEntity::getClassId, classId)
.eq(RemoteWalletApplicationEntity::getDeleted, false)
.orderByDesc(RemoteWalletApplicationEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public Optional<RemoteWalletApplication> findActivatedByCreditCode(String creditCode, String userId, long schoolId, long classId) {
RemoteWalletApplicationEntity entity = mapper.selectOne(
new LambdaQueryWrapper<RemoteWalletApplicationEntity>()
.eq(RemoteWalletApplicationEntity::getCreditCode, creditCode)
.eq(RemoteWalletApplicationEntity::getUserId, userId)
.eq(RemoteWalletApplicationEntity::getSchoolId, schoolId)
.eq(RemoteWalletApplicationEntity::getClassId, classId)
.eq(RemoteWalletApplicationEntity::getDeleted, false)
.isNotNull(RemoteWalletApplicationEntity::getWalletId)
.ne(RemoteWalletApplicationEntity::getWalletId, "")
.orderByDesc(RemoteWalletApplicationEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public void update(RemoteWalletApplication application) {
mapper.updateById(toEntity(application));
}
private RemoteWalletApplicationEntity toEntity(RemoteWalletApplication app) {
RemoteWalletApplicationEntity entity = new RemoteWalletApplicationEntity();
entity.setId(app.getId());
entity.setUserId(app.getUserId());
entity.setSchoolId(app.getSchoolId());
entity.setClassId(app.getClassId());
entity.setCorpName(app.getCorpName());
entity.setCreditCode(app.getCreditCode());
entity.setLegalPerson(app.getLegalPerson());
entity.setPhone(app.getPhone());
entity.setApplyTime(app.getApplyTime());
entity.setConcatenatedMessage(app.getConcatenatedMessage());
entity.setApplicationId(app.getApplicationId());
entity.setFaceRecognitionStatus(app.getFaceRecognitionStatus());
entity.setFaceVerifyTime(app.getFaceVerifyTime());
entity.setStatus(app.getStatus() == null ? null : app.getStatus().name());
entity.setSigningExtractMessage(app.getSigningExtractMessage());
entity.setSigningDigestAlgorithm(app.getSigningDigestAlgorithm());
entity.setSigningDigestValue(app.getSigningDigestValue());
entity.setSignPrivateKey(app.getSignPrivateKey());
entity.setSignature(app.getSignature());
entity.setContractId(app.getContractId());
entity.setSignTime(app.getSignTime());
entity.setWalletExtractMessage(app.getWalletExtractMessage());
entity.setWalletDigestAlgorithm(app.getWalletDigestAlgorithm());
entity.setWalletDigestValue(app.getWalletDigestValue());
entity.setWalletId(app.getWalletId());
entity.setWalletType(app.getWalletType());
entity.setActivateTime(app.getActivateTime());
return entity;
}
private RemoteWalletApplication toDomain(RemoteWalletApplicationEntity entity) {
RemoteWalletApplication app = new RemoteWalletApplication();
app.setId(entity.getId());
app.setUserId(entity.getUserId());
app.setSchoolId(entity.getSchoolId());
app.setClassId(entity.getClassId());
app.setCorpName(entity.getCorpName());
app.setCreditCode(entity.getCreditCode());
app.setLegalPerson(entity.getLegalPerson());
app.setPhone(entity.getPhone());
app.setApplyTime(entity.getApplyTime());
app.setConcatenatedMessage(entity.getConcatenatedMessage());
app.setApplicationId(entity.getApplicationId());
app.setFaceRecognitionStatus(entity.getFaceRecognitionStatus());
app.setFaceVerifyTime(entity.getFaceVerifyTime());
app.setStatus(entity.getStatus() == null ? null : RemoteWalletApplication.Status.valueOf(entity.getStatus()));
app.setSigningExtractMessage(entity.getSigningExtractMessage());
app.setSigningDigestAlgorithm(entity.getSigningDigestAlgorithm());
app.setSigningDigestValue(entity.getSigningDigestValue());
app.setSignPrivateKey(entity.getSignPrivateKey());
app.setSignature(entity.getSignature());
app.setContractId(entity.getContractId());
app.setSignTime(entity.getSignTime());
app.setWalletExtractMessage(entity.getWalletExtractMessage());
app.setWalletDigestAlgorithm(entity.getWalletDigestAlgorithm());
app.setWalletDigestValue(entity.getWalletDigestValue());
app.setWalletId(entity.getWalletId());
app.setWalletType(entity.getWalletType());
app.setActivateTime(entity.getActivateTime());
return app;
}
}

@ -1,115 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatch;
import com.yau.digitalrmb.corporatewallet.domain.SalaryBatchRepository;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.Optional;
@Repository
public class MybatisSalaryBatchRepository implements SalaryBatchRepository {
@Resource
private SalaryBatchMapper mapper;
@Override
public SalaryBatch save(SalaryBatch batch) {
SalaryBatchEntity entity = toEntity(batch);
mapper.insert(entity);
batch.setId(entity.getId());
return batch;
}
@Override
public Optional<SalaryBatch> findLatest(String userId, long schoolId, long classId) {
SalaryBatchEntity entity = mapper.selectOne(
new LambdaQueryWrapper<SalaryBatchEntity>()
.eq(SalaryBatchEntity::getUserId, userId)
.eq(SalaryBatchEntity::getSchoolId, schoolId)
.eq(SalaryBatchEntity::getClassId, classId)
.eq(SalaryBatchEntity::getDeleted, false)
.orderByDesc(SalaryBatchEntity::getCreatedAt)
.last("LIMIT 1"));
return entity == null ? Optional.empty() : Optional.of(toDomain(entity));
}
@Override
public void update(SalaryBatch batch) {
mapper.updateById(toEntity(batch));
}
private SalaryBatchEntity toEntity(SalaryBatch batch) {
SalaryBatchEntity entity = new SalaryBatchEntity();
entity.setId(batch.getId());
entity.setUserId(batch.getUserId());
entity.setSchoolId(batch.getSchoolId());
entity.setClassId(batch.getClassId());
entity.setBatchId(batch.getBatchId());
entity.setCorpWalletId(batch.getCorpWalletId());
entity.setOperatorName(batch.getOperatorName());
entity.setOperatorTimestamp(batch.getOperatorTimestamp());
entity.setConcatenatedMessage(batch.getConcatenatedMessage());
entity.setDigestAlgorithm(batch.getDigestAlgorithm());
entity.setDigestValue(batch.getDigestValue());
entity.setSignature(batch.getSignature());
entity.setOutputJson(batch.getOutputJson());
entity.setStatus(batch.getStatus() == null ? null : batch.getStatus().name());
entity.setSubmitTime(batch.getSubmitTime());
entity.setReviewerName(batch.getReviewerName());
entity.setReviewTimestamp(batch.getReviewTimestamp());
entity.setReviewConcatenatedMessage(batch.getReviewConcatenatedMessage());
entity.setReviewDigestAlgorithm(batch.getReviewDigestAlgorithm());
entity.setReviewDigestValue(batch.getReviewDigestValue());
entity.setReviewSignature(batch.getReviewSignature());
entity.setReviewOutputJson(batch.getReviewOutputJson());
entity.setSingleLimitStatus(batch.getSingleLimitStatus());
entity.setBatchLimitStatus(batch.getBatchLimitStatus());
entity.setDailyLimitStatus(batch.getDailyLimitStatus());
entity.setReviewResult(batch.getReviewResult());
entity.setConfirmMessage(batch.getConfirmMessage());
entity.setConfirmTimestamp(batch.getConfirmTimestamp());
entity.setConfirmDigestAlgorithm(batch.getConfirmDigestAlgorithm());
entity.setConfirmDigestValue(batch.getConfirmDigestValue());
entity.setCbSignature(batch.getCbSignature());
entity.setConfirmOutputJson(batch.getConfirmOutputJson());
return entity;
}
private SalaryBatch toDomain(SalaryBatchEntity entity) {
SalaryBatch batch = new SalaryBatch();
batch.setId(entity.getId());
batch.setUserId(entity.getUserId());
batch.setSchoolId(entity.getSchoolId());
batch.setClassId(entity.getClassId());
batch.setBatchId(entity.getBatchId());
batch.setCorpWalletId(entity.getCorpWalletId());
batch.setOperatorName(entity.getOperatorName());
batch.setOperatorTimestamp(entity.getOperatorTimestamp());
batch.setConcatenatedMessage(entity.getConcatenatedMessage());
batch.setDigestAlgorithm(entity.getDigestAlgorithm());
batch.setDigestValue(entity.getDigestValue());
batch.setSignature(entity.getSignature());
batch.setOutputJson(entity.getOutputJson());
batch.setStatus(entity.getStatus() == null ? null : SalaryBatch.Status.valueOf(entity.getStatus()));
batch.setSubmitTime(entity.getSubmitTime());
batch.setReviewerName(entity.getReviewerName());
batch.setReviewTimestamp(entity.getReviewTimestamp());
batch.setReviewConcatenatedMessage(entity.getReviewConcatenatedMessage());
batch.setReviewDigestAlgorithm(entity.getReviewDigestAlgorithm());
batch.setReviewDigestValue(entity.getReviewDigestValue());
batch.setReviewSignature(entity.getReviewSignature());
batch.setReviewOutputJson(entity.getReviewOutputJson());
batch.setSingleLimitStatus(entity.getSingleLimitStatus());
batch.setBatchLimitStatus(entity.getBatchLimitStatus());
batch.setDailyLimitStatus(entity.getDailyLimitStatus());
batch.setReviewResult(entity.getReviewResult());
batch.setConfirmMessage(entity.getConfirmMessage());
batch.setConfirmTimestamp(entity.getConfirmTimestamp());
batch.setConfirmDigestAlgorithm(entity.getConfirmDigestAlgorithm());
batch.setConfirmDigestValue(entity.getConfirmDigestValue());
batch.setCbSignature(entity.getCbSignature());
batch.setConfirmOutputJson(entity.getConfirmOutputJson());
return batch;
}
}

@ -1,68 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("remote_wallet_application")
public class RemoteWalletApplicationEntity extends AuditableEntity {
@TableField("user_id")
private String userId;
@TableField("school_id")
private Long schoolId;
@TableField("class_id")
private Long classId;
@TableField("corp_name")
private String corpName;
@TableField("credit_code")
private String creditCode;
@TableField("legal_person")
private String legalPerson;
private String phone;
@TableField("apply_time")
private String applyTime;
@TableField("concatenated_message")
private String concatenatedMessage;
@TableField("application_id")
private String applicationId;
@TableField("face_recognition_status")
private Integer faceRecognitionStatus;
@TableField("face_verify_time")
private String faceVerifyTime;
private String status;
@TableField("signing_extract_message")
private String signingExtractMessage;
@TableField("signing_digest_algorithm")
private String signingDigestAlgorithm;
@TableField("signing_digest_value")
private String signingDigestValue;
@TableField("sign_private_key")
private String signPrivateKey;
private String signature;
@TableField("contract_id")
private String contractId;
@TableField("sign_time")
private String signTime;
@TableField("wallet_extract_message")
private String walletExtractMessage;
@TableField("wallet_digest_algorithm")
private String walletDigestAlgorithm;
@TableField("wallet_digest_value")
private String walletDigestValue;
@TableField("wallet_id")
private String walletId;
@TableField("wallet_type")
private String walletType;
@TableField("activate_time")
private String activateTime;
}

@ -1,8 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface RemoteWalletApplicationMapper extends BaseMapper<RemoteWalletApplicationEntity> {
}

@ -1,79 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@TableName("salary_batch")
public class SalaryBatchEntity extends AuditableEntity {
@TableField("user_id")
private String userId;
@TableField("school_id")
private Long schoolId;
@TableField("class_id")
private Long classId;
@TableField("batch_id")
private String batchId;
@TableField("corp_wallet_id")
private String corpWalletId;
@TableField("operator_name")
private String operatorName;
@TableField("operator_timestamp")
private String operatorTimestamp;
@TableField("concatenated_message")
private String concatenatedMessage;
@TableField("digest_algorithm")
private String digestAlgorithm;
@TableField("digest_value")
private String digestValue;
@TableField("signature")
private String signature;
@TableField("output_json")
private String outputJson;
@TableField("status")
private String status;
@TableField("submit_time")
private String submitTime;
// 复核员审核相关
@TableField("reviewer_name")
private String reviewerName;
@TableField("review_timestamp")
private String reviewTimestamp;
@TableField("review_concatenated_message")
private String reviewConcatenatedMessage;
@TableField("review_digest_algorithm")
private String reviewDigestAlgorithm;
@TableField("review_digest_value")
private String reviewDigestValue;
@TableField("review_signature")
private String reviewSignature;
@TableField("review_output_json")
private String reviewOutputJson;
@TableField("single_limit_status")
private Integer singleLimitStatus;
@TableField("batch_limit_status")
private Integer batchLimitStatus;
@TableField("daily_limit_status")
private Integer dailyLimitStatus;
@TableField("review_result")
private String reviewResult;
// 交易确认相关
@TableField("confirm_message")
private String confirmMessage;
@TableField("confirm_timestamp")
private String confirmTimestamp;
@TableField("confirm_digest_algorithm")
private String confirmDigestAlgorithm;
@TableField("confirm_digest_value")
private String confirmDigestValue;
@TableField("cb_signature")
private String cbSignature;
@TableField("confirm_output_json")
private String confirmOutputJson;
}

@ -1,8 +0,0 @@
package com.yau.digitalrmb.corporatewallet.infrastructure;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SalaryBatchMapper extends BaseMapper<SalaryBatchEntity> {
}

@ -1,101 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.corporatewallet.application.CorporateApplicationResult;
import com.yau.digitalrmb.corporatewallet.application.CorporateConcatenateResult;
import com.yau.digitalrmb.corporatewallet.application.CorporateDigestResult;
import com.yau.digitalrmb.corporatewallet.application.CorporateExtractResult;
import com.yau.digitalrmb.corporatewallet.application.CorporateVerifyResult;
import com.yau.digitalrmb.corporatewallet.application.CorporateWalletKeyResult;
import com.yau.digitalrmb.corporatewallet.application.CorporateWalletKeyService;
import com.yau.digitalrmb.corporatewallet.application.CorporateWalletResult;
import com.yau.digitalrmb.corporatewallet.application.CorporateWalletService;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.CorporateWalletApplicationRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/v1/corporate-wallet")
@Tag(name = "模块六 - 对公钱包开通", description = "企业法人申请开通数字人民币对公钱包全流程")
public class CorporateWalletController {
@Resource
private CorporateWalletService corporateWalletService;
@Resource
private CorporateWalletKeyService corporateWalletKeyService;
@PostMapping("/keys")
@Operation(summary = "获取全部密钥信息",
description = "复用央行第一密钥和商业银行第二密钥,新生成企业法人、数币操作员、数币复核员、监管机构密钥,存储到数据库并返回")
public ApiResponse<List<CorporateWalletKeyResult>> getAllKeys() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(corporateWalletKeyService.getAllKeys(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
}
@PostMapping("/concatenate")
@Operation(summary = "拼接原文",
description = "根据企业名称、统一社会信用代码、法人代表、注册资本、经营范围、申请时间拼接原文")
public ApiResponse<CorporateConcatenateResult> concatenate(
@Valid @RequestBody CorporateWalletApplicationRequest request) {
return ApiResponse.success(corporateWalletService.concatenate(
InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId());
}
@PostMapping("/output")
@Operation(summary = "企业法人提交申请输出",
description = "保存企业申请信息到数据库,返回申请编号、企业名称、信用代码、法人、注册资本、申请时间和状态")
public ApiResponse<CorporateApplicationResult> output(
@Valid @RequestBody CorporateWalletApplicationRequest request) {
return ApiResponse.success(corporateWalletService.outputApplication(
InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId());
}
@PostMapping("/verify")
@Operation(summary = "银行端审核材料",
description = "点击审核按钮审核通过后状态变为APPROVED返回审核编号、申请编号、审核状态、审核人和审核时间")
public ApiResponse<CorporateVerifyResult> verify() {
return ApiResponse.success(corporateWalletService.verify(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/extract")
@Operation(summary = "提取摘要原文",
description = "审核通过后提取摘要原文,格式为:统一社会信用代码|审核时间")
public ApiResponse<CorporateExtractResult> extract() {
return ApiResponse.success(corporateWalletService.extract(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/digest")
@Operation(summary = "SM3运算摘要",
description = "对提取的摘要原文进行SM3哈希运算返回算法名称和摘要值")
public ApiResponse<CorporateDigestResult> computeDigest() {
return ApiResponse.success(corporateWalletService.computeDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/wallet-output")
@Operation(summary = "生成对公钱包输出",
description = "根据SM3摘要生成对公钱包ID返回钱包ID、企业名称、信用代码、钱包类型、状态和激活时间")
public ApiResponse<CorporateWalletResult> outputWallet() {
return ApiResponse.success(corporateWalletService.outputWallet(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -1,128 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.corporatewallet.application.RemoteApplicationResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteConcatenateResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteContractResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteDigestResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteExtractResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteFaceRecognitionResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteSignatureResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteWalletResult;
import com.yau.digitalrmb.corporatewallet.application.RemoteWalletService;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteFaceRecognitionRequest;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.RemoteWalletApplicationRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/corporate-wallet/remote")
@Tag(name = "模块六 - 对公钱包远程开立", description = "企业法人远程申请开通数字人民币对公钱包全流程")
public class RemoteWalletController {
@Resource
private RemoteWalletService remoteWalletService;
// ==================== 步骤一:企业法人在线填写信息 ====================
@PostMapping("/concatenate")
@Operation(summary = "步骤一-拼接原文",
description = "根据企业名称、统一社会信用代码、法人代表、手机号码、申请时间拼接原文")
public ApiResponse<RemoteConcatenateResult> concatenate(
@Valid @RequestBody RemoteWalletApplicationRequest request) {
return ApiResponse.success(remoteWalletService.concatenate(
InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId());
}
@PostMapping("/face-recognition")
@Operation(summary = "步骤一-更新人脸识别状态",
description = "前端完成人脸识别后调用参数1=未成功2=已成功。存储状态后才能进行输出")
public ApiResponse<RemoteFaceRecognitionResult> faceRecognition(
@Valid @RequestBody RemoteFaceRecognitionRequest request) {
return ApiResponse.success(remoteWalletService.updateFaceRecognition(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getStatus()), traceId());
}
@PostMapping("/output")
@Operation(summary = "步骤一-输出申请报文",
description = "需先完成人脸识别。返回申请编号、企业信息、人脸识别详情和状态")
public ApiResponse<RemoteApplicationResult> output(
@Valid @RequestBody RemoteWalletApplicationRequest request) {
return ApiResponse.success(remoteWalletService.outputApplication(
InstitutionKeySubject.from(AuthContextHolder.get()), request), traceId());
}
// ==================== 步骤二:法人人脸识别与电子签约 ====================
@PostMapping("/extract-signing")
@Operation(summary = "步骤二-提取电子签约摘要原文",
description = "格式E_SIGN|申请编号|法人代表|签约时间")
public ApiResponse<RemoteExtractResult> extractSigning() {
return ApiResponse.success(remoteWalletService.extractSigning(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/signing-digest")
@Operation(summary = "步骤二-SM3运算签约摘要",
description = "对电子签约摘要原文进行SM3哈希运算")
public ApiResponse<RemoteDigestResult> computeSigningDigest() {
return ApiResponse.success(remoteWalletService.computeSigningDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/sign")
@Operation(summary = "步骤二-SM2计算签名",
description = "生成SM2密钥对对签约原文进行签名返回私钥和签名值")
public ApiResponse<RemoteSignatureResult> computeSignature() {
return ApiResponse.success(remoteWalletService.computeSignature(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/contract-output")
@Operation(summary = "步骤二-输出电子签约结果",
description = "返回合约编号、申请编号、签约人、人脸识别确认、签名值、签约时间和状态")
public ApiResponse<RemoteContractResult> outputContract() {
return ApiResponse.success(remoteWalletService.outputContract(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
// ==================== 步骤三:自动审核与钱包生成 ====================
@PostMapping("/extract-wallet")
@Operation(summary = "步骤三-提取钱包摘要原文",
description = "格式:统一社会信用代码|签约时间")
public ApiResponse<RemoteExtractResult> extractWallet() {
return ApiResponse.success(remoteWalletService.extractWallet(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/wallet-digest")
@Operation(summary = "步骤三-SM3运算钱包摘要",
description = "对钱包摘要原文进行SM3哈希运算生成钱包ID")
public ApiResponse<RemoteDigestResult> computeWalletDigest() {
return ApiResponse.success(remoteWalletService.computeWalletDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/wallet-output")
@Operation(summary = "步骤三-输出对公钱包",
description = "返回钱包ID、企业名称、信用代码、钱包类型、状态、激活时间和人脸识别记录")
public ApiResponse<RemoteWalletResult> outputWallet() {
return ApiResponse.success(remoteWalletService.outputWallet(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -1,92 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.corporatewallet.application.SalaryBatchService;
import com.yau.digitalrmb.corporatewallet.application.SalaryBlockchainResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConcatenateResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryDigestResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryOutputResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryPageResult;
import com.yau.digitalrmb.corporatewallet.application.SalarySendResult;
import com.yau.digitalrmb.corporatewallet.application.SalarySignatureResult;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.SalarySignRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
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 javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/salary")
@Tag(name = "模块六-企业发放数字人民币工资 - 步骤1数币操作员导入工资明细", description = "数币操作员导入工资明细、签名并发送全流程")
public class SalaryBatchController {
@Resource
private SalaryBatchService salaryBatchService;
@PostMapping("/blockchain")
@Operation(summary = "调取区块信息",
description = "从步骤一对公钱包开立获取付款钱包ID随机生成批次号操作员为王五生成时间戳")
public ApiResponse<SalaryBlockchainResult> fetchBlockchain() {
JwtUser jwtUser = AuthContextHolder.get();
return ApiResponse.success(salaryBatchService.fetchBlockchain(
InstitutionKeySubject.from(jwtUser), jwtUser.getUsername()), traceId());
}
@PostMapping("/concatenate")
@Operation(summary = "拼接工资发放申请原文",
description = "拼接批次号|付款钱包|操作员|员工姓名|钱包ID|金额|...|时间戳")
public ApiResponse<SalaryConcatenateResult> concatenate() {
return ApiResponse.success(salaryBatchService.concatenate(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/digest")
@Operation(summary = "生成摘要",
description = "对拼接原文进行SM3运算只返回摘要值")
public ApiResponse<SalaryDigestResult> computeDigest() {
return ApiResponse.success(salaryBatchService.computeDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/sign")
@Operation(summary = "SM2签名",
description = "传入数币操作员私钥进行SM2签名私钥不正确则错误数+1只返回签名值")
public ApiResponse<SalarySignatureResult> sign(@Valid @RequestBody SalarySignRequest request) {
return ApiResponse.success(salaryBatchService.sign(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId());
}
@PostMapping("/output")
@Operation(summary = "输出",
description = "生成包含批次号、钱包、工资明细、摘要、签名等信息的JSON报文")
public ApiResponse<SalaryOutputResult> output() {
return ApiResponse.success(salaryBatchService.output(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/send")
@Operation(summary = "发送",
description = "发送工资发放申请到数币复核员审核,状态变为待审核")
public ApiResponse<SalarySendResult> send() {
return ApiResponse.success(salaryBatchService.send(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@GetMapping("/page")
@Operation(summary = "页面查询",
description = "查询工资明细和当前步骤状态,前面步骤未完成时返回工资明细但其他字段为空(不报错)")
public ApiResponse<SalaryPageResult> pageQuery() {
return ApiResponse.success(salaryBatchService.pageQuery(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -1,63 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.corporatewallet.application.SalaryBatchService;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmDigestResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmExtractResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmOutputResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryConfirmSignatureResult;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.SalaryConfirmSignRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder;
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 javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/salary/confirm")
@Tag(name = "模块六-企业发放数字人民币工资 - 步骤3交易确认与工资发放", description = "提取交易确认原文、生成摘要、央行签名并输出全流程")
public class SalaryConfirmController {
@Resource
private SalaryBatchService salaryBatchService;
@PostMapping("/extract")
@Operation(summary = "提取交易确认原文",
description = "从已审核的工资批次中提取交易确认原文格式CONFIRM|批次号|总金额|人数|时间戳")
public ApiResponse<SalaryConfirmExtractResult> extractConfirmText() {
return ApiResponse.success(salaryBatchService.extractConfirmText(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/digest")
@Operation(summary = "生成交易确认摘要",
description = "对拼接交易确认原文进行SM3运算只返回摘要值")
public ApiResponse<SalaryConfirmDigestResult> computeConfirmDigest() {
return ApiResponse.success(salaryBatchService.computeConfirmDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/sign")
@Operation(summary = "央行第一私钥签名",
description = "传入央行第一私钥进行SM2签名私钥不正确则错误数+1返回带CB_FINAL_前缀的签名值")
public ApiResponse<SalaryConfirmSignatureResult> signConfirm(@Valid @RequestBody SalaryConfirmSignRequest request) {
return ApiResponse.success(salaryBatchService.signConfirm(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId());
}
@PostMapping("/output")
@Operation(summary = "输出交易确认结果",
description = "生成包含批次号、总金额、摘要、央行签名等信息的JSON报文状态为COMPLETED")
public ApiResponse<SalaryConfirmOutputResult> outputConfirm() {
return ApiResponse.success(salaryBatchService.outputConfirm(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -1,72 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest;
import com.yau.digitalrmb.corporatewallet.application.SalaryBatchService;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewCheckResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewDigestResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewExtractResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewOutputResult;
import com.yau.digitalrmb.corporatewallet.application.SalaryReviewSignatureResult;
import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.SalaryReviewSignRequest;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.security.context.AuthContextHolder;
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 javax.annotation.Resource;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/salary/review")
@Tag(name = "模块六-企业发放数字人民币工资 - 步骤2数币复核员审核与权限审批", description = "数币复核员提取审核原文、权限校验、摘要、签名并输出全流程")
public class SalaryReviewController {
@Resource
private SalaryBatchService salaryBatchService;
@PostMapping("/extract")
@Operation(summary = "提取审核原文",
description = "从已发送的工资批次中提取审核原文格式REVIEW|批次号|复核员|总金额|人数|时间戳")
public ApiResponse<SalaryReviewExtractResult> extractReviewText() {
return ApiResponse.success(salaryBatchService.extractReviewText(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/check")
@Operation(summary = "审核与权限校验",
description = "校验单笔限额(≤50000元)、批次限额(≤500000元)、日累计限额(≤1000000元)返回1-通过/2-未通过")
public ApiResponse<SalaryReviewCheckResult> checkPermission() {
return ApiResponse.success(salaryBatchService.checkPermission(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/digest")
@Operation(summary = "生成审核摘要",
description = "对拼接审核原文进行SM3运算只返回摘要值")
public ApiResponse<SalaryReviewDigestResult> computeReviewDigest() {
return ApiResponse.success(salaryBatchService.computeReviewDigest(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
@PostMapping("/sign")
@Operation(summary = "SM2签名复核员私钥",
description = "传入数币复核员私钥进行SM2签名私钥不正确则错误数+1只返回签名值")
public ApiResponse<SalaryReviewSignatureResult> signReview(@Valid @RequestBody SalaryReviewSignRequest request) {
return ApiResponse.success(salaryBatchService.signReview(
InstitutionKeySubject.from(AuthContextHolder.get()), request.getPrivateKey()), traceId());
}
@PostMapping("/output")
@Operation(summary = "输出审核结果",
description = "生成包含批次号、复核员、权限校验、签名等信息的JSON报文")
public ApiResponse<SalaryReviewOutputResult> outputReview() {
return ApiResponse.success(salaryBatchService.outputReview(
InstitutionKeySubject.from(AuthContextHolder.get())), traceId());
}
private String traceId() {
return MDC.get(TraceIdFilter.MDC_KEY);
}
}

@ -1,36 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
@Schema(description = "企业法人提交申请请求")
public class CorporateWalletApplicationRequest {
@NotBlank(message = "企业名称不能为空")
@Schema(description = "企业名称", example = "创新科技公司")
private String corpName;
@NotBlank(message = "统一社会信用代码不能为空")
@Schema(description = "统一社会信用代码", example = "91440101MA5XXXXXX")
private String creditCode;
@NotBlank(message = "法人代表不能为空")
@Schema(description = "法人代表", example = "张三")
private String legalPerson;
@NotBlank(message = "注册资本不能为空")
@Schema(description = "注册资本", example = "10000000.00")
private String capital;
@NotBlank(message = "经营范围不能为空")
@Schema(description = "经营范围", example = "软件开发、技术服务")
private String businessScope;
@NotBlank(message = "申请时间不能为空")
@Schema(description = "申请时间格式yyyyMMddHHmmss", example = "20260801100000")
private String applyTime;
}

@ -1,20 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
@Getter
@Setter
@Schema(description = "远程开立-人脸识别状态更新请求")
public class RemoteFaceRecognitionRequest {
@NotNull(message = "人脸识别状态不能为空")
@Min(value = 1, message = "人脸识别状态只能为1或2")
@Max(value = 2, message = "人脸识别状态只能为1或2")
@Schema(description = "人脸识别状态1=未成功2=已成功", example = "2")
private Integer status;
}

@ -1,32 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
@Schema(description = "远程开立-企业法人在线填写信息请求")
public class RemoteWalletApplicationRequest {
@NotBlank(message = "企业名称不能为空")
@Schema(description = "企业名称", example = "阳光公益基金")
private String corpName;
@NotBlank(message = "统一社会信用代码不能为空")
@Schema(description = "统一社会信用代码", example = "91440101MA6XXXXXX")
private String creditCode;
@NotBlank(message = "法人代表不能为空")
@Schema(description = "法人代表", example = "李四")
private String legalPerson;
@NotBlank(message = "手机号码不能为空")
@Schema(description = "手机号码", example = "13900139000")
private String phone;
@NotBlank(message = "申请时间不能为空")
@Schema(description = "申请时间格式yyyyMMddHHmmss", example = "20260801110000")
private String applyTime;
}

@ -1,14 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@Schema(description = "交易确认央行签名请求")
public class SalaryConfirmSignRequest {
@NotBlank(message = "央行第一私钥不能为空")
@Schema(description = "央行第一私钥(十六进制)", required = true)
private String privateKey;
}

@ -1,14 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@Schema(description = "复核员SM2签名请求")
public class SalaryReviewSignRequest {
@NotBlank(message = "数币复核员私钥不能为空")
@Schema(description = "数币复核员私钥(十六进制)", required = true)
private String privateKey;
}

@ -1,14 +0,0 @@
package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
@Schema(description = "SM2签名请求")
public class SalarySignRequest {
@NotBlank(message = "数币操作员私钥不能为空")
@Schema(description = "数币操作员私钥(十六进制)", required = true)
private String privateKey;
}

@ -4,9 +4,6 @@ import java.math.BigDecimal;
import java.time.Instant;
public final class ExchangeContext {
private final String userName;
private final String idNumber;
private final String phone;
private final String walletId;
private final String walletType;
private final String walletStatus;
@ -28,15 +25,13 @@ public final class ExchangeContext {
private final BigDecimal bankInventoryBalance;
private final String organizationId;
public ExchangeContext(String userName, String idNumber, String phone,
String walletId, String walletType, String walletStatus, BigDecimal walletBalance,
public ExchangeContext(String walletId, String walletType, String walletStatus, BigDecimal walletBalance,
BigDecimal singleLimit, BigDecimal dailyLimit, BigDecimal usedToday,
BigDecimal annualLimit, BigDecimal balanceLimit, Instant contractValidUntil,
String contractId, String walletPublicKey, String bankAccountId, String bankCode,
String bankName, String bankCardNumber, String bankCardLast4,
BigDecimal bankAccountBalance, BigDecimal bankInventoryBalance,
String organizationId) {
this.userName = userName; this.idNumber = idNumber; this.phone = phone;
this.walletId = walletId; this.walletType = walletType; this.walletStatus = walletStatus;
this.walletBalance = walletBalance; this.singleLimit = singleLimit; this.dailyLimit = dailyLimit;
this.annualLimit = annualLimit; this.balanceLimit = balanceLimit; this.contractValidUntil = contractValidUntil;
@ -47,9 +42,6 @@ public final class ExchangeContext {
this.organizationId = organizationId;
}
public String getUserName() { return userName; }
public String getIdNumber() { return idNumber; }
public String getPhone() { return phone; }
public String getWalletId() { return walletId; }
public String getWalletType() { return walletType; }
public String getWalletStatus() { return walletStatus; }

@ -48,7 +48,6 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor
@Override
public ExchangeContext loadContext(ExchangeActor actor) {
Institution institution = loadIssuedInstitution(actor);
UserProfile profile = loadUserProfile(actor);
List<ExchangeContext> values = jdbc.query(
"SELECT w.wallet_id,w.wallet_type,w.status wallet_status,w.balance wallet_balance," +
"c.contract_id,c.single_payment_limit,c.daily_payment_limit,c.annual_payment_limit," +
@ -75,8 +74,7 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor
"c.daily_payment_limit,c.annual_payment_limit,c.balance_limit,c.valid_until," +
"c.daily_counter_date,c.daily_used_amount,cert.public_key,a.account_id," +
"a.bank_code,a.bank_name,a.card_number,a.card_last4,a.balance",
(rs, row) -> new ExchangeContext(profile.userName, profile.idNumber, profile.phone,
rs.getString("wallet_id"), rs.getString("wallet_type"),
(rs, row) -> new ExchangeContext(rs.getString("wallet_id"), rs.getString("wallet_type"),
rs.getString("wallet_status"), rs.getBigDecimal("wallet_balance"),
rs.getBigDecimal("single_payment_limit"), rs.getBigDecimal("daily_payment_limit"),
rs.getBigDecimal("used_today"), rs.getBigDecimal("annual_payment_limit"),
@ -92,21 +90,6 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor
return values.get(0);
}
private UserProfile loadUserProfile(ExchangeActor actor) {
List<UserProfile> values = jdbc.query(
"SELECT user_name,COALESCE(input_id_number,id_number) id_number," +
"COALESCE(input_phone,phone) phone FROM wallet_application " +
"WHERE user_id=? AND school_id=? AND class_id=? AND status='SUBMITTED' " +
"AND deleted=FALSE ORDER BY created_at DESC LIMIT 1",
(rs, row) -> new UserProfile(rs.getString("user_name"), rs.getString("id_number"),
rs.getString("phone")),
actor.getUserId(), actor.getSchoolId(), actor.getClassId());
if (values.isEmpty()) {
throw validation("请先完成个人数字钱包开通实验的用户申请信息");
}
return values.get(0);
}
@Override
public void initializeContext(ExchangeActor actor) {
walletPrerequisites.ensureForSubject(new InstitutionKeySubject(
@ -450,18 +433,6 @@ public class JdbcExchangeResourceRepository implements ExchangeResourceRepositor
}
}
private static final class UserProfile {
private final String userName;
private final String idNumber;
private final String phone;
private UserProfile(String userName, String idNumber, String phone) {
this.userName = userName;
this.idNumber = idNumber;
this.phone = phone;
}
}
private static final class AccountSnapshot {
private final BigDecimal balance;
private final BigDecimal frozen;

@ -52,7 +52,7 @@ public class CurrencyGenerationRequestService {
request = CurrencyGenerationRequest.restore(requestId, input.getIdentifierApplicationId(),
input.getInstitutionIdentifier(), input.getFullInstitutionIdentifier(), input.getAmount(),
input.getDeliveryNodeCode(), input.getRequestTimestamp(), input.getRequestOriginalText(),
null, null, null, null, null, input.getStatus());
null, null, null, null, input.getStatus());
}
String digest = cryptography.sm3(input.getRequestOriginalText());
CurrencyGenerationRequest.Status expected = request.getStatus();
@ -77,7 +77,7 @@ public class CurrencyGenerationRequestService {
"签名必须使用当前实训主体的商业银行第二私钥,本次实训第"
+ errorSequence + "次错误");
}
invokeDomain(() -> request.recordSignature(InstitutionKeyService.BANK_SECOND_KEY, signature, privateKey));
invokeDomain(() -> request.recordSignature(InstitutionKeyService.BANK_SECOND_KEY, signature));
repository.update(request, expected, "SIGN", "使用商业银行第二私钥完成SM2签名", operator);
return new CurrencyRequestResult(request);
}
@ -127,9 +127,7 @@ public class CurrencyGenerationRequestService {
@Transactional(readOnly = true)
public CurrencyRequestResult page(InstitutionKeySubject subject) {
return repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.map(CurrencyRequestResult::new)
.orElseGet(CurrencyRequestResult::new);
return new CurrencyRequestResult(requireCurrentIdentifierRequest(subject));
}
@Transactional(readOnly = true)

@ -13,8 +13,8 @@ import java.math.BigDecimal;
public class CurrencyGenerationVerificationResult {
@Schema(description = "货币生成验证记录编号;尚未接收时为空", example = "1900000000000000151")
private final Long verificationId;
@Schema(description = "关联的货币生成请求编号")
private final Long requestId;
@Schema(description = "关联的货币生成请求编号", example = "1900000000000000101")
private final long requestId;
@Schema(description = "当前验证状态", example = "VERIFIED")
private final String status;
@Schema(description = "央行收到的货币生成请求报文")
@ -51,25 +51,6 @@ public class CurrencyGenerationVerificationResult {
private final String confirmationTime;
@Schema(description = "央行打包完成的确认报文;尚未点击打包报文时为空")
private final String confirmationMessage;
@Schema(description = "用户提交的商业银行第二公钥;尚未验签时为空")
private final String submittedPublicKey;
@Schema(description = "用户提交的央行第一私钥;尚未确权时为空")
private final String submittedPrivateKey;
@Schema(description = "用户在步骤三运算摘要时输入的请求原文;尚未运算时为空")
private final String userInputOriginalText;
public CurrencyGenerationVerificationResult() {
verificationId = null; requestId = null; status = null;
requestMessage = null; requestOriginalText = null;
requestDigest = null; bankSignature = null; bankKeyId = null;
amount = null; signatureValid = null; recomputedDigest = null;
digestMatches = null; totalQuota = null; usedQuota = null;
remainingQuota = null; quotaSufficient = null;
centralBankKeyId = null; centralBankSignature = null;
confirmationTime = null; confirmationMessage = null;
submittedPublicKey = null; submittedPrivateKey = null;
userInputOriginalText = null;
}
public CurrencyGenerationVerificationResult(CurrencyGenerationVerification v) {
verificationId = v.getId(); requestId = v.getRequestId(); status = v.getStatus().name();
@ -80,12 +61,10 @@ public class CurrencyGenerationVerificationResult {
remainingQuota = v.getRemainingQuota(); quotaSufficient = v.getQuotaSufficient();
centralBankKeyId = v.getCentralBankKeyId(); centralBankSignature = v.getCentralBankSignature();
confirmationTime = v.getConfirmationTime(); confirmationMessage = v.getConfirmationMessage();
submittedPublicKey = v.getSubmittedPublicKey(); submittedPrivateKey = v.getSubmittedPrivateKey();
userInputOriginalText = v.getUserInputOriginalText();
}
public Long getVerificationId() { return verificationId; }
public Long getRequestId() { return requestId; }
public long getRequestId() { return requestId; }
public String getStatus() { return status; }
public String getRequestMessage() { return requestMessage; }
public String getRequestOriginalText() { return requestOriginalText; }
@ -104,7 +83,4 @@ public class CurrencyGenerationVerificationResult {
public String getCentralBankSignature() { return centralBankSignature; }
public String getConfirmationTime() { return confirmationTime; }
public String getConfirmationMessage() { return confirmationMessage; }
public String getSubmittedPublicKey() { return submittedPublicKey; }
public String getSubmittedPrivateKey() { return submittedPrivateKey; }
public String getUserInputOriginalText() { return userInputOriginalText; }
}

@ -51,13 +51,13 @@ public class CurrencyGenerationVerificationService {
return new CurrencyGenerationVerificationResult(CurrencyGenerationVerification.restore(id, requestId,
value.getRequestMessage(), value.getRequestOriginalText(), value.getRequestDigest(),
value.getBankSignature(), value.getBankKeyId(), value.getAmount(), null, null, null,
null, null, null, null, null, null, null, null, null, null, null, value.getStatus()));
null, null, null, null, null, null, null, null, value.getStatus()));
}
@Transactional
public CurrencyGenerationVerificationResult verifySignature(String publicKey,
public CurrencyGenerationVerificationResult verifySignature(long id, String publicKey,
InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = requireLatest(subject);
CurrencyGenerationVerification value = require(id, subject);
if (!keyService.matchesCommercialBankPublicKey(subject, publicKey)) {
throw keyError(subject, "验签必须使用当前实训主体的商业银行第二公钥");
}
@ -67,7 +67,7 @@ public class CurrencyGenerationVerificationService {
if (!valid) {
throw keyError(subject, "商业银行第二公钥验签未通过");
}
invoke(() -> value.verifySignature(valid, publicKey));
invoke(() -> value.verifySignature(valid));
repository.update(value, expected, "VERIFY_SIGNATURE", "使用商业银行第二公钥验证SM2签名", operator);
return new CurrencyGenerationVerificationResult(value);
}
@ -78,14 +78,14 @@ public class CurrencyGenerationVerificationService {
CurrencyGenerationVerification value = require(id, subject);
CurrencyGenerationVerification.Status expected = value.getStatus();
String digest = cryptography.sm3(requestOriginalText);
invoke(() -> value.calculateDigest(digest, requestOriginalText));
invoke(() -> value.calculateDigest(digest));
repository.update(value, expected, "CALCULATE_DIGEST", "对用户输入的请求原文计算摘要一", operator);
return new CurrencyDigestResult(digest);
}
@Transactional
public CurrencyGenerationVerificationResult matchDigest(InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = requireLatest(subject);
public CurrencyGenerationVerificationResult matchDigest(long id, InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = require(id, subject);
CurrencyGenerationVerification.Status expected = value.getStatus();
try {
value.matchDigest();
@ -99,8 +99,8 @@ public class CurrencyGenerationVerificationService {
}
@Transactional
public CurrencyQuotaVerificationResult verifyQuota(InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = requireLatest(subject);
public CurrencyQuotaVerificationResult verifyQuota(long id, InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = require(id, subject);
if (value.getStatus().ordinal() >= CurrencyGenerationVerification.Status.QUOTA_VERIFIED.ordinal()) {
return new CurrencyQuotaVerificationResult(value);
}
@ -129,15 +129,15 @@ public class CurrencyGenerationVerificationService {
return new CurrencyConfirmationSignatureResult(value.getCentralBankSignature());
}
String time = LocalDateTime.now().format(TIME);
invoke(() -> value.confirm(InstitutionKeyService.CENTRAL_FIRST_KEY, signature, time, privateKey));
invoke(() -> value.confirm(InstitutionKeyService.CENTRAL_FIRST_KEY, signature, time));
repository.update(value, expected, "CONFIRM", "校验用户输入请求摘要并使用央行第一私钥完成签名确权", operator);
return new CurrencyConfirmationSignatureResult(value.getCentralBankSignature());
}
@Transactional
public CurrencyGenerationVerificationResult packageResponse(InstitutionKeySubject subject,
public CurrencyGenerationVerificationResult packageResponse(long id, InstitutionKeySubject subject,
String operator) {
CurrencyGenerationVerification value = requireLatest(subject);
CurrencyGenerationVerification value = require(id, subject);
CurrencyGenerationVerification.Status expected = value.getStatus();
CurrencyGenerationRequest request = requestService.requireReceived(value.getRequestId(), subject);
String message = "{\"requestId\":\"" + CurrencyGenerationRequestService.displayRequestId(request)
@ -150,8 +150,8 @@ public class CurrencyGenerationVerificationService {
}
@Transactional
public CurrencyGenerationVerificationResult returnResponse(InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = requireLatest(subject);
public CurrencyGenerationVerificationResult returnResponse(long id, InstitutionKeySubject subject, String operator) {
CurrencyGenerationVerification value = require(id, subject);
CurrencyGenerationVerification.Status expected = value.getStatus();
invoke(value::returnResponse);
repository.update(value, expected, "RETURN_RESPONSE", "向商业银行返回确认报文", operator);
@ -184,10 +184,11 @@ public class CurrencyGenerationVerificationService {
@Transactional(readOnly = true)
public CurrencyGenerationVerificationResult current(InstitutionKeySubject subject) {
return repository.findLatestBySubject(subject.getUserId(),
CurrencyGenerationVerification value = repository.findLatestBySubject(subject.getUserId(),
subject.getSchoolId(), subject.getClassId())
.map(CurrencyGenerationVerificationResult::new)
.orElseGet(CurrencyGenerationVerificationResult::new);
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"当前实训主体没有未逻辑删除的步骤三验证记录"));
return new CurrencyGenerationVerificationResult(value);
}
@Transactional(readOnly = true)
@ -201,13 +202,6 @@ public class CurrencyGenerationVerificationService {
"当前实训主体下的步骤三验证记录不存在"));
}
private CurrencyGenerationVerification requireLatest(InstitutionKeySubject subject) {
return repository.findLatestBySubject(subject.getUserId(),
subject.getSchoolId(), subject.getClassId())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"当前实训主体没有未逻辑删除的步骤三验证记录,请先接收步骤二请求报文"));
}
private BusinessException keyError(InstitutionKeySubject subject, String message) {
return scoreError(subject, message);
}

@ -1,64 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
@Getter
@Schema(description = "币串ID生成规则")
public class CurrencyIdRuleResult {
@Schema(description = "DC前缀", example = "DC")
private final String prefix;
@Schema(description = "币串ID生成规则", example = "批次号+枚序号+随机校验码")
private final String idRule;
@Schema(description = "当前批次号", example = "20260819_001")
private final String batchNumber;
@Schema(description = "币串总枚数", example = "28")
private final Integer totalCount;
@Schema(description = "操作状态", example = "RULE_CONFIRMED")
private final String status;
@Schema(description = "币串ID规则明细")
private final List<RuleItem> ruleItems;
public CurrencyIdRuleResult(String prefix, String idRule, String batchNumber,
Integer totalCount, String status) {
this.prefix = prefix;
this.idRule = idRule;
this.batchNumber = batchNumber;
this.totalCount = totalCount;
this.status = status;
this.ruleItems = buildRuleItems(batchNumber, totalCount);
}
private List<RuleItem> buildRuleItems(String batchNumber, Integer totalCount) {
String lastSeq = totalCount != null ? String.format("%03d", totalCount) : "028";
String serialExample = "001、002 ... " + lastSeq;
String checkCodeExample = "A1B2C3";
String fullIdExample = "DC_" + batchNumber + "_001_" + checkCodeExample;
return Arrays.asList(
new RuleItem("批次号", batchNumber, "年月日_当日批次序号"),
new RuleItem("枚序号", serialExample, "该批次内的唯一序号"),
new RuleItem("随机校验码", checkCodeExample, "6位随机字符用于防碰撞和校验"),
new RuleItem("完整币串ID", fullIdExample, "DC_前缀 + 批次号 + 枚序号 + 校验码")
);
}
@Getter
@Schema(description = "币串ID规则明细项")
public static class RuleItem {
@Schema(description = "字段名", example = "批次号")
private final String field;
@Schema(description = "示例", example = "20260819_001")
private final String example;
@Schema(description = "说明", example = "年月日_当日批次序号")
private final String description;
public RuleItem(String field, String example, String description) {
this.field = field;
this.example = example;
this.description = description;
}
}
}

@ -12,9 +12,9 @@ import java.math.BigDecimal;
@Schema(description = "商业银行货币生成请求页面数据尚未完成的阶段字段返回null")
public class CurrencyRequestResult {
@Schema(description = "货币生成请求编号", example = "1900000000000000101")
private final Long requestId;
private final long requestId;
@Schema(description = "关联的机构标识申请编号", example = "1900000000000000001")
private final Long identifierApplicationId;
private final long identifierApplicationId;
@Schema(description = "当前处理状态", example = "SIGNED")
private final String status;
@Schema(description = "简洁机构标识", example = "ORG_3A4B5C6D7E8F")
@ -43,32 +43,9 @@ public class CurrencyRequestResult {
private final String bankSignature;
@Schema(description = "发送给央行的货币生成请求报文;尚未组装时为空")
private final String requestMessage;
@Schema(description = "用户提交的商业银行第二私钥;尚未签名时为空")
private final String submittedPrivateKey;
@Schema(description = "央行接收状态", example = "NOT_RECEIVED")
private final String centralBankReceiveStatus;
public CurrencyRequestResult() {
this.requestId = null;
this.identifierApplicationId = null;
this.status = null;
this.institutionIdentifier = null;
this.fullInstitutionIdentifier = null;
this.amount = null;
this.deliveryNodeCode = null;
this.requestTimestamp = null;
this.requestOriginalText = null;
this.digestAlgorithm = null;
this.requestDigest = null;
this.signatureAlgorithm = null;
this.signatureHashAlgorithm = null;
this.signingKeyId = null;
this.bankSignature = null;
this.requestMessage = null;
this.submittedPrivateKey = null;
this.centralBankReceiveStatus = null;
}
public CurrencyRequestResult(CurrencyGenerationRequest request) {
this.requestId = request.getId();
this.identifierApplicationId = request.getIdentifierApplicationId();
@ -86,13 +63,12 @@ public class CurrencyRequestResult {
this.signingKeyId = request.getSigningKeyId();
this.bankSignature = request.getBankSignature();
this.requestMessage = request.getRequestMessage();
this.submittedPrivateKey = request.getSubmittedPrivateKey();
this.centralBankReceiveStatus = request.getStatus() == CurrencyGenerationRequest.Status.RECEIVED
? "RECEIVED" : "NOT_RECEIVED";
}
public Long getRequestId() { return requestId; }
public Long getIdentifierApplicationId() { return identifierApplicationId; }
public long getRequestId() { return requestId; }
public long getIdentifierApplicationId() { return identifierApplicationId; }
public String getStatus() { return status; }
public String getInstitutionIdentifier() { return institutionIdentifier; }
public String getFullInstitutionIdentifier() { return fullInstitutionIdentifier; }
@ -107,6 +83,5 @@ public class CurrencyRequestResult {
public String getSigningKeyId() { return signingKeyId; }
public String getBankSignature() { return bankSignature; }
public String getRequestMessage() { return requestMessage; }
public String getSubmittedPrivateKey() { return submittedPrivateKey; }
public String getCentralBankReceiveStatus() { return centralBankReceiveStatus; }
}

@ -19,9 +19,7 @@ public class QuotaRequestSourceDataResult {
this.transactionIdentifier = value.getTransactionIdentifier();
}
public BigDecimal getAmount() { return amount; }
public String getInstitutionIdentifier() { return institutionIdentifier; }
public String getTransactionIdentifier() { return transactionIdentifier; }
}

@ -1,76 +0,0 @@
package com.yau.digitalrmb.institutionidentity.application;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
@Getter
@Schema(description = "标准面额币串规则")
public class StandardCurrencyRuleResult {
@Schema(description = "DC前缀", example = "DC")
private final String prefix;
@Schema(description = "币串初始状态", example = "待生效")
private final String currencyStatus;
@Schema(description = "当前批次号", example = "20260819_001")
private final String batchNumber;
@Schema(description = "币串总枚数", example = "28")
private final Integer totalCount;
@Schema(description = "操作状态", example = "RULE_CONFIRMED")
private final String status;
@Schema(description = "标准面额币串规则明细以第1枚币串为例")
private final List<RuleItem> ruleItems;
public StandardCurrencyRuleResult(String prefix, String currencyStatus, String batchNumber,
Integer totalCount, String status,
BigDecimal firstDenomination, String institutionIdentifier,
String bankSignature, String centralBankSignatureSegment) {
this.prefix = prefix;
this.currencyStatus = currencyStatus;
this.batchNumber = batchNumber;
this.totalCount = totalCount;
this.status = status;
this.ruleItems = buildRuleItems(prefix, batchNumber, firstDenomination,
institutionIdentifier, bankSignature, centralBankSignatureSegment, currencyStatus);
}
private List<RuleItem> buildRuleItems(String prefix, String batchNumber,
BigDecimal firstDenomination, String institutionIdentifier,
String bankSignature, String centralBankSignatureSegment,
String currencyStatus) {
String checkCodeExample = "A1B2C3";
String currencyIdExample = "DC_" + batchNumber + "_001_" + checkCodeExample;
String denominationStr = firstDenomination != null ? firstDenomination.setScale(2).toPlainString() : "100.00";
String orgId = institutionIdentifier != null ? institutionIdentifier : "ORG_3A4B5C6D7E8F";
String bankSig = bankSignature != null ? bankSignature : "SIG_8A7B9C2D4F1E3A5B7C9D2E4F6A8B1C3D5E7F8A9B0C1D2E3F4A5B6C7D8E9F0A";
String cbSig = centralBankSignatureSegment != null ? centralBankSignatureSegment : "CB_CTRL_9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F7A8";
return Arrays.asList(
new RuleItem("前缀", prefix, "固定标识,代表数字货币"),
new RuleItem("币串ID", currencyIdExample, "包含批次号001和枚序号001确保全球唯一"),
new RuleItem("标准面额", denominationStr, "拆解后的标准面额,非总额"),
new RuleItem("机构标识", orgId, "关联发起机构,用于溯源"),
new RuleItem("银行签名段", bankSig, "复用步骤八的银行签名段"),
new RuleItem("央行签名段", cbSig, "复用步骤七的央行签名段"),
new RuleItem("币串状态", currencyStatus, "发行时为待生效,央行确权后变为可用")
);
}
@Getter
@Schema(description = "标准面额币串规则明细项")
public static class RuleItem {
@Schema(description = "币串字段", example = "前缀")
private final String field;
@Schema(description = "示例值以第1枚币串为例", example = "DC")
private final String example;
@Schema(description = "说明", example = "固定标识,代表数字货币")
private final String description;
public RuleItem(String field, String example, String description) {
this.field = field;
this.example = example;
this.description = description;
}
}
}

@ -96,11 +96,21 @@ public class StandardCurrencyService {
}
@Transactional
public CurrencyIdRuleResult confirmIdRule(InstitutionKeySubject subject, String operator) {
public StandardCurrencyBatchResult confirmRule(String prefix, String idRule, String currencyStatus,
InstitutionKeySubject subject, String operator) {
StandardCurrencyBatchEntity batch = requireLatest(subject);
if (batch.getDenominationSummary() == null || batch.getTotalAmount() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先执行面额拆解");
}
if (!"DC".equals(prefix == null ? null : prefix.trim().toUpperCase())) {
throw inputError(subject, "币串前缀必须为DC");
}
if (!ID_RULE.equals(idRule == null ? null : idRule.trim())) {
throw inputError(subject, "币串ID规则必须为“批次号+枚序号+随机校验码”");
}
if (!"待生效".equals(currencyStatus == null ? null : currencyStatus.trim())) {
throw inputError(subject, "币串初始状态必须为“待生效”");
}
int updated = batchMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyBatchEntity>()
.eq(StandardCurrencyBatchEntity::getId, batch.getId())
.eq(StandardCurrencyBatchEntity::getStatus, batch.getStatus())
@ -112,25 +122,7 @@ public class StandardCurrencyService {
.set(StandardCurrencyBatchEntity::getUpdatedAt, LocalDateTime.now())
.set(StandardCurrencyBatchEntity::getUpdatedBy, operator));
if (updated != 1) throw stateChanged();
StandardCurrencyBatchEntity latest = requireLatest(subject);
return new CurrencyIdRuleResult("DC", ID_RULE,
latest.getBatchNumber(), latest.getTotalCount(), latest.getStatus());
}
@Transactional
public StandardCurrencyRuleResult confirmCurrencyRule(InstitutionKeySubject subject, String operator) {
StandardCurrencyBatchEntity batch = requireLatest(subject);
if (batch.getPrefix() == null || batch.getIdRule() == null) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先确认币串ID生成规则");
}
// 从步骤八获取真实数据
QuotaControlBitEntity step8 = requireStepEight(subject);
List<DenominationBreakdownResult> breakdown = decode(batch.getDenominationSummary());
BigDecimal firstDenomination = breakdown.isEmpty() ? new BigDecimal("100.00") : breakdown.get(0).getDenomination();
return new StandardCurrencyRuleResult(batch.getPrefix(), batch.getCurrencyStatus(),
batch.getBatchNumber(), batch.getTotalCount(), batch.getStatus(),
firstDenomination, step8.getInstitutionIdentifier(),
step8.getBankSignature(), step8.getCentralBankSignatureSegment());
return result(requireLatest(subject));
}
@Transactional

@ -143,64 +143,21 @@ public class TransactionInformationIdentifierService {
return new TransactionIdentifierAssemblyResult(value);
}
// @Transactional
// public QuotaRequestSourceDataResult quotaRequestSourceData(InstitutionKeySubject subject) {
// TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(),
// subject.getSchoolId(), subject.getClassId()).orElseThrow(() ->
// new BusinessException(ErrorCode.VALIDATION_ERROR,
// "步骤四尚未完成交易信息标识组装,不能查询步骤五前序数据"));
// if (value.getStatus().ordinal() < TransactionInformationIdentifier.Status.ASSEMBLED.ordinal()
// || value.getTransactionIdentifier() == null
// || !value.getTransactionIdentifier().matches("TXN_[0-9A-F]{64}")) {
// throw new BusinessException(ErrorCode.VALIDATION_ERROR,
// "步骤四尚未生成交易信息标识,请先完成步骤四");
// }
// return new QuotaRequestSourceDataResult(value);
// }
//@Transactional(readOnly = true) // 既然只查询,用 readOnly 优化
//public QuotaRequestSourceDataResult quotaRequestSourceData(InstitutionKeySubject subject) {
// // 1. 尝试查询,查不到则直接返回空结果
// TransactionInformationIdentifier value = repository.findLatestBySubject(
// subject.getUserId(), subject.getSchoolId(), subject.getClassId())
// .orElse(null);
//
// // 2. 如果查不到,或者状态/标识不满足条件,返回一个“空”结果对象
// if (value == null ||
// value.getStatus().ordinal() < TransactionInformationIdentifier.Status.ASSEMBLED.ordinal() ||
// value.getTransactionIdentifier() == null ||
// !value.getTransactionIdentifier().matches("TXN_[0-9A-F]{64}")) {
//
// // 这里返回一个代表“未就绪”的空对象,前端通过检查关键字段(如 transactionIdentifier是否为 null 来判断
// return new QuotaRequestSourceDataResult(null); // 假设你在 Result 类里增加了 static empty() 方法
// }
//
// // 3. 条件满足,正常返回
// return new QuotaRequestSourceDataResult(value);
//}
@Transactional(readOnly = true)
public QuotaRequestSourceDataResult quotaRequestSourceData(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = repository.findLatestBySubject(
subject.getUserId(),
subject.getSchoolId(),
subject.getClassId()
).orElse(null);
// 无记录 → 返回 null
if (value == null) {
return null;
}
// 状态或标识不满足条件 → 返回 null
if (value.getStatus().ordinal() < TransactionInformationIdentifier.Status.ASSEMBLED.ordinal()
|| value.getTransactionIdentifier() == null
|| !value.getTransactionIdentifier().matches("TXN_[0-9A-F]{64}")) {
return null;
@Transactional
public QuotaRequestSourceDataResult quotaRequestSourceData(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(),
subject.getSchoolId(), subject.getClassId()).orElseThrow(() ->
new BusinessException(ErrorCode.VALIDATION_ERROR,
"步骤四尚未完成交易信息标识组装,不能查询步骤五前序数据"));
if (value.getStatus().ordinal() < TransactionInformationIdentifier.Status.ASSEMBLED.ordinal()
|| value.getTransactionIdentifier() == null
|| !value.getTransactionIdentifier().matches("TXN_[0-9A-F]{64}")) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"步骤四尚未生成交易信息标识,请先完成步骤四");
}
return new QuotaRequestSourceDataResult(value);
}
// 正常返回
return new QuotaRequestSourceDataResult(value);
}
@Transactional
public QuotaRequestDigestResult digestQuotaRequest(String originalText,
InstitutionKeySubject subject,
@ -358,31 +315,17 @@ public QuotaRequestSourceDataResult quotaRequestSourceData(InstitutionKeySubject
return new QuotaVerificationResult(value);
}
// @Transactional(readOnly = true)
// public QuotaRequestMessageResult quotaRequestMessageDetail(InstitutionKeySubject subject) {
// return new QuotaRequestMessageResult(requireLatest(subject));
// }
@Transactional(readOnly = true)
public QuotaRequestMessageResult quotaRequestMessageDetail(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = repository.findLatestBySubject(
subject.getUserId(),
subject.getSchoolId(),
subject.getClassId()
).orElse(null);
if (value == null) {
return null; // 返回 null前端收到 data: nullHTTP 200
@Transactional(readOnly = true)
public QuotaRequestMessageResult quotaRequestMessageDetail(InstitutionKeySubject subject) {
return new QuotaRequestMessageResult(requireLatest(subject));
}
return new QuotaRequestMessageResult(value);
}
@Transactional(readOnly = true)
public TransactionInformationIdentifierResult detail(InstitutionKeySubject subject) {
TransactionInformationIdentifier value = repository.findLatestBySubject(
subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null); // 查不到就返回 null
// 注意:这里 value 可能为 null
return value != null ? new TransactionInformationIdentifierResult(value) : null;
TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(),
subject.getSchoolId(), subject.getClassId()).orElseThrow(() ->
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前没有交易信息标识记录"));
return new TransactionInformationIdentifierResult(value);
}
private CurrencyGenerationVerificationEntity requireCompletedVerification(InstitutionKeySubject subject) {
@ -435,21 +378,18 @@ public QuotaRequestMessageResult quotaRequestMessageDetail(InstitutionKeySubject
return value;
}
// private TransactionInformationIdentifier requireLatest(InstitutionKeySubject subject) {
// return repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
// .orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
// "当前没有额度请求记录"));
// }
private TransactionInformationIdentifier requireLatest(InstitutionKeySubject subject) {
private TransactionInformationIdentifier requireLatest(InstitutionKeySubject subject) {
return repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
.orElse(null);
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"当前没有额度请求记录"));
}
private TransactionInformationIdentifier requireStatus(InstitutionKeySubject subject,
TransactionInformationIdentifier.Status status,
String message) {
return repository.findLatestBySubjectAndStatus(subject.getUserId(), subject.getSchoolId(),
subject.getClassId(), status).orElse(null);
subject.getClassId(), status).orElseThrow(() ->
new BusinessException(ErrorCode.VALIDATION_ERROR, message));
}
private BusinessException keyError(InstitutionKeySubject subject, String message) {

@ -19,14 +19,12 @@ public class CurrencyGenerationRequest {
private String bankSignature;
private String signingKeyId;
private String requestMessage;
private String submittedPrivateKey;
private Status status;
private CurrencyGenerationRequest(Long id, long identifierApplicationId, String institutionIdentifier,
String fullInstitutionIdentifier, BigDecimal amount, String deliveryNodeCode,
String requestTimestamp, String requestOriginalText, String requestDigest,
String bankSignature, String signingKeyId, String requestMessage,
String submittedPrivateKey, Status status) {
String bankSignature, String signingKeyId, String requestMessage, Status status) {
this.id = id;
this.identifierApplicationId = identifierApplicationId;
this.institutionIdentifier = institutionIdentifier;
@ -39,7 +37,6 @@ public class CurrencyGenerationRequest {
this.bankSignature = bankSignature;
this.signingKeyId = signingKeyId;
this.requestMessage = requestMessage;
this.submittedPrivateKey = submittedPrivateKey;
this.status = status;
}
@ -70,7 +67,7 @@ public class CurrencyGenerationRequest {
+ deliveryNodeCode + "|" + requestTimestamp;
return new CurrencyGenerationRequest(null, identifierApplicationId, institutionIdentifier,
fullInstitutionIdentifier, normalizedAmount, deliveryNodeCode, requestTimestamp,
originalText, null, null, null, null, null, Status.CONCATENATED);
originalText, null, null, null, null, Status.CONCATENATED);
}
public static CurrencyGenerationRequest restore(Long id, long identifierApplicationId,
@ -78,10 +75,10 @@ public class CurrencyGenerationRequest {
BigDecimal amount, String deliveryNodeCode, String requestTimestamp,
String requestOriginalText, String requestDigest,
String bankSignature, String signingKeyId,
String requestMessage, String submittedPrivateKey, Status status) {
String requestMessage, Status status) {
return new CurrencyGenerationRequest(id, identifierApplicationId, institutionIdentifier,
fullInstitutionIdentifier, amount, deliveryNodeCode, requestTimestamp, requestOriginalText,
requestDigest, bankSignature, signingKeyId, requestMessage, submittedPrivateKey, status);
requestDigest, bankSignature, signingKeyId, requestMessage, status);
}
public void concatenate() {
@ -122,7 +119,7 @@ public class CurrencyGenerationRequest {
status = Status.DIGESTED;
}
public void recordSignature(String keyId, String signature, String submittedPrivateKey) {
public void recordSignature(String keyId, String signature) {
if (requestDigest == null) {
throw new IllegalStateException("请先完成SM3摘要计算");
}
@ -131,7 +128,6 @@ public class CurrencyGenerationRequest {
}
signingKeyId = keyId;
bankSignature = signature;
this.submittedPrivateKey = submittedPrivateKey;
requestMessage = null;
status = Status.SIGNED;
}
@ -179,6 +175,5 @@ public class CurrencyGenerationRequest {
public String getBankSignature() { return bankSignature; }
public String getSigningKeyId() { return signingKeyId; }
public String getRequestMessage() { return requestMessage; }
public String getSubmittedPrivateKey() { return submittedPrivateKey; }
public Status getStatus() { return status; }
}

@ -27,9 +27,6 @@ public class CurrencyGenerationVerification {
private String centralBankSignature;
private String confirmationTime;
private String confirmationMessage;
private String submittedPublicKey;
private String submittedPrivateKey;
private String userInputOriginalText;
private Status status;
private CurrencyGenerationVerification(Long id, long requestId, String requestMessage,
@ -40,8 +37,7 @@ public class CurrencyGenerationVerification {
BigDecimal usedQuota, BigDecimal remainingQuota,
Boolean quotaSufficient, String centralBankKeyId,
String centralBankSignature, String confirmationTime,
String confirmationMessage, String submittedPublicKey,
String submittedPrivateKey, String userInputOriginalText, Status status) {
String confirmationMessage, Status status) {
this.id = id;
this.requestId = requestId;
this.requestMessage = requestMessage;
@ -61,9 +57,6 @@ public class CurrencyGenerationVerification {
this.centralBankSignature = centralBankSignature;
this.confirmationTime = confirmationTime;
this.confirmationMessage = confirmationMessage;
this.submittedPublicKey = submittedPublicKey;
this.submittedPrivateKey = submittedPrivateKey;
this.userInputOriginalText = userInputOriginalText;
this.status = status;
}
@ -77,7 +70,7 @@ public class CurrencyGenerationVerification {
}
return new CurrencyGenerationVerification(null, requestId, requestMessage, requestOriginalText,
requestDigest, bankSignature, bankKeyId, amount, null, null, null, null, null, null,
null, null, null, null, null, null, null, null, Status.RECEIVED);
null, null, null, null, null, Status.RECEIVED);
}
public static CurrencyGenerationVerification restore(Long id, long requestId, String requestMessage,
@ -88,14 +81,11 @@ public class CurrencyGenerationVerification {
BigDecimal usedQuota, BigDecimal remainingQuota,
Boolean quotaSufficient, String centralBankKeyId,
String centralBankSignature, String confirmationTime,
String confirmationMessage, String submittedPublicKey,
String submittedPrivateKey, String userInputOriginalText,
Status status) {
String confirmationMessage, Status status) {
return new CurrencyGenerationVerification(id, requestId, requestMessage, requestOriginalText, requestDigest,
bankSignature, bankKeyId, amount, signatureValid, recomputedDigest, digestMatches, totalQuota,
usedQuota, remainingQuota, quotaSufficient, centralBankKeyId, centralBankSignature,
confirmationTime, confirmationMessage, submittedPublicKey, submittedPrivateKey,
userInputOriginalText, status);
confirmationTime, confirmationMessage, status);
}
public void receiveAgain() {
@ -110,26 +100,21 @@ public class CurrencyGenerationVerification {
centralBankSignature = null;
confirmationTime = null;
confirmationMessage = null;
submittedPublicKey = null;
submittedPrivateKey = null;
userInputOriginalText = null;
status = Status.RECEIVED;
}
public void verifySignature(boolean valid, String publicKey) {
public void verifySignature(boolean valid) {
requireBeforeDigestMatch("第二公钥SM2验证");
if (!valid) throw new IllegalArgumentException("商业银行SM2签名验证未通过");
signatureValid = true;
submittedPublicKey = publicKey;
status = recomputedDigest == null ? Status.SIGNATURE_VERIFIED : Status.DIGEST_CALCULATED;
}
public void calculateDigest(String digest, String userInputOriginalText) {
public void calculateDigest(String digest) {
if (digest == null || !digest.matches("[0-9A-F]{64}")) {
throw new IllegalArgumentException("重新计算的摘要一格式不正确");
}
recomputedDigest = digest;
this.userInputOriginalText = userInputOriginalText;
digestMatches = null;
if (isBefore(Status.DIGEST_CALCULATED)) {
status = Status.DIGEST_CALCULATED;
@ -160,7 +145,7 @@ public class CurrencyGenerationVerification {
}
}
public void confirm(String keyId, String signature, String time, String privateKey) {
public void confirm(String keyId, String signature, String time) {
if (!Boolean.TRUE.equals(quotaSufficient)) {
throw new IllegalStateException("请先完成额度检查");
}
@ -170,7 +155,6 @@ public class CurrencyGenerationVerification {
centralBankKeyId = keyId;
centralBankSignature = signature;
confirmationTime = time;
submittedPrivateKey = privateKey;
status = Status.CONFIRMED;
}
@ -227,8 +211,5 @@ public class CurrencyGenerationVerification {
public String getCentralBankSignature() { return centralBankSignature; }
public String getConfirmationTime() { return confirmationTime; }
public String getConfirmationMessage() { return confirmationMessage; }
public String getSubmittedPublicKey() { return submittedPublicKey; }
public String getSubmittedPrivateKey() { return submittedPrivateKey; }
public String getUserInputOriginalText() { return userInputOriginalText; }
public Status getStatus() { return status; }
}

@ -168,22 +168,9 @@ public class TransactionInformationIdentifier {
if (quotaRequestId == null || !quotaRequestId.matches("QR_\\d{8}_\\d{3}")) {
throw new IllegalArgumentException("额度请求编号格式不正确");
}
if (quotaOriginalText == null || quotaOriginalText.trim().isEmpty()) {
throw new IllegalArgumentException("额度请求原文不能为空");
}
String[] parts = quotaOriginalText.split("\\|", -1);
if (parts.length != 3) {
throw new IllegalArgumentException("额度请求原文格式必须为:金额|机构标识|交易信息标识");
}
BigDecimal inputAmount;
try {
inputAmount = new BigDecimal(parts[0]);
} catch (NumberFormatException exception) {
throw new IllegalArgumentException("额度请求原文中的金额格式不正确");
}
if (inputAmount.compareTo(amount) != 0
|| !institutionIdentifier.equals(parts[1])
|| !transactionIdentifier.equals(parts[2])) {
String expectedOriginalText = amount.toPlainString() + "|" + institutionIdentifier
+ "|" + transactionIdentifier;
if (!expectedOriginalText.equals(quotaOriginalText)) {
throw new IllegalArgumentException("额度请求原文与步骤四数据不一致");
}
this.quotaRequestId = quotaRequestId;

@ -23,7 +23,6 @@ public class CurrencyGenerationRequestEntity extends AuditableEntity {
@TableField("bank_signature") private String bankSignature;
@TableField("signing_key_id") private String signingKeyId;
@TableField("request_message") private String requestMessage;
@TableField("submitted_private_key") private String submittedPrivateKey;
private String status;
@TableField("user_id") private String userId;
@TableField("school_id") private Long schoolId;

@ -30,9 +30,6 @@ public class CurrencyGenerationVerificationEntity extends AuditableEntity {
@TableField("central_bank_signature") private String centralBankSignature;
@TableField("confirmation_time") private String confirmationTime;
@TableField("confirmation_message") private String confirmationMessage;
@TableField("submitted_public_key") private String submittedPublicKey;
@TableField("submitted_private_key") private String submittedPrivateKey;
@TableField("user_input_original_text") private String userInputOriginalText;
private String status;
@TableField("user_id") private String userId;
@TableField("school_id") private Long schoolId;

@ -88,7 +88,6 @@ public class MybatisCurrencyGenerationRequestRepository implements CurrencyGener
.set(CurrencyGenerationRequestEntity::getBankSignature, request.getBankSignature())
.set(CurrencyGenerationRequestEntity::getSigningKeyId, request.getSigningKeyId())
.set(CurrencyGenerationRequestEntity::getRequestMessage, request.getRequestMessage())
.set(CurrencyGenerationRequestEntity::getSubmittedPrivateKey, request.getSubmittedPrivateKey())
.set(CurrencyGenerationRequestEntity::getStatus, request.getStatus().name())
.set(CurrencyGenerationRequestEntity::getUpdatedAt, LocalDateTime.now())
.set(CurrencyGenerationRequestEntity::getUpdatedBy, operator);
@ -113,7 +112,6 @@ public class MybatisCurrencyGenerationRequestRepository implements CurrencyGener
entity.setBankSignature(request.getBankSignature());
entity.setSigningKeyId(request.getSigningKeyId());
entity.setRequestMessage(request.getRequestMessage());
entity.setSubmittedPrivateKey(request.getSubmittedPrivateKey());
entity.setStatus(request.getStatus().name());
return entity;
}
@ -123,8 +121,7 @@ public class MybatisCurrencyGenerationRequestRepository implements CurrencyGener
entity.getInstitutionIdentifier(), entity.getFullInstitutionIdentifier(), entity.getAmount(),
entity.getDeliveryNodeCode(), entity.getRequestTimestamp(), entity.getRequestOriginalText(),
entity.getRequestDigest(), entity.getBankSignature(), entity.getSigningKeyId(),
entity.getRequestMessage(), entity.getSubmittedPrivateKey(),
CurrencyGenerationRequest.Status.valueOf(entity.getStatus()));
entity.getRequestMessage(), CurrencyGenerationRequest.Status.valueOf(entity.getStatus()));
}
private void insertLog(Long requestId, String operation, String fromStatus, String toStatus,

@ -82,12 +82,6 @@ public class MybatisCurrencyGenerationVerificationRepository implements Currency
.set(CurrencyGenerationVerificationEntity::getConfirmationTime, value.getConfirmationTime())
.set(CurrencyGenerationVerificationEntity::getConfirmationMessage,
value.getConfirmationMessage())
.set(CurrencyGenerationVerificationEntity::getSubmittedPublicKey,
value.getSubmittedPublicKey())
.set(CurrencyGenerationVerificationEntity::getSubmittedPrivateKey,
value.getSubmittedPrivateKey())
.set(CurrencyGenerationVerificationEntity::getUserInputOriginalText,
value.getUserInputOriginalText())
.set(CurrencyGenerationVerificationEntity::getStatus, value.getStatus().name())
.set(CurrencyGenerationVerificationEntity::getUpdatedAt, LocalDateTime.now())
.set(CurrencyGenerationVerificationEntity::getUpdatedBy, operator);
@ -107,11 +101,7 @@ public class MybatisCurrencyGenerationVerificationRepository implements Currency
e.setUsedQuota(value.getUsedQuota()); e.setRemainingQuota(value.getRemainingQuota());
e.setQuotaSufficient(value.getQuotaSufficient()); e.setCentralBankKeyId(value.getCentralBankKeyId());
e.setCentralBankSignature(value.getCentralBankSignature()); e.setConfirmationTime(value.getConfirmationTime());
e.setConfirmationMessage(value.getConfirmationMessage());
e.setSubmittedPublicKey(value.getSubmittedPublicKey());
e.setSubmittedPrivateKey(value.getSubmittedPrivateKey());
e.setUserInputOriginalText(value.getUserInputOriginalText());
e.setStatus(value.getStatus().name());
e.setConfirmationMessage(value.getConfirmationMessage()); e.setStatus(value.getStatus().name());
return e;
}
@ -120,8 +110,7 @@ public class MybatisCurrencyGenerationVerificationRepository implements Currency
e.getRequestOriginalText(), e.getRequestDigest(), e.getBankSignature(), e.getBankKeyId(), e.getAmount(),
e.getSignatureValid(), e.getRecomputedDigest(), e.getDigestMatches(), e.getTotalQuota(), e.getUsedQuota(),
e.getRemainingQuota(), e.getQuotaSufficient(), e.getCentralBankKeyId(), e.getCentralBankSignature(),
e.getConfirmationTime(), e.getConfirmationMessage(), e.getSubmittedPublicKey(),
e.getSubmittedPrivateKey(), e.getUserInputOriginalText(),
e.getConfirmationTime(), e.getConfirmationMessage(),
CurrencyGenerationVerification.Status.valueOf(e.getStatus()));
}

@ -0,0 +1,19 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
public class CurrencyRuleRequest {
@NotBlank(message = "币串前缀不能为空")
private String prefix;
@NotBlank(message = "币串ID规则不能为空")
private String idRule;
@NotBlank(message = "币串初始状态不能为空")
private String currencyStatus;
}

@ -0,0 +1,31 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
@ApiModel(description = "货币生成的步骤三货币生成请求验证分步操作参数")
@Schema(description = "货币生成的步骤三货币生成请求验证分步操作参数")
public class CurrencyVerificationStepRequest {
@ApiModelProperty(value = "货币生成的步骤三验证记录编号", required = true, example = "1900000000000000003")
@Schema(description = "货币生成的步骤三验证记录编号", required = true, example = "1900000000000000003")
@NotNull(message = "步骤三验证编号不能为空")
@Positive(message = "步骤三验证编号必须大于0")
private Long verificationId;
@ApiModelProperty(value = "验签步骤使用的商业银行第二SM2公钥完整值")
@Schema(description = "验签步骤使用的商业银行第二SM2公钥完整值")
private String publicKey;
@ApiModelProperty(value = "确认步骤使用的中央银行第一SM2私钥完整值")
@Schema(description = "确认步骤使用的中央银行第一SM2私钥完整值")
private String privateKey;
public Long getVerificationId() { return verificationId; }
public void setVerificationId(Long verificationId) { this.verificationId = verificationId; }
public String getPublicKey() { return publicKey; }
public void setPublicKey(String publicKey) { this.publicKey = publicKey; }
public String getPrivateKey() { return privateKey; }
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
}

@ -1,20 +0,0 @@
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotBlank;
@Schema(description = "第二公钥SM2验签参数")
public class VerifySignatureRequest {
@NotBlank(message = "商业银行第二公钥不能为空")
@Schema(description = "货币生成的步骤一生成并备案的商业银行第二SM2公钥", required = true)
private String publicKey;
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
}

@ -15,8 +15,8 @@ import com.yau.digitalrmb.institutionidentity.application.OperationStatusResult;
import com.yau.digitalrmb.institutionidentity.application.SignatureResult;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CalculateCurrencyDigestRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ConfirmCurrencyRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyVerificationStepRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.ReceiveCurrencyVerificationRequest;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.VerifySignatureRequest;
import com.yau.digitalrmb.security.context.AuthContextHolder;
import com.yau.digitalrmb.security.context.JwtUser;
import com.yau.digitalrmb.shared.api.ApiResponse;
@ -49,11 +49,11 @@ public class CurrencyGenerationVerificationController {
@PostMapping("/steps/verify-signature")
@Operation(summary = "第二公钥SM2验证",
description = "对应页面第二公钥SM2验证按钮。前端只需传商业银行第二公钥完整值,后端自动查找当前实训主体最新的步骤三验证记录,校验公钥归属并使用该第二公钥执行SM2_Verify(摘要值, 签名值)。")
description = "对应页面第二公钥SM2验证按钮。前端传当前实训主体的商业银行第二公钥完整值后端校验公钥归属并使用该第二公钥执行SM2_Verify(摘要值, 签名值)。")
public ApiResponse<OperationBooleanResult> verifySignature(
@Valid @RequestBody VerifySignatureRequest request) {
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
CurrencyGenerationVerificationResult result = service.verifySignature(
CurrencyGenerationVerificationResult result = service.verifySignature(request.getVerificationId(),
request.getPublicKey(), InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationBooleanResult(Boolean.TRUE.equals(result.getSignatureValid())),
MDC.get(TraceIdFilter.MDC_KEY));
@ -71,10 +71,11 @@ public class CurrencyGenerationVerificationController {
}
@PostMapping("/steps/match-digest")
@Operation(summary = "匹配摘要一和摘要二", description = "对应页面匹配按钮,比较重算摘要一与请求报文摘要二。无需传参,后端自动查找当前实训最新记录。")
public ApiResponse<OperationBooleanResult> matchDigest() {
@Operation(summary = "匹配摘要一和摘要二", description = "对应页面“匹配”按钮,比较重算摘要一与请求报文摘要二。")
public ApiResponse<OperationBooleanResult> matchDigest(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
CurrencyGenerationVerificationResult result = service.matchDigest(
CurrencyGenerationVerificationResult result = service.matchDigest(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationBooleanResult(Boolean.TRUE.equals(result.getDigestMatches())),
MDC.get(TraceIdFilter.MDC_KEY));
@ -82,10 +83,11 @@ public class CurrencyGenerationVerificationController {
@PostMapping("/steps/verify-quota")
@Operation(summary = "额度验证",
description = "对应页面额度验证按钮。整个系统共享50000元实训额度累计所有未逻辑删除申请只返回总额度、已用额度、剩余额度、本次申请和是否充足。无需传参。")
public ApiResponse<CurrencyQuotaVerificationResult> verifyQuota() {
description = "对应页面“额度验证”按钮。整个系统共享50000元实训额度累计所有未逻辑删除申请只返回总额度、已用额度、剩余额度、本次申请和是否充足。")
public ApiResponse<CurrencyQuotaVerificationResult> verifyQuota(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
return ApiResponse.success(service.verifyQuota(InstitutionKeySubject.from(user),
return ApiResponse.success(service.verifyQuota(request.getVerificationId(), InstitutionKeySubject.from(user),
user.getUsername()), MDC.get(TraceIdFilter.MDC_KEY));
}
@ -103,20 +105,22 @@ public class CurrencyGenerationVerificationController {
}
@PostMapping("/steps/package-response")
@Operation(summary = "打包确认报文", description = "对应页面打包报文按钮,只组装确认报文,不执行返回。无需传参,后端自动查找当前实训最新记录。")
public ApiResponse<OperationMessageResult> packageResponse() {
@Operation(summary = "打包确认报文", description = "对应页面“打包报文”按钮,只组装确认报文,不执行返回。")
public ApiResponse<OperationMessageResult> packageResponse(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
CurrencyGenerationVerificationResult result = service.packageResponse(
CurrencyGenerationVerificationResult result = service.packageResponse(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationMessageResult(result.getConfirmationMessage()),
MDC.get(TraceIdFilter.MDC_KEY));
}
@PostMapping("/steps/return-response")
@Operation(summary = "返回确认报文", description = "对应页面返回确认报文按钮,只返回已打包完成的确认报文。无需传参,后端自动查找当前实训最新记录。")
public ApiResponse<OperationStatusResult> returnResponse() {
@Operation(summary = "返回确认报文", description = "对应页面“返回确认报文”按钮,只返回已打包完成的确认报文。")
public ApiResponse<OperationStatusResult> returnResponse(
@Valid @RequestBody CurrencyVerificationStepRequest request) {
JwtUser user = AuthContextHolder.get();
CurrencyGenerationVerificationResult result = service.returnResponse(
CurrencyGenerationVerificationResult result = service.returnResponse(request.getVerificationId(),
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new OperationStatusResult(result.getStatus()), MDC.get(TraceIdFilter.MDC_KEY));
}

@ -1,13 +1,12 @@
package com.yau.digitalrmb.institutionidentity.interfaces.rest;
import com.yau.digitalrmb.institutionidentity.application.CurrencyIdRuleResult;
import com.yau.digitalrmb.institutionidentity.application.DenominationDecompositionResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyBatchResult;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyListItemResult;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyOperationResult;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyRuleResult;
import com.yau.digitalrmb.institutionidentity.application.StandardCurrencyService;
import com.yau.digitalrmb.institutionidentity.interfaces.dto.CurrencyRuleRequest;
import com.yau.digitalrmb.shared.api.ApiResponse;
import com.yau.digitalrmb.shared.web.TraceIdFilter;
import com.yau.digitalrmb.security.context.AuthContextHolder;
@ -18,10 +17,12 @@ import org.slf4j.MDC;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;
@Validated
@ -42,22 +43,14 @@ public class StandardCurrencyController {
result.getTotalCount(), result.getBreakdown()), traceId());
}
@PostMapping("/steps/confirm-id-rule")
@Operation(summary = "确认币串ID生成规则", description = "无需请求体。后端自动设置DC前缀和批次号+枚序号+随机校验码ID规则返回ID规则明细表批次号、枚序号、随机校验码、完整币串ID。")
public ApiResponse<CurrencyIdRuleResult> confirmIdRule() {
@PostMapping("/steps/confirm-rule")
@Operation(summary = "确认币串生成规则", description = "校验DC前缀、币串ID规则和待生效初始状态。")
public ApiResponse<StandardCurrencyOperationResult> confirmRule(@Valid @RequestBody CurrencyRuleRequest request) {
JwtUser user = AuthContextHolder.get();
CurrencyIdRuleResult result = service.confirmIdRule(
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(result, traceId());
}
@PostMapping("/steps/confirm-currency-rule")
@Operation(summary = "确认标准面额币串规则", description = "无需请求体。返回标准面额币串规则明细表前缀、币串ID、标准面额、机构标识、银行签名段、央行签名段、币串状态数据从步骤七步骤八动态获取。")
public ApiResponse<StandardCurrencyRuleResult> confirmCurrencyRule() {
JwtUser user = AuthContextHolder.get();
StandardCurrencyRuleResult result = service.confirmCurrencyRule(
InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(result, traceId());
StandardCurrencyBatchResult result = service.confirmRule(request.getPrefix(), request.getIdRule(),
request.getCurrencyStatus(), InstitutionKeySubject.from(user), user.getUsername());
return ApiResponse.success(new StandardCurrencyOperationResult(
result.getStatus(), result.getTotalCount()), traceId());
}
@PostMapping("/steps/generate")

@ -24,7 +24,7 @@ import javax.validation.Valid;
@RestController
@RequestMapping("/api/v1/institution-identifiers/transaction-information-identifiers")
@Tag(name = "货币生成的模块四:生成交易信息标识")
@Tag(name = "模块四:生成交易信息标识")
public class TransactionInformationIdentifierController {
@Resource
private TransactionInformationIdentifierService service;

@ -1,22 +0,0 @@
package com.yau.digitalrmb.issuance.application.query;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.math.BigDecimal;
import java.util.List;
@Getter
@AllArgsConstructor
public class DigitalCurrencyProductionDigestView {
private final DigitalCurrencyProductionBatchView batch;
private final List<CoinDigestView> digests;
@Getter
@AllArgsConstructor
public static class CoinDigestView {
private final String coinId;
private final BigDecimal denomination;
private final String digest;
}
}

@ -18,14 +18,9 @@ public class ReserveDeductionExecutionView {
private final Instant executedAt;
private final String executedByUserId;
private final String executedBy;
private final BigDecimal reversedBalance;
private final Instant reversedAt;
private final String reversedByUserId;
private final String reversedBy;
public static ReserveDeductionExecutionView from(ReserveDeductionExecution value) {
return new ReserveDeductionExecutionView(value.getTransactionId(), value.getBeforeBalance(), value.getDeductionAmount(),
value.getAfterBalance(), value.getStatus(), value.getExecutedAt(), value.getExecutedByUserId(), value.getExecutedBy(),
value.getReversedBalance(), value.getReversedAt(), value.getReversedByUserId(), value.getReversedBy());
value.getAfterBalance(), value.getStatus(), value.getExecutedAt(), value.getExecutedByUserId(), value.getExecutedBy());
}
}

@ -2,7 +2,6 @@ package com.yau.digitalrmb.issuance.application.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.issuance.application.query.CentralBankIssuanceBusinessReviewView;
import com.yau.digitalrmb.issuance.domain.model.CentralBankCurrencyVault;
import com.yau.digitalrmb.issuance.domain.model.CentralBankInstitutionAccount;
@ -34,7 +33,6 @@ public class CentralBankIssuanceBusinessReviewService {
private final CentralBankInstitutionAccountRepository accountRepository;
private final CentralBankCurrencyVaultRepository vaultRepository;
private final CentralBankIssuanceBusinessReviewRepository reviewRepository;
private final DigitalCurrencyGenerationModuleGateway generationModuleGateway;
private final ObjectMapper objectMapper;
public CentralBankIssuanceBusinessReviewService(CentralBankIssuanceReceiptRepository receiptRepository,
@ -42,20 +40,17 @@ public class CentralBankIssuanceBusinessReviewService {
CentralBankInstitutionAccountRepository accountRepository,
CentralBankCurrencyVaultRepository vaultRepository,
CentralBankIssuanceBusinessReviewRepository reviewRepository,
DigitalCurrencyGenerationModuleGateway generationModuleGateway,
ObjectMapper objectMapper) {
this.receiptRepository = receiptRepository;
this.verificationRepository = verificationRepository;
this.accountRepository = accountRepository;
this.vaultRepository = vaultRepository;
this.reviewRepository = reviewRepository;
this.generationModuleGateway = generationModuleGateway;
this.objectMapper = objectMapper;
}
@Transactional
public CentralBankIssuanceBusinessReviewView review(UUID requestId, IssuanceAuditActor actor,
InstitutionKeySubject keySubject) {
public CentralBankIssuanceBusinessReviewView review(UUID requestId, IssuanceAuditActor actor) {
requireActor(actor);
requireVerificationPassed(requestId);
CentralBankIssuanceBusinessReview result;
@ -67,14 +62,9 @@ public class CentralBankIssuanceBusinessReviewService {
String currency = text(payload, "currency");
String denominationSummary = text(payload, "denominations");
BigDecimal requestedAmount = new BigDecimal(text(payload, "totalAmount"));
List<com.yau.digitalrmb.issuance.domain.model.DenominationItem> denominations =
denominations(denominationSummary);
BigDecimal verifiedAmount = denominationTotal(denominations);
generationModuleGateway.validateIssuancePrerequisites(
keySubject, requestedAmount, organizationId, denominations);
BigDecimal verifiedAmount = denominationTotal(denominationSummary);
Optional<CentralBankInstitutionAccount> account = accountRepository.findByBankCode(bankCode);
boolean accountValid = account.isPresent() && "NORMAL".equals(account.get().getStatus())
&& generationModuleGateway.isCurrentConfirmedInstitutionIdentifier(keySubject, organizationId);
boolean accountValid = account.isPresent() && organizationId.equals(account.get().getOrganizationId()) && "NORMAL".equals(account.get().getStatus());
Optional<CentralBankCurrencyVault> vault = vaultRepository.findByCurrency(currency);
boolean vaultSufficient = vault.isPresent() && vault.get().getAvailableBalance().compareTo(requestedAmount) >= 0;
boolean amountConsistent = requestedAmount.compareTo(verifiedAmount) == 0;
@ -111,9 +101,8 @@ public class CentralBankIssuanceBusinessReviewService {
}
}
private List<com.yau.digitalrmb.issuance.domain.model.DenominationItem> denominations(String value) {
List<com.yau.digitalrmb.issuance.domain.model.DenominationItem> result =
new ArrayList<com.yau.digitalrmb.issuance.domain.model.DenominationItem>();
private BigDecimal denominationTotal(String value) {
BigDecimal total = BigDecimal.ZERO;
String[] items = value.split(",");
if (items.length == 0) throw new IllegalArgumentException("面额明细不能为空");
for (String item : items) {
@ -122,17 +111,7 @@ public class CentralBankIssuanceBusinessReviewService {
BigDecimal denomination = new BigDecimal(pair[0]);
int quantity = Integer.parseInt(pair[1]);
if (denomination.signum() <= 0 || quantity < 0) throw new IllegalArgumentException("面额明细数值无效:" + item);
result.add(new com.yau.digitalrmb.issuance.domain.model.DenominationItem(
denomination, quantity));
}
return result;
}
private BigDecimal denominationTotal(
List<com.yau.digitalrmb.issuance.domain.model.DenominationItem> denominations) {
BigDecimal total = BigDecimal.ZERO;
for (com.yau.digitalrmb.issuance.domain.model.DenominationItem item : denominations) {
total = total.add(item.getDenomination().multiply(BigDecimal.valueOf(item.getQuantity())));
total = total.add(denomination.multiply(BigDecimal.valueOf(quantity)));
}
return total.setScale(2);
}

@ -3,7 +3,6 @@ package com.yau.digitalrmb.issuance.application.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yau.digitalrmb.issuance.application.query.DigitalCurrencyProductionBatchView;
import com.yau.digitalrmb.issuance.application.query.DigitalCurrencyProductionDigestView;
import com.yau.digitalrmb.issuance.domain.model.CentralBankIssuanceReceipt;
import com.yau.digitalrmb.issuance.domain.model.DigitalCurrencyProductionBatch;
import com.yau.digitalrmb.issuance.domain.model.DraftDigitalCurrency;
@ -66,7 +65,7 @@ public class DigitalCurrencyDraftProductionService {
if (existing.isPresent()) return DigitalCurrencyProductionBatchView.from(existing.get());
ReserveDeductionNotification notification = notificationRepository.findByRequestId(requestId)
.orElseThrow(() -> new BusinessException(ErrorCode.VALIDATION_ERROR, "请先发送准备金扣减通知"));
ReserveDeductionExecution execution = executionRepository.findByTransactionIdForUpdate(notification.getTransactionId())
ReserveDeductionExecution execution = executionRepository.findByTransactionId(notification.getTransactionId())
.orElseThrow(() -> new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成存款准备金扣减"));
if (!"DEDUCTED".equals(execution.getStatus())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "存款准备金尚未扣减成功,不能生产数字货币");
@ -109,20 +108,6 @@ public class DigitalCurrencyDraftProductionService {
return productionRepository.findByRequestId(requestId).map(DigitalCurrencyProductionBatchView::from);
}
@Transactional(readOnly = true)
public Optional<DigitalCurrencyProductionDigestView> digests(UUID requestId) {
return productionRepository.findByRequestId(requestId).map(batch -> {
List<DigitalCurrencyProductionDigestView.CoinDigestView> values =
new ArrayList<DigitalCurrencyProductionDigestView.CoinDigestView>();
for (DraftDigitalCurrency coin : batch.getDraftCoins()) {
values.add(new DigitalCurrencyProductionDigestView.CoinDigestView(
coin.getCoinId(), coin.getDenomination(),
generationGateway.digest(coin.getSourceCompleteCurrency())));
}
return new DigitalCurrencyProductionDigestView(DigitalCurrencyProductionBatchView.from(batch), values);
});
}
private List<DraftDigitalCurrency> buildCoins(String batchId, String organizationId, String currency,
List<GeneratedCurrencyStockItem> reservedCurrencies,
Instant createdAt, IssuanceAuditActor actor,

@ -2,8 +2,6 @@ package com.yau.digitalrmb.issuance.application.service;
import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureResult;
import com.yau.digitalrmb.institutionidentity.application.ControlSystemSignatureService;
import com.yau.digitalrmb.institutionidentity.application.CommercialBankInstitutionIdentifierResult;
import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierFeedbackService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
import com.yau.digitalrmb.institutionidentity.application.QuotaControlBitResult;
@ -45,7 +43,6 @@ public class DigitalCurrencyGenerationModuleGateway {
private final IssuanceSourceCurrencyUsageMapper sourceUsageMapper;
private final JdbcTemplate jdbcTemplate;
private final InstitutionIdentityCryptography cryptography;
private final InstitutionIdentifierFeedbackService identifierFeedbackService;
@Autowired
public DigitalCurrencyGenerationModuleGateway(InstitutionKeyService keyService,
@ -55,8 +52,7 @@ public class DigitalCurrencyGenerationModuleGateway {
StandardCurrencyMapper standardCurrencyMapper,
IssuanceSourceCurrencyUsageMapper sourceUsageMapper,
JdbcTemplate jdbcTemplate,
InstitutionIdentityCryptography cryptography,
InstitutionIdentifierFeedbackService identifierFeedbackService) {
InstitutionIdentityCryptography cryptography) {
this.keyService = keyService;
this.quotaService = quotaService;
this.controlSignatureService = controlSignatureService;
@ -65,27 +61,12 @@ public class DigitalCurrencyGenerationModuleGateway {
this.sourceUsageMapper = sourceUsageMapper;
this.jdbcTemplate = jdbcTemplate;
this.cryptography = cryptography;
this.identifierFeedbackService = identifierFeedbackService;
}
public DigitalCurrencyGenerationModuleGateway(InstitutionKeyService keyService,
QuotaControlBitService quotaService,
ControlSystemSignatureService controlSignatureService) {
this(keyService, quotaService, controlSignatureService, null, null, null, null, null, null);
}
public boolean isCurrentConfirmedInstitutionIdentifier(InstitutionKeySubject subject,
String institutionIdentifier) {
requiredSubject(subject);
if (identifierFeedbackService == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR,
"Institution identifier feedback service is not configured");
}
CommercialBankInstitutionIdentifierResult current =
identifierFeedbackService.commercialBankResult(subject);
return "FEEDBACKED".equals(current.getApplicationStatus())
&& required(institutionIdentifier, "issuance institution identifier")
.equals(current.getInstitutionIdentifier());
this(keyService, quotaService, controlSignatureService, null, null, null, null, null);
}
public String signIssuanceDigest(InstitutionKeySubject subject, String digest) {
@ -119,32 +100,6 @@ public class DigitalCurrencyGenerationModuleGateway {
public String commercialBankSigningKeyId() { return InstitutionKeyService.BANK_SECOND_KEY; }
public String centralBankSigningKeyId() { return InstitutionKeyService.CENTRAL_FIRST_KEY; }
@Transactional(readOnly = true)
public void validateIssuancePrerequisites(InstitutionKeySubject subject, BigDecimal amount,
String institutionIdentifier,
List<DenominationItem> denominations) {
IssuanceControlMaterial material = requireControlMaterial(subject, amount, institutionIdentifier);
if (standardCurrencyBatchMapper == null || sourceUsageMapper == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR,
"生成模块币串库存服务尚未配置");
}
StandardCurrencyBatchEntity batch = standardCurrencyBatchMapper.selectOne(
new LambdaQueryWrapper<StandardCurrencyBatchEntity>()
.eq(StandardCurrencyBatchEntity::getQuotaControlBitId,
material.getQuotaControlBitId())
.eq(StandardCurrencyBatchEntity::getUserId, subject.getUserId())
.eq(StandardCurrencyBatchEntity::getSchoolId, subject.getSchoolId())
.eq(StandardCurrencyBatchEntity::getClassId, subject.getClassId())
.eq(StandardCurrencyBatchEntity::getStatus, "GENERATED")
.eq(StandardCurrencyBatchEntity::getDeleted, false)
.last("LIMIT 1"));
if (batch == null) {
throw validation("生成模块尚未生成与本次额度控制位对应的标准币串");
}
selectExactQuantities(sourceUsageMapper.selectAvailableSources(batch.getId()),
requestedQuantities(denominations));
}
@Transactional
public List<GeneratedCurrencyStockItem> reserveGeneratedCurrencies(InstitutionKeySubject subject,
Long quotaControlBitId,
@ -404,8 +359,8 @@ public class DigitalCurrencyGenerationModuleGateway {
String institutionIdentifier) {
QuotaControlBitResult quota = quotaService.detail(subject);
ControlSystemSignatureResult signature = controlSignatureService.detail(subject);
requireStatus("额度控制位", quota.getStatus(), "RECEIVED");
requireStatus("控制系统签名", signature.getStatus(), "SENT");
requireCompleted("额度控制位", quota.getStatus());
requireCompleted("控制系统签名", signature.getStatus());
if (amount == null || quota.getAmount() == null || signature.getAmount() == null
|| quota.getAmount().compareTo(amount) != 0 || signature.getAmount().compareTo(amount) != 0) {
throw validation("生成模块的控制数据金额与发行批次金额不一致");
@ -428,9 +383,9 @@ public class DigitalCurrencyGenerationModuleGateway {
required(signature.getControlSignature(), "控制系统签名"));
}
private void requireStatus(String name, String status, String expectedStatus) {
if (!expectedStatus.equals(status)) {
throw validation(name + "尚未完成,当前状态:" + (status == null ? "缺失" : status));
private void requireCompleted(String name, String status) {
if (!"RECEIVED".equals(status)) {
throw validation(name + "尚未完成接收,当前状态:" + (status == null ? "缺失" : status));
}
}

@ -44,9 +44,7 @@ public class IssuanceAttemptCancellationHandler implements AttemptCancellationHa
Optional<ReserveDeductionNotificationView> notification = notificationService.find(requestId);
if (notification.isPresent()
&& executionService.find(notification.get().getTransactionId()).isPresent()) {
executionService.reverse(notification.get().getTransactionId(),
new IssuanceAuditActor(subject.getUserId(), subject.getUserId()));
return;
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "准备金已扣减,不能取消发行实验");
}
generationGateway.releaseGeneratedCurrencies(requestId,
new IssuanceAuditActor(subject.getUserId(), subject.getUserId()));

@ -148,7 +148,7 @@ public class IssuanceTrainingActionService {
case "03:query-application-message":
return completed(centralBankService.getCentralBankView(requestId));
case "04:auto-confirm-plan":
return completed(reviewService.review(requestId, actor, keySubject));
return completed(reviewService.review(requestId, actor));
case "04:generate-reserve-deduction-request":
return completed(notificationService.send(requestId, actor));
case "05:confirm-receipt":
@ -167,7 +167,7 @@ public class IssuanceTrainingActionService {
case "06:confirm-production-receipt":
return completed(productionService.produce(requestId, actor, keySubject));
case "06:generate-production-digest":
return completed(productionService.digests(requestId)
return completed(productionService.find(requestId)
.orElseThrow(() -> validation("请先生成数字货币生产批次")));
case "07:central-confirm-ownership":
return completed(ownershipService.confirm(requestId, actor, keySubject));

@ -6,7 +6,6 @@ import com.yau.digitalrmb.issuance.domain.model.IssuanceAuditActor;
import com.yau.digitalrmb.issuance.domain.model.ReserveDeductionExecution;
import com.yau.digitalrmb.issuance.domain.model.ReserveDeductionNotification;
import com.yau.digitalrmb.issuance.domain.repository.CentralBankInstitutionAccountRepository;
import com.yau.digitalrmb.issuance.domain.repository.DigitalCurrencyProductionRepository;
import com.yau.digitalrmb.issuance.domain.repository.ReserveDeductionExecutionRepository;
import com.yau.digitalrmb.issuance.domain.repository.ReserveDeductionNotificationRepository;
import com.yau.digitalrmb.shared.api.ErrorCode;
@ -23,16 +22,13 @@ public class ReserveDeductionExecutionService {
private final ReserveDeductionNotificationRepository notificationRepository;
private final ReserveDeductionExecutionRepository executionRepository;
private final CentralBankInstitutionAccountRepository accountRepository;
private final DigitalCurrencyProductionRepository productionRepository;
public ReserveDeductionExecutionService(ReserveDeductionNotificationRepository notificationRepository,
ReserveDeductionExecutionRepository executionRepository,
CentralBankInstitutionAccountRepository accountRepository,
DigitalCurrencyProductionRepository productionRepository) {
CentralBankInstitutionAccountRepository accountRepository) {
this.notificationRepository = notificationRepository;
this.executionRepository = executionRepository;
this.accountRepository = accountRepository;
this.productionRepository = productionRepository;
}
@Transactional
@ -81,43 +77,6 @@ public class ReserveDeductionExecutionService {
return executionRepository.findByTransactionId(transactionId).map(ReserveDeductionExecutionView::from);
}
@Transactional
public ReserveDeductionExecutionView reverse(String transactionId, IssuanceAuditActor actor) {
requireActor(actor);
String requiredTransactionId = required(transactionId, "transaction id");
ReserveDeductionExecution execution = executionRepository.findByTransactionIdForUpdate(requiredTransactionId)
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"Reserve deduction execution does not exist"));
if ("REVERSED".equals(execution.getStatus())) {
return ReserveDeductionExecutionView.from(execution);
}
if (!"DEDUCTED".equals(execution.getStatus())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"Only a deducted reserve execution can be reversed");
}
if (productionRepository.findByRequestId(execution.getRequestId()).isPresent()) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"Digital currency has already been produced; reserve deduction cannot be reversed");
}
ReserveDeductionNotification notification = notificationRepository
.findByTransactionId(requiredTransactionId)
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"Reserve deduction notification does not exist"));
CentralBankInstitutionAccount account = accountRepository.findByBankCodeForUpdate(notification.getBankCode())
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
"Reserve account does not exist"));
if (!notification.getReserveAccountNo().equals(account.getReserveAccountNo())
|| !notification.getReserveAccountName().equals(account.getReserveAccountName())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
"Reserve account does not match the original deduction notification");
}
BigDecimal reversedBalance = account.getReserveBalance().add(execution.getDeductionAmount());
ReserveDeductionExecution reversed = execution.reversed(reversedBalance, Instant.now(), actor);
accountRepository.updateReserveBalance(account.getBankCode(), reversedBalance);
executionRepository.save(reversed);
return ReserveDeductionExecutionView.from(reversed);
}
private void requireNotificationMatches(ReserveDeductionNotification notification, String reserveAccountNo,
String reserveAccountName, BigDecimal deductionAmount) {
if (!"SUBMITTED".equals(notification.getStatus())) {

@ -65,7 +65,7 @@ public class ReserveDeductionNotificationService {
BigDecimal deductionAmount = new BigDecimal(requiredText(payload, "totalAmount"));
CentralBankInstitutionAccount account = accountRepository.findByBankCode(bankCode)
.orElseThrow(() -> new BusinessException(ErrorCode.VALIDATION_ERROR, "申请机构未开立准备金账户"));
if (!"NORMAL".equals(account.getStatus())) {
if (!organizationId.equals(account.getOrganizationId()) || !"NORMAL".equals(account.getStatus())) {
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "申请机构准备金账户状态异常");
}
if (blank(account.getReserveAccountName())) {

@ -15,22 +15,10 @@ public final class ReserveDeductionExecution {
private final Instant executedAt;
private final String executedByUserId;
private final String executedBy;
private final BigDecimal reversedBalance;
private final Instant reversedAt;
private final String reversedByUserId;
private final String reversedBy;
public ReserveDeductionExecution(UUID requestId, String transactionId, BigDecimal beforeBalance, BigDecimal deductionAmount,
BigDecimal afterBalance, String status, Instant executedAt,
String executedByUserId, String executedBy) {
this(requestId, transactionId, beforeBalance, deductionAmount, afterBalance, status, executedAt,
executedByUserId, executedBy, null, null, null, null);
}
public ReserveDeductionExecution(UUID requestId, String transactionId, BigDecimal beforeBalance, BigDecimal deductionAmount,
BigDecimal afterBalance, String status, Instant executedAt,
String executedByUserId, String executedBy, BigDecimal reversedBalance,
Instant reversedAt, String reversedByUserId, String reversedBy) {
this.requestId = Objects.requireNonNull(requestId, "发行请求标识不能为空");
this.transactionId = Objects.requireNonNull(transactionId, "交易信息标识不能为空");
this.beforeBalance = Objects.requireNonNull(beforeBalance, "扣款前余额不能为空");
@ -40,23 +28,7 @@ public final class ReserveDeductionExecution {
this.executedAt = Objects.requireNonNull(executedAt, "扣款时间不能为空");
this.executedByUserId = Objects.requireNonNull(executedByUserId, "扣款操作人ID不能为空");
this.executedBy = Objects.requireNonNull(executedBy, "扣款操作人名称不能为空");
this.reversedBalance = reversedBalance;
this.reversedAt = reversedAt;
this.reversedByUserId = reversedByUserId;
this.reversedBy = reversedBy;
}
public ReserveDeductionExecution reversed(BigDecimal balance, Instant time, IssuanceAuditActor actor) {
if (!"DEDUCTED".equals(status)) {
throw new IllegalStateException("only a deducted execution can be reversed");
}
return new ReserveDeductionExecution(requestId, transactionId, beforeBalance, deductionAmount,
afterBalance, "REVERSED", executedAt, executedByUserId, executedBy,
Objects.requireNonNull(balance, "reversed balance must not be null"),
Objects.requireNonNull(time, "reversed time must not be null"),
Objects.requireNonNull(actor, "reversal actor must not be null").getUserId(), actor.getUsername());
}
public UUID getRequestId() { return requestId; }
public String getTransactionId() { return transactionId; }
public BigDecimal getBeforeBalance() { return beforeBalance; }
@ -66,8 +38,4 @@ public final class ReserveDeductionExecution {
public Instant getExecutedAt() { return executedAt; }
public String getExecutedByUserId() { return executedByUserId; }
public String getExecutedBy() { return executedBy; }
public BigDecimal getReversedBalance() { return reversedBalance; }
public Instant getReversedAt() { return reversedAt; }
public String getReversedByUserId() { return reversedByUserId; }
public String getReversedBy() { return reversedBy; }
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save