From 1e111da5bb79a6f7bc5974f2c9cfcae965de7671 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Tue, 18 Aug 2026 13:52:50 +0800 Subject: [PATCH] refactor: isolate issuance source currency usage --- ...igitalCurrencyGenerationModuleGateway.java | 130 +++++++++++------- .../IssuanceSourceCurrencyUsageEntity.java | 29 ++++ .../IssuanceSourceCurrencyUsageMapper.java | 32 +++++ .../module_2_4_5_training_attempt_upgrade.sql | 17 +++ src/main/resources/schema.sql | 17 +++ ...urrencyGenerationStockIntegrationTest.java | 40 +++++- src/test/resources/schema.sql | 17 +++ 7 files changed, 227 insertions(+), 55 deletions(-) create mode 100644 src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/entity/IssuanceSourceCurrencyUsageEntity.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/mapper/IssuanceSourceCurrencyUsageMapper.java diff --git a/src/main/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationModuleGateway.java b/src/main/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationModuleGateway.java index e25805b..8353bb8 100644 --- a/src/main/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationModuleGateway.java +++ b/src/main/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationModuleGateway.java @@ -11,6 +11,8 @@ import com.yau.digitalrmb.shared.exception.BusinessException; import com.yau.digitalrmb.issuance.domain.model.DenominationItem; import com.yau.digitalrmb.issuance.domain.model.DraftDigitalCurrency; import com.yau.digitalrmb.issuance.domain.model.IssuanceAuditActor; +import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceSourceCurrencyUsageEntity; +import com.yau.digitalrmb.issuance.infrastructure.persistence.mapper.IssuanceSourceCurrencyUsageMapper; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyBatchEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyBatchMapper; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyEntity; @@ -37,6 +39,7 @@ public class DigitalCurrencyGenerationModuleGateway { private final ControlSystemSignatureService controlSignatureService; private final StandardCurrencyBatchMapper standardCurrencyBatchMapper; private final StandardCurrencyMapper standardCurrencyMapper; + private final IssuanceSourceCurrencyUsageMapper sourceUsageMapper; private final JdbcTemplate jdbcTemplate; @Autowired @@ -45,19 +48,21 @@ public class DigitalCurrencyGenerationModuleGateway { ControlSystemSignatureService controlSignatureService, StandardCurrencyBatchMapper standardCurrencyBatchMapper, StandardCurrencyMapper standardCurrencyMapper, + IssuanceSourceCurrencyUsageMapper sourceUsageMapper, JdbcTemplate jdbcTemplate) { this.keyService = keyService; this.quotaService = quotaService; this.controlSignatureService = controlSignatureService; this.standardCurrencyBatchMapper = standardCurrencyBatchMapper; this.standardCurrencyMapper = standardCurrencyMapper; + this.sourceUsageMapper = sourceUsageMapper; this.jdbcTemplate = jdbcTemplate; } public DigitalCurrencyGenerationModuleGateway(InstitutionKeyService keyService, QuotaControlBitService quotaService, ControlSystemSignatureService controlSignatureService) { - this(keyService, quotaService, controlSignatureService, null, null, null); + this(keyService, quotaService, controlSignatureService, null, null, null, null); } public String signIssuanceDigest(InstitutionKeySubject subject, String digest) { @@ -82,7 +87,7 @@ public class DigitalCurrencyGenerationModuleGateway { UUID requestId, List denominations, IssuanceAuditActor actor) { - if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null) { + if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null || sourceUsageMapper == null) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成模块币串库存服务尚未配置"); } requiredSubject(subject); @@ -102,39 +107,47 @@ public class DigitalCurrencyGenerationModuleGateway { if (batch == null) { throw validation("生成模块步骤九尚未生成与本次额度控制位对应的标准币串"); } - List alreadyReserved = standardCurrencyMapper.selectList( - new LambdaQueryWrapper() - .eq(StandardCurrencyEntity::getBatchId, batch.getId()) - .eq(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString()) - .eq(StandardCurrencyEntity::getStatus, "发行锁定") - .eq(StandardCurrencyEntity::getDeleted, false) - .orderByAsc(StandardCurrencyEntity::getSequenceNumber)); + List alreadyReserved = sourceUsageMapper.selectReservedSources( + batch.getId(), requestId.toString()); if (!alreadyReserved.isEmpty()) { assertExactQuantities(alreadyReserved, requested); return stockItems(alreadyReserved); } - List available = standardCurrencyMapper.selectList( - new LambdaQueryWrapper() - .eq(StandardCurrencyEntity::getBatchId, batch.getId()) - .eq(StandardCurrencyEntity::getStatus, "待生效") - .eq(StandardCurrencyEntity::getDeleted, false) - .orderByAsc(StandardCurrencyEntity::getSequenceNumber) - .last("FOR UPDATE")); + List available = sourceUsageMapper.selectAvailableSourcesForUpdate(batch.getId()); List selected = selectExactQuantities(available, requested); LocalDateTime now = LocalDateTime.now(); for (StandardCurrencyEntity currency : selected) { - int updated = standardCurrencyMapper.update(null, new LambdaUpdateWrapper() - .eq(StandardCurrencyEntity::getId, currency.getId()) - .eq(StandardCurrencyEntity::getStatus, "待生效") - .eq(StandardCurrencyEntity::getDeleted, false) - .set(StandardCurrencyEntity::getStatus, "发行锁定") - .set(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString()) - .set(StandardCurrencyEntity::getReservedAt, now) - .set(StandardCurrencyEntity::getReservedByUserId, actor.getUserId()) - .set(StandardCurrencyEntity::getReservedBy, actor.getUsername()) - .set(StandardCurrencyEntity::getUpdatedAt, now) - .set(StandardCurrencyEntity::getUpdatedBy, actor.getUsername())); - if (updated != 1) throw validation("币串库存已变化,请重新获取后再发行"); + IssuanceSourceCurrencyUsageEntity existing = sourceUsageMapper.selectBySourceCurrencyIdForUpdate( + currency.getId()); + if (existing == null) { + IssuanceSourceCurrencyUsageEntity usage = new IssuanceSourceCurrencyUsageEntity(); + usage.setSourceCurrencyId(currency.getId()); + usage.setSourceBatchId(currency.getBatchId()); + usage.setIssuanceRequestId(requestId.toString()); + usage.setStatus("RESERVED"); + usage.setReservedAt(now); + usage.setAuditUserId(actor.getUserId()); + usage.setAuditUsername(actor.getUsername()); + usage.setCreatedAt(now); + usage.setUpdatedAt(now); + if (sourceUsageMapper.insert(usage) != 1) { + throw validation("币串库存已变化,请重新获取后再发行"); + } + } else { + int reused = sourceUsageMapper.update(null, + new LambdaUpdateWrapper() + .eq(IssuanceSourceCurrencyUsageEntity::getId, existing.getId()) + .eq(IssuanceSourceCurrencyUsageEntity::getStatus, "RELEASED") + .set(IssuanceSourceCurrencyUsageEntity::getIssuanceRequestId, requestId.toString()) + .set(IssuanceSourceCurrencyUsageEntity::getStatus, "RESERVED") + .set(IssuanceSourceCurrencyUsageEntity::getReservedAt, now) + .set(IssuanceSourceCurrencyUsageEntity::getReleasedAt, null) + .set(IssuanceSourceCurrencyUsageEntity::getIssuedAt, null) + .set(IssuanceSourceCurrencyUsageEntity::getAuditUserId, actor.getUserId()) + .set(IssuanceSourceCurrencyUsageEntity::getAuditUsername, actor.getUsername()) + .set(IssuanceSourceCurrencyUsageEntity::getUpdatedAt, now)); + if (reused != 1) throw validation("币串库存已变化,请重新获取后再发行"); + } } return stockItems(selected); } @@ -143,7 +156,8 @@ public class DigitalCurrencyGenerationModuleGateway { public void activateGeneratedCurrencies(InstitutionKeySubject subject, UUID requestId, String bankCode, String institutionIdentifier, List confirmedCoins, IssuanceAuditActor actor) { - if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null || jdbcTemplate == null) { + if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null + || sourceUsageMapper == null || jdbcTemplate == null) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成模块币串发行服务尚未配置"); } requiredSubject(subject); @@ -162,15 +176,16 @@ public class DigitalCurrencyGenerationModuleGateway { if (!requiredInstitution.equals(coin.getOrganizationId())) throw validation("发行币串机构标识不一致"); if (currencyCode == null) currencyCode = required(coin.getCurrency(), "发行币种"); if (!currencyCode.equals(coin.getCurrency())) throw validation("同一发行批次币种不一致"); - StandardCurrencyEntity source = standardCurrencyMapper.selectOne( - new LambdaQueryWrapper() - .eq(StandardCurrencyEntity::getId, coin.getSourceCurrencyId()) - .eq(StandardCurrencyEntity::getBatchId, coin.getSourceCurrencyBatchId()) - .eq(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString()) - .eq(StandardCurrencyEntity::getStatus, "发行锁定") - .eq(StandardCurrencyEntity::getDeleted, false) - .last("FOR UPDATE")); + StandardCurrencyEntity source = standardCurrencyMapper.selectById(coin.getSourceCurrencyId()); + IssuanceSourceCurrencyUsageEntity usage = sourceUsageMapper.selectBySourceCurrencyIdForUpdate( + coin.getSourceCurrencyId()); if (source == null || !coin.getCoinId().equals(source.getCurrencyId()) + || !coin.getSourceCurrencyBatchId().equals(source.getBatchId()) + || Boolean.TRUE.equals(source.getDeleted()) + || usage == null + || !source.getBatchId().equals(usage.getSourceBatchId()) + || !requestId.toString().equals(usage.getIssuanceRequestId()) + || !"RESERVED".equals(usage.getStatus()) || !requiredInstitution.equals(source.getInstitutionIdentifier()) || !coin.getSourceCompleteCurrency().equals(source.getCompleteCurrency())) { throw validation("生成模块来源币串与发行记录不一致"); @@ -187,19 +202,16 @@ public class DigitalCurrencyGenerationModuleGateway { for (int index = 0; index < sources.size(); index++) { StandardCurrencyEntity source = sources.get(index); DraftDigitalCurrency coin = confirmedCoins.get(index); - int updated = standardCurrencyMapper.update(null, new LambdaUpdateWrapper() - .eq(StandardCurrencyEntity::getId, source.getId()) - .eq(StandardCurrencyEntity::getStatus, "发行锁定") - .eq(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString()) - .eq(StandardCurrencyEntity::getDeleted, false) - .set(StandardCurrencyEntity::getStatus, "可用") - .set(StandardCurrencyEntity::getIssuedAt, now) - .set(StandardCurrencyEntity::getIssuedByUserId, actor.getUserId()) - .set(StandardCurrencyEntity::getIssuedBy, actor.getUsername()) - .set(StandardCurrencyEntity::getOwnershipSignature, coin.getOwnershipSignature()) - .set(StandardCurrencyEntity::getIssuedCompleteCurrency, coin.getCompleteCurrency()) - .set(StandardCurrencyEntity::getUpdatedAt, now) - .set(StandardCurrencyEntity::getUpdatedBy, actor.getUsername())); + int updated = sourceUsageMapper.update(null, + new LambdaUpdateWrapper() + .eq(IssuanceSourceCurrencyUsageEntity::getSourceCurrencyId, source.getId()) + .eq(IssuanceSourceCurrencyUsageEntity::getIssuanceRequestId, requestId.toString()) + .eq(IssuanceSourceCurrencyUsageEntity::getStatus, "RESERVED") + .set(IssuanceSourceCurrencyUsageEntity::getStatus, "ISSUED") + .set(IssuanceSourceCurrencyUsageEntity::getIssuedAt, now) + .set(IssuanceSourceCurrencyUsageEntity::getAuditUserId, actor.getUserId()) + .set(IssuanceSourceCurrencyUsageEntity::getAuditUsername, actor.getUsername()) + .set(IssuanceSourceCurrencyUsageEntity::getUpdatedAt, now)); if (updated != 1) throw validation("币串发行状态已变化,不能重复确权"); jdbcTemplate.update("INSERT INTO commercial_bank_currency " + "(currency_id, source_currency_id, source_batch_id, issuance_request_id, bank_code, " @@ -217,6 +229,24 @@ public class DigitalCurrencyGenerationModuleGateway { } } + @Transactional + public void releaseGeneratedCurrencies(UUID requestId, IssuanceAuditActor actor) { + if (sourceUsageMapper == null) { + throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成模块币串库存服务尚未配置"); + } + if (requestId == null) throw validation("发行申请标识不能为空"); + requiredActor(actor); + LocalDateTime now = LocalDateTime.now(); + sourceUsageMapper.update(null, new LambdaUpdateWrapper() + .eq(IssuanceSourceCurrencyUsageEntity::getIssuanceRequestId, requestId.toString()) + .eq(IssuanceSourceCurrencyUsageEntity::getStatus, "RESERVED") + .set(IssuanceSourceCurrencyUsageEntity::getStatus, "RELEASED") + .set(IssuanceSourceCurrencyUsageEntity::getReleasedAt, now) + .set(IssuanceSourceCurrencyUsageEntity::getAuditUserId, actor.getUserId()) + .set(IssuanceSourceCurrencyUsageEntity::getAuditUsername, actor.getUsername()) + .set(IssuanceSourceCurrencyUsageEntity::getUpdatedAt, now)); + } + private void assertOwnedBatch(Long batchId, InstitutionKeySubject subject) { StandardCurrencyBatchEntity batch = standardCurrencyBatchMapper.selectById(batchId); if (batch == null || !subject.getUserId().equals(batch.getUserId()) diff --git a/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/entity/IssuanceSourceCurrencyUsageEntity.java b/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/entity/IssuanceSourceCurrencyUsageEntity.java new file mode 100644 index 0000000..f49b95d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/entity/IssuanceSourceCurrencyUsageEntity.java @@ -0,0 +1,29 @@ +package com.yau.digitalrmb.issuance.infrastructure.persistence.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Getter +@Setter +@TableName("issuance_source_currency_usage") +public class IssuanceSourceCurrencyUsageEntity { + @TableId(type = IdType.ASSIGN_ID) + private Long id; + @TableField("source_currency_id") private Long sourceCurrencyId; + @TableField("source_batch_id") private Long sourceBatchId; + @TableField("issuance_request_id") private String issuanceRequestId; + private String status; + @TableField("reserved_at") private LocalDateTime reservedAt; + @TableField("released_at") private LocalDateTime releasedAt; + @TableField("issued_at") private LocalDateTime issuedAt; + @TableField("audit_user_id") private String auditUserId; + @TableField("audit_username") private String auditUsername; + @TableField("created_at") private LocalDateTime createdAt; + @TableField("updated_at") private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/mapper/IssuanceSourceCurrencyUsageMapper.java b/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/mapper/IssuanceSourceCurrencyUsageMapper.java new file mode 100644 index 0000000..1e4370d --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/infrastructure/persistence/mapper/IssuanceSourceCurrencyUsageMapper.java @@ -0,0 +1,32 @@ +package com.yau.digitalrmb.issuance.infrastructure.persistence.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyEntity; +import com.yau.digitalrmb.issuance.infrastructure.persistence.entity.IssuanceSourceCurrencyUsageEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface IssuanceSourceCurrencyUsageMapper extends BaseMapper { + @Select("SELECT sc.* FROM standard_currency sc " + + "JOIN issuance_source_currency_usage u ON u.source_currency_id = sc.id " + + "WHERE sc.batch_id = #{batchId} AND sc.deleted = FALSE " + + "AND u.issuance_request_id = #{requestId} AND u.status = 'RESERVED' " + + "ORDER BY sc.sequence_number") + List selectReservedSources(@Param("batchId") Long batchId, + @Param("requestId") String requestId); + + @Select("SELECT sc.* FROM standard_currency sc WHERE sc.batch_id = #{batchId} " + + "AND sc.status = '待生效' AND sc.deleted = FALSE " + + "AND NOT EXISTS (SELECT 1 FROM issuance_source_currency_usage u " + + "WHERE u.source_currency_id = sc.id AND u.status IN ('RESERVED','ISSUED')) " + + "ORDER BY sc.sequence_number FOR UPDATE") + List selectAvailableSourcesForUpdate(@Param("batchId") Long batchId); + + @Select("SELECT * FROM issuance_source_currency_usage WHERE source_currency_id = #{sourceCurrencyId} FOR UPDATE") + IssuanceSourceCurrencyUsageEntity selectBySourceCurrencyIdForUpdate( + @Param("sourceCurrencyId") Long sourceCurrencyId); +} diff --git a/src/main/resources/db/manual/module_2_4_5_training_attempt_upgrade.sql b/src/main/resources/db/manual/module_2_4_5_training_attempt_upgrade.sql index fb43466..be4e5b2 100644 --- a/src/main/resources/db/manual/module_2_4_5_training_attempt_upgrade.sql +++ b/src/main/resources/db/manual/module_2_4_5_training_attempt_upgrade.sql @@ -39,3 +39,20 @@ CREATE TABLE IF NOT EXISTS training_experiment_action ( CONSTRAINT fk_experiment_action_attempt FOREIGN KEY (attempt_id) REFERENCES training_experiment_attempt(id) ); + +CREATE TABLE IF NOT EXISTS issuance_source_currency_usage ( + id BIGINT PRIMARY KEY, + source_currency_id BIGINT NOT NULL, + source_batch_id BIGINT NOT NULL, + issuance_request_id CHAR(36) NOT NULL, + status VARCHAR(16) NOT NULL, + reserved_at TIMESTAMP NOT NULL, + released_at TIMESTAMP NULL, + issued_at TIMESTAMP NULL, + audit_user_id VARCHAR(36) NOT NULL, + audit_username VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + CONSTRAINT uk_issuance_source_currency UNIQUE (source_currency_id), + INDEX idx_issuance_source_request (issuance_request_id, status) +); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 2be7220..df1e3d7 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -309,6 +309,23 @@ CREATE TABLE IF NOT EXISTS digital_currency_ownership_confirmation ( CONSTRAINT fk_digital_currency_ownership_batch FOREIGN KEY (batch_id) REFERENCES digital_currency_production_batch(batch_id) ); +CREATE TABLE IF NOT EXISTS issuance_source_currency_usage ( + id BIGINT PRIMARY KEY, + source_currency_id BIGINT NOT NULL, + source_batch_id BIGINT NOT NULL, + issuance_request_id CHAR(36) NOT NULL, + status VARCHAR(16) NOT NULL, + reserved_at TIMESTAMP NOT NULL, + released_at TIMESTAMP NULL, + issued_at TIMESTAMP NULL, + audit_user_id VARCHAR(36) NOT NULL, + audit_username VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + CONSTRAINT uk_issuance_source_currency UNIQUE (source_currency_id), + INDEX idx_issuance_source_request (issuance_request_id, status) +); + CREATE TABLE IF NOT EXISTS commercial_bank_currency ( currency_id VARCHAR(96) PRIMARY KEY, source_currency_id BIGINT NOT NULL UNIQUE, diff --git a/src/test/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationStockIntegrationTest.java b/src/test/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationStockIntegrationTest.java index 15b3caf..04b0bf4 100644 --- a/src/test/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationStockIntegrationTest.java +++ b/src/test/java/com/yau/digitalrmb/issuance/application/service/DigitalCurrencyGenerationStockIntegrationTest.java @@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.UUID; import java.time.Instant; @@ -38,6 +39,7 @@ class DigitalCurrencyGenerationStockIntegrationTest { "stock-user", 1001L, 2001L); insertCoin(8901L, batchId, 1, "100.00", "DC_SOURCE_001", "SOURCE_CURRENCY_001"); insertCoin(8902L, batchId, 2, "50.00", "DC_SOURCE_002", "SOURCE_CURRENCY_002"); + List> sourceBefore = sourceState(batchId); List reserved = gateway.reserveGeneratedCurrencies( new InstitutionKeySubject("stock-user", 1001L, 2001L), 81L, requestId, @@ -47,9 +49,27 @@ class DigitalCurrencyGenerationStockIntegrationTest { assertThat(reserved).extracting(GeneratedCurrencyStockItem::getCurrencyId) .containsExactly("DC_SOURCE_001", "DC_SOURCE_002"); + assertThat(sourceState(batchId)).isEqualTo(sourceBefore); assertThat(jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM standard_currency WHERE batch_id = ? AND status = '发行锁定' AND issuance_request_id = ?", + "SELECT COUNT(*) FROM issuance_source_currency_usage WHERE source_batch_id = ? " + + "AND issuance_request_id = ? AND status = 'RESERVED'", Integer.class, batchId, requestId.toString())).isEqualTo(2); + + IssuanceAuditActor actor = new IssuanceAuditActor("stock-user", "库存测试员"); + gateway.releaseGeneratedCurrencies(requestId, actor); + assertThat(jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM issuance_source_currency_usage WHERE source_batch_id = ? AND status = 'RELEASED'", + Integer.class, batchId)).isEqualTo(2); + + UUID retryRequestId = UUID.fromString("00000000-0000-0000-0000-000000008811"); + gateway.reserveGeneratedCurrencies(new InstitutionKeySubject("stock-user", 1001L, 2001L), 81L, + retryRequestId, Arrays.asList(new DenominationItem(new BigDecimal("100.00"), 1), + new DenominationItem(new BigDecimal("50.00"), 1)), actor); + assertThat(jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM issuance_source_currency_usage WHERE source_batch_id = ? " + + "AND issuance_request_id = ? AND status = 'RESERVED'", + Integer.class, batchId, retryRequestId.toString())).isEqualTo(2); + assertThat(sourceState(batchId)).isEqualTo(sourceBefore); } @Test @@ -63,6 +83,7 @@ class DigitalCurrencyGenerationStockIntegrationTest { batchId, 82L, "20260807_802", new BigDecimal("100.00"), 1, "100.00:1", "stock-user", 1001L, 2001L); insertCoin(8903L, batchId, 1, "100.00", "DC_SOURCE_003", "SOURCE_CURRENCY_003"); + List> sourceBefore = sourceState(batchId); InstitutionKeySubject subject = new InstitutionKeySubject("stock-user", 1001L, 2001L); IssuanceAuditActor actor = new IssuanceAuditActor("stock-user", "库存测试员"); gateway.reserveGeneratedCurrencies(subject, 82L, requestId, @@ -81,10 +102,13 @@ class DigitalCurrencyGenerationStockIntegrationTest { gateway.activateGeneratedCurrencies(subject, requestId, "BKCHCNBJ00001", "ORG_001", Arrays.asList(confirmed), actor); - assertThat(jdbcTemplate.queryForObject("SELECT status FROM standard_currency WHERE id = 8903", String.class)) - .isEqualTo("可用"); - assertThat(jdbcTemplate.queryForObject("SELECT issued_complete_currency FROM standard_currency WHERE id = 8903", String.class)) - .isEqualTo("SOURCE_CURRENCY_003|OWNERSHIP_SIGNATURE"); + assertThat(sourceState(batchId)).isEqualTo(sourceBefore); + assertThat(jdbcTemplate.queryForObject( + "SELECT status FROM issuance_source_currency_usage WHERE source_currency_id = 8903", + String.class)).isEqualTo("ISSUED"); + assertThat(jdbcTemplate.queryForObject( + "SELECT issuance_request_id FROM issuance_source_currency_usage WHERE source_currency_id = 8903", + String.class)).isEqualTo(requestId.toString()); assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM commercial_bank_currency WHERE currency_id = 'DC_SOURCE_003'", Integer.class)).isEqualTo(1); assertThat(jdbcTemplate.queryForObject( @@ -104,4 +128,10 @@ class DigitalCurrencyGenerationStockIntegrationTest { + "CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'tester', 'tester', FALSE)", id, batchId, sequence, new BigDecimal(denomination), currencyId, completeCurrency); } + + private List> sourceState(long batchId) { + return jdbcTemplate.queryForList("SELECT id, status, issuance_request_id, reserved_at, reserved_by_user_id, " + + "reserved_by, issued_at, issued_by_user_id, issued_by, ownership_signature, issued_complete_currency " + + "FROM standard_currency WHERE batch_id = ? ORDER BY id", batchId); + } } diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 3fc0ee1..e16cdb1 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -186,6 +186,23 @@ CREATE TABLE IF NOT EXISTS standard_currency ( deleted BOOLEAN NOT NULL DEFAULT FALSE ); +CREATE TABLE IF NOT EXISTS issuance_source_currency_usage ( + id BIGINT PRIMARY KEY, + source_currency_id BIGINT NOT NULL, + source_batch_id BIGINT NOT NULL, + issuance_request_id CHAR(36) NOT NULL, + status VARCHAR(16) NOT NULL, + reserved_at TIMESTAMP NOT NULL, + released_at TIMESTAMP NULL, + issued_at TIMESTAMP NULL, + audit_user_id VARCHAR(36) NOT NULL, + audit_username VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + CONSTRAINT uk_issuance_source_currency UNIQUE (source_currency_id), + INDEX idx_issuance_source_request (issuance_request_id, status) +); + CREATE TABLE IF NOT EXISTS institution_sm2_key_audit ( id BIGINT PRIMARY KEY, key_record_id BIGINT NOT NULL,