refactor: isolate issuance source currency usage

agent/payment-training-progress
chenyuan 2 weeks ago
parent dcd01e57be
commit 1e111da5bb

@ -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.DenominationItem;
import com.yau.digitalrmb.issuance.domain.model.DraftDigitalCurrency; import com.yau.digitalrmb.issuance.domain.model.DraftDigitalCurrency;
import com.yau.digitalrmb.issuance.domain.model.IssuanceAuditActor; 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.StandardCurrencyBatchEntity;
import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyBatchMapper; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyBatchMapper;
import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyEntity; import com.yau.digitalrmb.institutionidentity.infrastructure.StandardCurrencyEntity;
@ -37,6 +39,7 @@ public class DigitalCurrencyGenerationModuleGateway {
private final ControlSystemSignatureService controlSignatureService; private final ControlSystemSignatureService controlSignatureService;
private final StandardCurrencyBatchMapper standardCurrencyBatchMapper; private final StandardCurrencyBatchMapper standardCurrencyBatchMapper;
private final StandardCurrencyMapper standardCurrencyMapper; private final StandardCurrencyMapper standardCurrencyMapper;
private final IssuanceSourceCurrencyUsageMapper sourceUsageMapper;
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
@Autowired @Autowired
@ -45,19 +48,21 @@ public class DigitalCurrencyGenerationModuleGateway {
ControlSystemSignatureService controlSignatureService, ControlSystemSignatureService controlSignatureService,
StandardCurrencyBatchMapper standardCurrencyBatchMapper, StandardCurrencyBatchMapper standardCurrencyBatchMapper,
StandardCurrencyMapper standardCurrencyMapper, StandardCurrencyMapper standardCurrencyMapper,
IssuanceSourceCurrencyUsageMapper sourceUsageMapper,
JdbcTemplate jdbcTemplate) { JdbcTemplate jdbcTemplate) {
this.keyService = keyService; this.keyService = keyService;
this.quotaService = quotaService; this.quotaService = quotaService;
this.controlSignatureService = controlSignatureService; this.controlSignatureService = controlSignatureService;
this.standardCurrencyBatchMapper = standardCurrencyBatchMapper; this.standardCurrencyBatchMapper = standardCurrencyBatchMapper;
this.standardCurrencyMapper = standardCurrencyMapper; this.standardCurrencyMapper = standardCurrencyMapper;
this.sourceUsageMapper = sourceUsageMapper;
this.jdbcTemplate = jdbcTemplate; this.jdbcTemplate = jdbcTemplate;
} }
public DigitalCurrencyGenerationModuleGateway(InstitutionKeyService keyService, public DigitalCurrencyGenerationModuleGateway(InstitutionKeyService keyService,
QuotaControlBitService quotaService, QuotaControlBitService quotaService,
ControlSystemSignatureService controlSignatureService) { ControlSystemSignatureService controlSignatureService) {
this(keyService, quotaService, controlSignatureService, null, null, null); this(keyService, quotaService, controlSignatureService, null, null, null, null);
} }
public String signIssuanceDigest(InstitutionKeySubject subject, String digest) { public String signIssuanceDigest(InstitutionKeySubject subject, String digest) {
@ -82,7 +87,7 @@ public class DigitalCurrencyGenerationModuleGateway {
UUID requestId, UUID requestId,
List<DenominationItem> denominations, List<DenominationItem> denominations,
IssuanceAuditActor actor) { IssuanceAuditActor actor) {
if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null) { if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null || sourceUsageMapper == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成模块币串库存服务尚未配置"); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成模块币串库存服务尚未配置");
} }
requiredSubject(subject); requiredSubject(subject);
@ -102,39 +107,47 @@ public class DigitalCurrencyGenerationModuleGateway {
if (batch == null) { if (batch == null) {
throw validation("生成模块步骤九尚未生成与本次额度控制位对应的标准币串"); throw validation("生成模块步骤九尚未生成与本次额度控制位对应的标准币串");
} }
List<StandardCurrencyEntity> alreadyReserved = standardCurrencyMapper.selectList( List<StandardCurrencyEntity> alreadyReserved = sourceUsageMapper.selectReservedSources(
new LambdaQueryWrapper<StandardCurrencyEntity>() batch.getId(), requestId.toString());
.eq(StandardCurrencyEntity::getBatchId, batch.getId())
.eq(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString())
.eq(StandardCurrencyEntity::getStatus, "发行锁定")
.eq(StandardCurrencyEntity::getDeleted, false)
.orderByAsc(StandardCurrencyEntity::getSequenceNumber));
if (!alreadyReserved.isEmpty()) { if (!alreadyReserved.isEmpty()) {
assertExactQuantities(alreadyReserved, requested); assertExactQuantities(alreadyReserved, requested);
return stockItems(alreadyReserved); return stockItems(alreadyReserved);
} }
List<StandardCurrencyEntity> available = standardCurrencyMapper.selectList( List<StandardCurrencyEntity> available = sourceUsageMapper.selectAvailableSourcesForUpdate(batch.getId());
new LambdaQueryWrapper<StandardCurrencyEntity>()
.eq(StandardCurrencyEntity::getBatchId, batch.getId())
.eq(StandardCurrencyEntity::getStatus, "待生效")
.eq(StandardCurrencyEntity::getDeleted, false)
.orderByAsc(StandardCurrencyEntity::getSequenceNumber)
.last("FOR UPDATE"));
List<StandardCurrencyEntity> selected = selectExactQuantities(available, requested); List<StandardCurrencyEntity> selected = selectExactQuantities(available, requested);
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
for (StandardCurrencyEntity currency : selected) { for (StandardCurrencyEntity currency : selected) {
int updated = standardCurrencyMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyEntity>() IssuanceSourceCurrencyUsageEntity existing = sourceUsageMapper.selectBySourceCurrencyIdForUpdate(
.eq(StandardCurrencyEntity::getId, currency.getId()) currency.getId());
.eq(StandardCurrencyEntity::getStatus, "待生效") if (existing == null) {
.eq(StandardCurrencyEntity::getDeleted, false) IssuanceSourceCurrencyUsageEntity usage = new IssuanceSourceCurrencyUsageEntity();
.set(StandardCurrencyEntity::getStatus, "发行锁定") usage.setSourceCurrencyId(currency.getId());
.set(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString()) usage.setSourceBatchId(currency.getBatchId());
.set(StandardCurrencyEntity::getReservedAt, now) usage.setIssuanceRequestId(requestId.toString());
.set(StandardCurrencyEntity::getReservedByUserId, actor.getUserId()) usage.setStatus("RESERVED");
.set(StandardCurrencyEntity::getReservedBy, actor.getUsername()) usage.setReservedAt(now);
.set(StandardCurrencyEntity::getUpdatedAt, now) usage.setAuditUserId(actor.getUserId());
.set(StandardCurrencyEntity::getUpdatedBy, actor.getUsername())); usage.setAuditUsername(actor.getUsername());
if (updated != 1) throw validation("币串库存已变化,请重新获取后再发行"); usage.setCreatedAt(now);
usage.setUpdatedAt(now);
if (sourceUsageMapper.insert(usage) != 1) {
throw validation("币串库存已变化,请重新获取后再发行");
}
} else {
int reused = sourceUsageMapper.update(null,
new LambdaUpdateWrapper<IssuanceSourceCurrencyUsageEntity>()
.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); return stockItems(selected);
} }
@ -143,7 +156,8 @@ public class DigitalCurrencyGenerationModuleGateway {
public void activateGeneratedCurrencies(InstitutionKeySubject subject, UUID requestId, String bankCode, public void activateGeneratedCurrencies(InstitutionKeySubject subject, UUID requestId, String bankCode,
String institutionIdentifier, List<DraftDigitalCurrency> confirmedCoins, String institutionIdentifier, List<DraftDigitalCurrency> confirmedCoins,
IssuanceAuditActor actor) { IssuanceAuditActor actor) {
if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null || jdbcTemplate == null) { if (standardCurrencyBatchMapper == null || standardCurrencyMapper == null
|| sourceUsageMapper == null || jdbcTemplate == null) {
throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成模块币串发行服务尚未配置"); throw new BusinessException(ErrorCode.INTERNAL_ERROR, "生成模块币串发行服务尚未配置");
} }
requiredSubject(subject); requiredSubject(subject);
@ -162,15 +176,16 @@ public class DigitalCurrencyGenerationModuleGateway {
if (!requiredInstitution.equals(coin.getOrganizationId())) throw validation("发行币串机构标识不一致"); if (!requiredInstitution.equals(coin.getOrganizationId())) throw validation("发行币串机构标识不一致");
if (currencyCode == null) currencyCode = required(coin.getCurrency(), "发行币种"); if (currencyCode == null) currencyCode = required(coin.getCurrency(), "发行币种");
if (!currencyCode.equals(coin.getCurrency())) throw validation("同一发行批次币种不一致"); if (!currencyCode.equals(coin.getCurrency())) throw validation("同一发行批次币种不一致");
StandardCurrencyEntity source = standardCurrencyMapper.selectOne( StandardCurrencyEntity source = standardCurrencyMapper.selectById(coin.getSourceCurrencyId());
new LambdaQueryWrapper<StandardCurrencyEntity>() IssuanceSourceCurrencyUsageEntity usage = sourceUsageMapper.selectBySourceCurrencyIdForUpdate(
.eq(StandardCurrencyEntity::getId, coin.getSourceCurrencyId()) coin.getSourceCurrencyId());
.eq(StandardCurrencyEntity::getBatchId, coin.getSourceCurrencyBatchId())
.eq(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString())
.eq(StandardCurrencyEntity::getStatus, "发行锁定")
.eq(StandardCurrencyEntity::getDeleted, false)
.last("FOR UPDATE"));
if (source == null || !coin.getCoinId().equals(source.getCurrencyId()) 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()) || !requiredInstitution.equals(source.getInstitutionIdentifier())
|| !coin.getSourceCompleteCurrency().equals(source.getCompleteCurrency())) { || !coin.getSourceCompleteCurrency().equals(source.getCompleteCurrency())) {
throw validation("生成模块来源币串与发行记录不一致"); throw validation("生成模块来源币串与发行记录不一致");
@ -187,19 +202,16 @@ public class DigitalCurrencyGenerationModuleGateway {
for (int index = 0; index < sources.size(); index++) { for (int index = 0; index < sources.size(); index++) {
StandardCurrencyEntity source = sources.get(index); StandardCurrencyEntity source = sources.get(index);
DraftDigitalCurrency coin = confirmedCoins.get(index); DraftDigitalCurrency coin = confirmedCoins.get(index);
int updated = standardCurrencyMapper.update(null, new LambdaUpdateWrapper<StandardCurrencyEntity>() int updated = sourceUsageMapper.update(null,
.eq(StandardCurrencyEntity::getId, source.getId()) new LambdaUpdateWrapper<IssuanceSourceCurrencyUsageEntity>()
.eq(StandardCurrencyEntity::getStatus, "发行锁定") .eq(IssuanceSourceCurrencyUsageEntity::getSourceCurrencyId, source.getId())
.eq(StandardCurrencyEntity::getIssuanceRequestId, requestId.toString()) .eq(IssuanceSourceCurrencyUsageEntity::getIssuanceRequestId, requestId.toString())
.eq(StandardCurrencyEntity::getDeleted, false) .eq(IssuanceSourceCurrencyUsageEntity::getStatus, "RESERVED")
.set(StandardCurrencyEntity::getStatus, "可用") .set(IssuanceSourceCurrencyUsageEntity::getStatus, "ISSUED")
.set(StandardCurrencyEntity::getIssuedAt, now) .set(IssuanceSourceCurrencyUsageEntity::getIssuedAt, now)
.set(StandardCurrencyEntity::getIssuedByUserId, actor.getUserId()) .set(IssuanceSourceCurrencyUsageEntity::getAuditUserId, actor.getUserId())
.set(StandardCurrencyEntity::getIssuedBy, actor.getUsername()) .set(IssuanceSourceCurrencyUsageEntity::getAuditUsername, actor.getUsername())
.set(StandardCurrencyEntity::getOwnershipSignature, coin.getOwnershipSignature()) .set(IssuanceSourceCurrencyUsageEntity::getUpdatedAt, now));
.set(StandardCurrencyEntity::getIssuedCompleteCurrency, coin.getCompleteCurrency())
.set(StandardCurrencyEntity::getUpdatedAt, now)
.set(StandardCurrencyEntity::getUpdatedBy, actor.getUsername()));
if (updated != 1) throw validation("币串发行状态已变化,不能重复确权"); if (updated != 1) throw validation("币串发行状态已变化,不能重复确权");
jdbcTemplate.update("INSERT INTO commercial_bank_currency " jdbcTemplate.update("INSERT INTO commercial_bank_currency "
+ "(currency_id, source_currency_id, source_batch_id, issuance_request_id, bank_code, " + "(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<IssuanceSourceCurrencyUsageEntity>()
.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) { private void assertOwnedBatch(Long batchId, InstitutionKeySubject subject) {
StandardCurrencyBatchEntity batch = standardCurrencyBatchMapper.selectById(batchId); StandardCurrencyBatchEntity batch = standardCurrencyBatchMapper.selectById(batchId);
if (batch == null || !subject.getUserId().equals(batch.getUserId()) if (batch == null || !subject.getUserId().equals(batch.getUserId())

@ -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;
}

@ -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<IssuanceSourceCurrencyUsageEntity> {
@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<StandardCurrencyEntity> 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<StandardCurrencyEntity> 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);
}

@ -39,3 +39,20 @@ CREATE TABLE IF NOT EXISTS training_experiment_action (
CONSTRAINT fk_experiment_action_attempt CONSTRAINT fk_experiment_action_attempt
FOREIGN KEY (attempt_id) REFERENCES training_experiment_attempt(id) 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)
);

@ -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) 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 ( CREATE TABLE IF NOT EXISTS commercial_bank_currency (
currency_id VARCHAR(96) PRIMARY KEY, currency_id VARCHAR(96) PRIMARY KEY,
source_currency_id BIGINT NOT NULL UNIQUE, source_currency_id BIGINT NOT NULL UNIQUE,

@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.time.Instant; import java.time.Instant;
@ -38,6 +39,7 @@ class DigitalCurrencyGenerationStockIntegrationTest {
"stock-user", 1001L, 2001L); "stock-user", 1001L, 2001L);
insertCoin(8901L, batchId, 1, "100.00", "DC_SOURCE_001", "SOURCE_CURRENCY_001"); insertCoin(8901L, batchId, 1, "100.00", "DC_SOURCE_001", "SOURCE_CURRENCY_001");
insertCoin(8902L, batchId, 2, "50.00", "DC_SOURCE_002", "SOURCE_CURRENCY_002"); insertCoin(8902L, batchId, 2, "50.00", "DC_SOURCE_002", "SOURCE_CURRENCY_002");
List<Map<String, Object>> sourceBefore = sourceState(batchId);
List<GeneratedCurrencyStockItem> reserved = gateway.reserveGeneratedCurrencies( List<GeneratedCurrencyStockItem> reserved = gateway.reserveGeneratedCurrencies(
new InstitutionKeySubject("stock-user", 1001L, 2001L), 81L, requestId, new InstitutionKeySubject("stock-user", 1001L, 2001L), 81L, requestId,
@ -47,9 +49,27 @@ class DigitalCurrencyGenerationStockIntegrationTest {
assertThat(reserved).extracting(GeneratedCurrencyStockItem::getCurrencyId) assertThat(reserved).extracting(GeneratedCurrencyStockItem::getCurrencyId)
.containsExactly("DC_SOURCE_001", "DC_SOURCE_002"); .containsExactly("DC_SOURCE_001", "DC_SOURCE_002");
assertThat(sourceState(batchId)).isEqualTo(sourceBefore);
assertThat(jdbcTemplate.queryForObject( 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); 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 @Test
@ -63,6 +83,7 @@ class DigitalCurrencyGenerationStockIntegrationTest {
batchId, 82L, "20260807_802", new BigDecimal("100.00"), 1, "100.00:1", batchId, 82L, "20260807_802", new BigDecimal("100.00"), 1, "100.00:1",
"stock-user", 1001L, 2001L); "stock-user", 1001L, 2001L);
insertCoin(8903L, batchId, 1, "100.00", "DC_SOURCE_003", "SOURCE_CURRENCY_003"); insertCoin(8903L, batchId, 1, "100.00", "DC_SOURCE_003", "SOURCE_CURRENCY_003");
List<Map<String, Object>> sourceBefore = sourceState(batchId);
InstitutionKeySubject subject = new InstitutionKeySubject("stock-user", 1001L, 2001L); InstitutionKeySubject subject = new InstitutionKeySubject("stock-user", 1001L, 2001L);
IssuanceAuditActor actor = new IssuanceAuditActor("stock-user", "库存测试员"); IssuanceAuditActor actor = new IssuanceAuditActor("stock-user", "库存测试员");
gateway.reserveGeneratedCurrencies(subject, 82L, requestId, gateway.reserveGeneratedCurrencies(subject, 82L, requestId,
@ -81,10 +102,13 @@ class DigitalCurrencyGenerationStockIntegrationTest {
gateway.activateGeneratedCurrencies(subject, requestId, "BKCHCNBJ00001", "ORG_001", gateway.activateGeneratedCurrencies(subject, requestId, "BKCHCNBJ00001", "ORG_001",
Arrays.asList(confirmed), actor); Arrays.asList(confirmed), actor);
assertThat(jdbcTemplate.queryForObject("SELECT status FROM standard_currency WHERE id = 8903", String.class)) assertThat(sourceState(batchId)).isEqualTo(sourceBefore);
.isEqualTo("可用"); assertThat(jdbcTemplate.queryForObject(
assertThat(jdbcTemplate.queryForObject("SELECT issued_complete_currency FROM standard_currency WHERE id = 8903", String.class)) "SELECT status FROM issuance_source_currency_usage WHERE source_currency_id = 8903",
.isEqualTo("SOURCE_CURRENCY_003|OWNERSHIP_SIGNATURE"); 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'", assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) FROM commercial_bank_currency WHERE currency_id = 'DC_SOURCE_003'",
Integer.class)).isEqualTo(1); Integer.class)).isEqualTo(1);
assertThat(jdbcTemplate.queryForObject( assertThat(jdbcTemplate.queryForObject(
@ -104,4 +128,10 @@ class DigitalCurrencyGenerationStockIntegrationTest {
+ "CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'tester', 'tester', FALSE)", + "CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'tester', 'tester', FALSE)",
id, batchId, sequence, new BigDecimal(denomination), currencyId, completeCurrency); id, batchId, sequence, new BigDecimal(denomination), currencyId, completeCurrency);
} }
private List<Map<String, Object>> 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);
}
} }

@ -186,6 +186,23 @@ CREATE TABLE IF NOT EXISTS standard_currency (
deleted BOOLEAN NOT NULL DEFAULT FALSE 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 ( CREATE TABLE IF NOT EXISTS institution_sm2_key_audit (
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
key_record_id BIGINT NOT NULL, key_record_id BIGINT NOT NULL,

Loading…
Cancel
Save