From 2289aa94a1e15a243a170d7f3a410f13755ad0c4 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Thu, 27 Aug 2026 10:31:08 +0800 Subject: [PATCH] feat(issuance): add full training reset --- .../IssuanceTrainingResetResult.java | 26 ++ .../service/IssuanceTrainingResetService.java | 197 ++++++++++++++++ .../rest/IssuanceTrainingResetController.java | 59 +++++ .../IssuanceTrainingResetServiceTest.java | 223 ++++++++++++++++++ ...IssuanceTrainingAttemptControllerTest.java | 4 + 5 files changed, 509 insertions(+) create mode 100644 src/main/java/com/yau/digitalrmb/issuance/application/IssuanceTrainingResetResult.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetService.java create mode 100644 src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingResetController.java create mode 100644 src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetServiceTest.java diff --git a/src/main/java/com/yau/digitalrmb/issuance/application/IssuanceTrainingResetResult.java b/src/main/java/com/yau/digitalrmb/issuance/application/IssuanceTrainingResetResult.java new file mode 100644 index 0000000..e987c94 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/application/IssuanceTrainingResetResult.java @@ -0,0 +1,26 @@ +package com.yau.digitalrmb.issuance.application; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.math.BigDecimal; + +@Schema(description = "货币发行实验重置结果") +public class IssuanceTrainingResetResult { + private final boolean reset; + private final int deletedRecordCount; + private final BigDecimal restoredReserveAmount; + private final BigDecimal restoredCurrencyAmount; + + public IssuanceTrainingResetResult(int deletedRecordCount, BigDecimal restoredReserveAmount, + BigDecimal restoredCurrencyAmount) { + this.reset = true; + this.deletedRecordCount = deletedRecordCount; + this.restoredReserveAmount = restoredReserveAmount; + this.restoredCurrencyAmount = restoredCurrencyAmount; + } + + public boolean isReset() { return reset; } + public int getDeletedRecordCount() { return deletedRecordCount; } + public BigDecimal getRestoredReserveAmount() { return restoredReserveAmount; } + public BigDecimal getRestoredCurrencyAmount() { return restoredCurrencyAmount; } +} diff --git a/src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetService.java b/src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetService.java new file mode 100644 index 0000000..ef96b0a --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetService.java @@ -0,0 +1,197 @@ +package com.yau.digitalrmb.issuance.application.service; + +import com.yau.digitalrmb.issuance.application.IssuanceTrainingResetResult; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import com.yau.digitalrmb.training.attempt.domain.ExperimentModule; +import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Service +public class IssuanceTrainingResetService { + private static final String PROGRESS_MODULE_CODE = "PBOC_CURRENCY_ISSUANCE"; + + private final JdbcTemplate jdbc; + + public IssuanceTrainingResetService(JdbcTemplate jdbc) { + this.jdbc = jdbc; + } + + @Transactional + public IssuanceTrainingResetResult reset(ExperimentSubject subject) { + requireSubject(subject); + List attempts = jdbc.query( + "SELECT id,business_id FROM training_experiment_attempt WHERE module_code=? " + + "AND user_id=? AND school_id=? AND class_id=? AND organization_id=? FOR UPDATE", + (rs, rowNum) -> new AttemptRecord(rs.getString("id"), rs.getString("business_id")), + ExperimentModule.ISSUANCE.name(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(), + subject.getOrganizationId()); + Set requestIds = new LinkedHashSet(); + for (AttemptRecord attempt : attempts) { + if (attempt.businessId != null && !attempt.businessId.trim().isEmpty()) { + requestIds.add(attempt.businessId.trim()); + } + } + + for (String requestId : requestIds) { + requireOwnedRequest(requestId, subject.getUserId()); + requireUnusedByFollowingModules(requestId); + } + + BigDecimal restoredReserve = BigDecimal.ZERO.setScale(2); + BigDecimal restoredCurrency = BigDecimal.ZERO.setScale(2); + int affected = 0; + for (String requestId : requestIds) { + for (ReserveRestoration value : reserveRestorations(requestId)) { + int updated = jdbc.update("UPDATE central_bank_institution_account " + + "SET reserve_balance=reserve_balance+? WHERE bank_code=?", + value.amount, value.bankCode); + if (updated != 1) throw internal("恢复商业银行准备金余额失败:" + value.bankCode); + restoredReserve = restoredReserve.add(value.amount); + } + for (CurrencyRestoration value : currencyRestorations(requestId)) { + int vaultUpdated = jdbc.update("UPDATE central_bank_currency_vault " + + "SET available_balance=available_balance+? WHERE currency=?", + value.amount, value.currency); + if (vaultUpdated != 1) throw internal("恢复央行发行库余额失败:" + value.currency); + int bankUpdated = jdbc.update("UPDATE issuance_bank_inventory " + + "SET current_balance=current_balance-?,updated_at=CURRENT_TIMESTAMP " + + "WHERE bank_code=? AND current_balance>=?", + value.amount, value.bankCode, value.amount); + if (bankUpdated != 1) throw internal("恢复商业银行数字货币库存失败:" + value.bankCode); + restoredCurrency = restoredCurrency.add(value.amount); + } + affected += deleteRequestData(requestId, subject.getUserId()); + } + + for (AttemptRecord attempt : attempts) { + affected += jdbc.update("DELETE FROM training_experiment_action WHERE attempt_id=?", attempt.id); + } + affected += jdbc.update("DELETE FROM training_experiment_attempt WHERE module_code=? " + + "AND user_id=? AND school_id=? AND class_id=? AND organization_id=?", + ExperimentModule.ISSUANCE.name(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(), + subject.getOrganizationId()); + affected += jdbc.update("DELETE FROM training_task_step_progress WHERE user_id=? AND module_code=?", + subject.getUserId(), PROGRESS_MODULE_CODE); + return new IssuanceTrainingResetResult(affected, restoredReserve, restoredCurrency); + } + + private void requireOwnedRequest(String requestId, String userId) { + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM issuance_request " + + "WHERE id=? AND created_by_user_id=?", Integer.class, requestId, userId); + if (count == null || count != 1) { + throw new BusinessException(ErrorCode.UNAUTHORIZED, "发行申请不存在或不属于当前用户"); + } + } + + private void requireUnusedByFollowingModules(String requestId) { + Integer issued = jdbc.queryForObject("SELECT COUNT(*) FROM commercial_bank_currency " + + "WHERE issuance_request_id=?", Integer.class, requestId); + Integer resettable = jdbc.queryForObject("SELECT COUNT(*) FROM commercial_bank_currency c " + + "JOIN central_bank_currency_ownership o ON o.currency_id=c.currency_id " + + "WHERE c.issuance_request_id=? AND c.status='可用' AND o.owner_type='BANK' " + + "AND o.owner_id=c.bank_code AND o.status='AVAILABLE' AND o.lock_business_type IS NULL " + + "AND o.lock_business_id IS NULL AND o.last_transaction_id IS NULL", Integer.class, requestId); + if (!safeEquals(issued, resettable)) { + throw validation("模块二发行的数字货币已被后续模块使用,不能从模块二重置"); + } + Integer exchangeReferences = jdbc.queryForObject("SELECT COUNT(*) FROM exchange_coin_reservation r " + + "JOIN commercial_bank_currency c ON c.currency_id=r.currency_id " + + "WHERE c.issuance_request_id=?", Integer.class, requestId); + Integer paymentReferences = jdbc.queryForObject("SELECT COUNT(*) FROM payment_coin_reservation r " + + "JOIN commercial_bank_currency c ON c.currency_id=r.currency_id " + + "WHERE c.issuance_request_id=?", Integer.class, requestId); + if (positive(exchangeReferences) || positive(paymentReferences)) { + throw validation("模块二发行的数字货币已产生兑换或支付记录,不能从模块二重置"); + } + } + + private List reserveRestorations(String requestId) { + return jdbc.query("SELECT n.bank_code,e.deduction_amount FROM reserve_deduction_execution e " + + "JOIN reserve_deduction_notification n ON n.request_id=e.request_id " + + "WHERE e.request_id=? AND e.status='DEDUCTED' FOR UPDATE", + (rs, rowNum) -> new ReserveRestoration(rs.getString("bank_code"), + rs.getBigDecimal("deduction_amount")), requestId); + } + + private List currencyRestorations(String requestId) { + return jdbc.query("SELECT bank_code,currency,SUM(denomination) amount " + + "FROM commercial_bank_currency WHERE issuance_request_id=? " + + "GROUP BY bank_code,currency", + (rs, rowNum) -> new CurrencyRestoration(rs.getString("bank_code"), rs.getString("currency"), + rs.getBigDecimal("amount")), requestId); + } + + private int deleteRequestData(String requestId, String userId) { + int count = 0; + count += jdbc.update("DELETE FROM central_bank_currency_ownership WHERE currency_id IN " + + "(SELECT currency_id FROM commercial_bank_currency WHERE issuance_request_id=?)", requestId); + count += jdbc.update("DELETE FROM commercial_bank_currency WHERE issuance_request_id=?", requestId); + count += jdbc.update("DELETE FROM digital_currency_ownership_confirmation WHERE request_id=?", requestId); + count += jdbc.update("DELETE FROM draft_digital_currency WHERE batch_id IN " + + "(SELECT batch_id FROM digital_currency_production_batch WHERE request_id=?)", requestId); + count += jdbc.update("DELETE FROM digital_currency_production_batch WHERE request_id=?", requestId); + count += jdbc.update("DELETE FROM issuance_source_currency_usage WHERE issuance_request_id=?", requestId); + count += jdbc.update("DELETE FROM reserve_deduction_execution WHERE request_id=?", requestId); + count += jdbc.update("DELETE FROM reserve_deduction_notification WHERE request_id=?", requestId); + count += jdbc.update("DELETE FROM central_bank_issuance_business_review WHERE request_id=?", requestId); + count += jdbc.update("DELETE FROM central_bank_issuance_verification WHERE request_id=?", requestId); + count += jdbc.update("DELETE FROM central_bank_issuance_receipt WHERE request_id=?", requestId); + count += jdbc.update("DELETE FROM issuance_request_denomination WHERE request_id=?", requestId); + count += jdbc.update("UPDATE issuance_request SET deleted=TRUE,updated_at=CURRENT_TIMESTAMP," + + "updated_by_user_id=?,updated_by=? WHERE id=? AND created_by_user_id=? AND deleted=FALSE", + userId, userId, requestId, userId); + return count; + } + + private void requireSubject(ExperimentSubject subject) { + if (subject == null || subject.getUserId() == null || subject.getUserId().trim().isEmpty() + || subject.getSchoolId() <= 0 || subject.getClassId() <= 0) { + throw new BusinessException(ErrorCode.UNAUTHORIZED, "缺少货币发行实验主体信息"); + } + } + + private boolean safeEquals(Integer left, Integer right) { + return left != null && right != null && left.intValue() == right.intValue(); + } + + private boolean positive(Integer value) { return value != null && value > 0; } + + private BusinessException validation(String message) { + return new BusinessException(ErrorCode.VALIDATION_ERROR, message); + } + + private BusinessException internal(String message) { + return new BusinessException(ErrorCode.INTERNAL_ERROR, message); + } + + private static final class AttemptRecord { + private final String id; + private final String businessId; + private AttemptRecord(String id, String businessId) { this.id = id; this.businessId = businessId; } + } + + private static final class ReserveRestoration { + private final String bankCode; + private final BigDecimal amount; + private ReserveRestoration(String bankCode, BigDecimal amount) { + this.bankCode = bankCode; this.amount = amount; + } + } + + private static final class CurrencyRestoration { + private final String bankCode; + private final String currency; + private final BigDecimal amount; + private CurrencyRestoration(String bankCode, String currency, BigDecimal amount) { + this.bankCode = bankCode; this.currency = currency; this.amount = amount; + } + } +} diff --git a/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingResetController.java b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingResetController.java new file mode 100644 index 0000000..23a2252 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingResetController.java @@ -0,0 +1,59 @@ +package com.yau.digitalrmb.issuance.interfaces.rest; + +import com.yau.digitalrmb.issuance.application.IssuanceTrainingResetResult; +import com.yau.digitalrmb.issuance.application.service.IssuanceTrainingResetService; +import com.yau.digitalrmb.security.application.CurrentUser; +import com.yau.digitalrmb.security.application.CurrentUserService; +import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import com.yau.digitalrmb.shared.web.TraceIdFilter; +import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/issuance/training") +@Tag(name = "数字货币发行实验管理") +public class IssuanceTrainingResetController { + private final IssuanceTrainingResetService resetService; + private final CurrentUserService currentUserService; + + public IssuanceTrainingResetController(IssuanceTrainingResetService resetService, + CurrentUserService currentUserService) { + this.resetService = resetService; + this.currentUserService = currentUserService; + } + + @PostMapping("/reset") + @Operation(summary = "重新开始货币发行实验", + description = "恢复当前实训主体在模块二产生的准备金和数字货币库存,删除发行流程业务及操作数据;模块一前置数据保持不变。") + public ApiResponse reset() { + CurrentUser user = currentUserService.getCurrentUser(); + IssuanceTrainingResetResult result = resetService.reset(new ExperimentSubject( + required(user.getUserId(), "用户 ID"), numeric(user.getSchoolId(), "学校 ID"), + numeric(user.getClassId(), "班级 ID"), "")); + return ApiResponse.success(result, MDC.get(TraceIdFilter.MDC_KEY)); + } + + private long numeric(String value, String name) { + try { + long result = Long.parseLong(required(value, name)); + if (result <= 0) throw new NumberFormatException(name); + return result; + } catch (NumberFormatException exception) { + throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录凭据中的" + name + "无效"); + } + } + + private String required(String value, String name) { + if (value == null || value.trim().isEmpty()) { + throw new BusinessException(ErrorCode.UNAUTHORIZED, "登录凭据缺少" + name); + } + return value.trim(); + } +} diff --git a/src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetServiceTest.java b/src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetServiceTest.java new file mode 100644 index 0000000..420664a --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/issuance/application/service/IssuanceTrainingResetServiceTest.java @@ -0,0 +1,223 @@ +package com.yau.digitalrmb.issuance.application.service; + +import com.yau.digitalrmb.issuance.application.IssuanceTrainingResetResult; +import com.yau.digitalrmb.shared.exception.BusinessException; +import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +class IssuanceTrainingResetServiceTest { + private static final String USER_ID = "00000000-0000-0000-0000-000000009901"; + private static final String OTHER_USER_ID = "00000000-0000-0000-0000-000000009902"; + private static final long SCHOOL_ID = 9901L; + private static final long CLASS_ID = 9902L; + private static final String REQUEST_ID = "00000000-0000-0000-0000-000000009911"; + private static final String OTHER_REQUEST_ID = "00000000-0000-0000-0000-000000009912"; + private static final String ATTEMPT_ID = "00000000-0000-0000-0000-000000009921"; + private static final String OTHER_ATTEMPT_ID = "00000000-0000-0000-0000-000000009922"; + private static final String BANK_CODE = "RSTBANK9901"; + private static final String ORGANIZATION_ID = "ORG_RESET_9901"; + private static final String CURRENCY = "RST9901"; + private static final String TRANSACTION_ID = "RST_TX_9901"; + private static final String BATCH_ID = "RST_BATCH_9901"; + private static final String COIN_ID = "RST_COIN_9901"; + private static final long SOURCE_BATCH_ID = 9901001L; + private static final long SOURCE_CURRENCY_ID = 9901002L; + + @Autowired private JdbcTemplate jdbc; + @Autowired private IssuanceTrainingResetService service; + + @Test + void restoresCompletedIssuanceAndDeletesOnlyCurrentSubjectsOperationData() { + seedCompletedIssuance("BANK", "AVAILABLE", null); + seedOtherUsersRequestAndAttempt(); + + IssuanceTrainingResetResult result = service.reset(subject()); + + assertThat(result.isReset()).isTrue(); + assertThat(result.getRestoredReserveAmount()).isEqualByComparingTo("100.00"); + assertThat(result.getRestoredCurrencyAmount()).isEqualByComparingTo("100.00"); + assertThat(result.getDeletedRecordCount()).isPositive(); + assertThat(decimal("SELECT reserve_balance FROM central_bank_institution_account WHERE bank_code=?", BANK_CODE)) + .isEqualByComparingTo("1000.00"); + assertThat(decimal("SELECT current_balance FROM issuance_bank_inventory WHERE bank_code=?", BANK_CODE)) + .isEqualByComparingTo("0.00"); + assertThat(decimal("SELECT available_balance FROM central_bank_currency_vault WHERE currency=?", CURRENCY)) + .isEqualByComparingTo("1000.00"); + assertThat(count("SELECT COUNT(*) FROM standard_currency WHERE id=?", SOURCE_CURRENCY_ID)).isEqualTo(1); + assertThat(count("SELECT COUNT(*) FROM issuance_source_currency_usage WHERE issuance_request_id=?", REQUEST_ID)) + .isZero(); + assertThat(count("SELECT COUNT(*) FROM commercial_bank_currency WHERE issuance_request_id=?", REQUEST_ID)) + .isZero(); + assertThat(count("SELECT COUNT(*) FROM reserve_deduction_execution WHERE request_id=?", REQUEST_ID)) + .isZero(); + assertThat(count("SELECT COUNT(*) FROM training_experiment_attempt WHERE id=?", ATTEMPT_ID)).isZero(); + assertThat(count("SELECT COUNT(*) FROM training_experiment_action WHERE attempt_id=?", ATTEMPT_ID)).isZero(); + assertThat(count("SELECT COUNT(*) FROM training_task_step_progress WHERE user_id=? " + + "AND module_code='PBOC_CURRENCY_ISSUANCE'", USER_ID)).isZero(); + assertThat(count("SELECT COUNT(*) FROM issuance_request WHERE id=? AND deleted=TRUE", REQUEST_ID)) + .isEqualTo(1); + assertThat(count("SELECT COUNT(*) FROM issuance_request WHERE id=? AND deleted=FALSE", OTHER_REQUEST_ID)) + .isEqualTo(1); + assertThat(count("SELECT COUNT(*) FROM training_experiment_attempt WHERE id=?", OTHER_ATTEMPT_ID)) + .isEqualTo(1); + + IssuanceTrainingResetResult repeated = service.reset(subject()); + assertThat(repeated.getDeletedRecordCount()).isZero(); + assertThat(repeated.getRestoredReserveAmount()).isEqualByComparingTo("0.00"); + assertThat(repeated.getRestoredCurrencyAmount()).isEqualByComparingTo("0.00"); + } + + @Test + void rejectsResetWhenIssuedCurrencyWasUsedByFollowingModule() { + seedCompletedIssuance("WALLET", "AVAILABLE", "EXCHANGE_9901"); + + assertThatThrownBy(() -> service.reset(subject())) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("后续模块"); + + assertThat(decimal("SELECT reserve_balance FROM central_bank_institution_account WHERE bank_code=?", BANK_CODE)) + .isEqualByComparingTo("900.00"); + assertThat(count("SELECT COUNT(*) FROM issuance_request WHERE id=? AND deleted=FALSE", REQUEST_ID)) + .isEqualTo(1); + assertThat(count("SELECT COUNT(*) FROM commercial_bank_currency WHERE issuance_request_id=?", REQUEST_ID)) + .isEqualTo(1); + assertThat(count("SELECT COUNT(*) FROM training_experiment_attempt WHERE id=?", ATTEMPT_ID)).isEqualTo(1); + } + + private void seedCompletedIssuance(String ownerType, String ownerStatus, String lastTransactionId) { + jdbc.update("INSERT INTO central_bank_institution_account " + + "(bank_code,organization_id,reserve_account_no,reserve_account_name,status,opened_on,reserve_balance) " + + "VALUES (?,?,?,?,?,'2020-01-01',?)", + BANK_CODE, ORGANIZATION_ID, "RESERVE_RESET_9901", "重置测试准备金账户", "NORMAL", new BigDecimal("900.00")); + jdbc.update("INSERT INTO issuance_bank_inventory (bank_code,current_balance,warning_threshold,updated_at) " + + "VALUES (?,?,?,CURRENT_TIMESTAMP)", BANK_CODE, new BigDecimal("100.00"), BigDecimal.ZERO); + jdbc.update("INSERT INTO central_bank_currency_vault (currency,available_balance) VALUES (?,?)", + CURRENCY, new BigDecimal("900.00")); + jdbc.update("INSERT INTO standard_currency_batch " + + "(id,quota_control_bit_id,batch_number,total_amount,total_count,denomination_summary,status," + + "user_id,school_id,class_id,created_at,updated_at,created_by,updated_by,deleted) " + + "VALUES (?,?,?,?,?,?,?, ?,?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,?,?,FALSE)", + SOURCE_BATCH_ID, 9901000L, "RST_STD_BATCH", new BigDecimal("100.00"), 1, "100:1", "GENERATED", + USER_ID, SCHOOL_ID, CLASS_ID, "tester", "tester"); + jdbc.update("INSERT INTO standard_currency " + + "(id,batch_id,sequence_number,denomination,random_check_code,currency_id,institution_identifier," + + "bank_signature_segment,central_bank_signature_segment,complete_currency,status,created_at," + + "updated_at,created_by,updated_by,deleted) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,?,?,FALSE)", + SOURCE_CURRENCY_ID, SOURCE_BATCH_ID, 1, new BigDecimal("100.00"), "990101", COIN_ID, + ORGANIZATION_ID, "BANK_SIG", "CB_SIG", "SOURCE_COMPLETE", "待生效", "tester", "tester"); + insertRequest(REQUEST_ID, "RST_REQ_9901", USER_ID); + jdbc.update("INSERT INTO issuance_request_denomination " + + "(request_id,denomination,quantity,created_at,updated_at,created_by,updated_by,created_by_user_id,updated_by_user_id) " + + "VALUES (?,?,1,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'tester','tester',?,?)", + REQUEST_ID, new BigDecimal("100.00"), USER_ID, USER_ID); + jdbc.update("INSERT INTO central_bank_issuance_receipt " + + "(request_id,request_no,payload_json,received_at,received_by_user_id,received_by) " + + "VALUES (?,?,'{}',CURRENT_TIMESTAMP,?,'tester')", REQUEST_ID, "RST_REQ_9901", USER_ID); + jdbc.update("INSERT INTO central_bank_issuance_verification " + + "(request_id,digest_matched,signature_verified,status,verified_at,verified_by_user_id,verified_by) " + + "VALUES (?,TRUE,TRUE,'PASSED',CURRENT_TIMESTAMP,?,'tester')", REQUEST_ID, USER_ID); + jdbc.update("INSERT INTO central_bank_issuance_business_review " + + "(request_id,account_valid,vault_sufficient,amount_consistent,status,reviewed_at,reviewed_by_user_id,reviewed_by) " + + "VALUES (?,TRUE,TRUE,TRUE,'PASSED',CURRENT_TIMESTAMP,?,'tester')", REQUEST_ID, USER_ID); + jdbc.update("INSERT INTO reserve_deduction_notification " + + "(request_id,transaction_id,bank_code,organization_id,reserve_account_no,reserve_account_name,currency," + + "deduction_amount,status,sent_at,sent_by_user_id,sent_by) " + + "VALUES (?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,?,'tester')", + REQUEST_ID, TRANSACTION_ID, BANK_CODE, ORGANIZATION_ID, "RESERVE_RESET_9901", + "重置测试准备金账户", CURRENCY, new BigDecimal("100.00"), "SUBMITTED", USER_ID); + jdbc.update("INSERT INTO reserve_deduction_execution " + + "(transaction_id,request_id,before_balance,deduction_amount,after_balance,status,executed_at,executed_by_user_id,executed_by) " + + "VALUES (?,?,?,?,?,'DEDUCTED',CURRENT_TIMESTAMP,?,'tester')", + TRANSACTION_ID, REQUEST_ID, new BigDecimal("1000.00"), new BigDecimal("100.00"), + new BigDecimal("900.00"), USER_ID); + jdbc.update("INSERT INTO digital_currency_production_batch " + + "(batch_id,request_id,deduction_transaction_id,organization_id,currency,total_amount,status,created_at," + + "created_by_user_id,created_by,quota_control_bit_id,control_system_signature_id,complete_control_bit,control_system_signature) " + + "VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP,?,'tester',?,?,?,?)", + BATCH_ID, REQUEST_ID, TRANSACTION_ID, ORGANIZATION_ID, CURRENCY, new BigDecimal("100.00"), + "OWNERSHIP_CONFIRMED", USER_ID, 9901000L, 9901003L, "CONTROL_BIT", "CONTROL_SIG"); + jdbc.update("INSERT INTO draft_digital_currency " + + "(coin_id,batch_id,sequence_no,denomination,organization_id,currency,status,created_at,created_by_user_id,created_by," + + "source_currency_id,source_currency_batch_id,source_complete_currency,complete_control_bit,control_system_signature," + + "ownership_original_text,ownership_signature,complete_currency,confirmed_at,confirmed_by_user_id,confirmed_by) " + + "VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP,?,'tester',?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP,?,'tester')", + COIN_ID, BATCH_ID, 1, new BigDecimal("100.00"), ORGANIZATION_ID, CURRENCY, "AVAILABLE", USER_ID, + SOURCE_CURRENCY_ID, SOURCE_BATCH_ID, "SOURCE_COMPLETE", "CONTROL_BIT", "CONTROL_SIG", "OWNERSHIP_TEXT", + "OWNERSHIP_SIG", "COMPLETE_CURRENCY", USER_ID); + jdbc.update("INSERT INTO digital_currency_ownership_confirmation " + + "(request_id,batch_id,status,confirmed_at,confirmed_by_user_id,confirmed_by,signing_key_id,central_bank_signature) " + + "VALUES (?,?,'RECEIPT_SENT',CURRENT_TIMESTAMP,?,'tester','CENTRAL_FIRST_SM2_KEY','CB_FINAL_SIG')", + REQUEST_ID, BATCH_ID, USER_ID); + jdbc.update("INSERT INTO issuance_source_currency_usage " + + "(id,source_currency_id,source_batch_id,issuance_request_id,status,reserved_at,issued_at,audit_user_id,audit_username,created_at,updated_at) " + + "VALUES (?,?,?,?, 'ISSUED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,?,'tester',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", + 9901004L, SOURCE_CURRENCY_ID, SOURCE_BATCH_ID, REQUEST_ID, USER_ID); + jdbc.update("INSERT INTO commercial_bank_currency " + + "(currency_id,source_currency_id,source_batch_id,issuance_request_id,bank_code,organization_id,denomination,currency," + + "complete_currency,issuance_ownership_original_text,issuance_ownership_signature,status,received_at,received_by_user_id,received_by) " + + "VALUES (?,?,?,?,?,?,?,?,?,?,?,'可用',CURRENT_TIMESTAMP,?,'tester')", + COIN_ID, SOURCE_CURRENCY_ID, SOURCE_BATCH_ID, REQUEST_ID, BANK_CODE, ORGANIZATION_ID, + new BigDecimal("100.00"), CURRENCY, "COMPLETE_CURRENCY", "OWNERSHIP_TEXT", "OWNERSHIP_SIG", USER_ID); + jdbc.update("INSERT INTO central_bank_currency_ownership " + + "(currency_id,owner_type,owner_id,status,lock_business_type,lock_business_id,last_transaction_id,updated_at) " + + "VALUES (?,?,?,?,NULL,NULL,?,CURRENT_TIMESTAMP)", + COIN_ID, ownerType, "BANK".equals(ownerType) ? BANK_CODE : "WALLET_9901", ownerStatus, + lastTransactionId); + insertAttempt(ATTEMPT_ID, USER_ID, REQUEST_ID); + jdbc.update("INSERT INTO training_experiment_action " + + "(id,attempt_id,step_code,action_code,status,request_fingerprint,started_at,completed_at,updated_at,version) " + + "VALUES ('00000000-0000-0000-0000-000000009931',?,'07','sign-bank-receipt','COMPLETED'," + + "'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,0)", + ATTEMPT_ID); + jdbc.update("INSERT INTO training_task_step_progress (user_id,module_code,step_code,completed_at) " + + "VALUES (?,'PBOC_CURRENCY_ISSUANCE','07',CURRENT_TIMESTAMP)", USER_ID); + } + + private void seedOtherUsersRequestAndAttempt() { + insertRequest(OTHER_REQUEST_ID, "RST_REQ_9902", OTHER_USER_ID); + insertAttempt(OTHER_ATTEMPT_ID, OTHER_USER_ID, OTHER_REQUEST_ID); + } + + private void insertRequest(String requestId, String requestNo, String userId) { + jdbc.update("INSERT INTO issuance_request " + + "(id,request_no,bank_code,organization_id,total_amount,currency,status,central_receive_status," + + "created_at,updated_at,created_by,updated_by,created_by_user_id,updated_by_user_id,deleted) " + + "VALUES (?,?,?,?,?,?,'SENT','RECEIVED',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'tester','tester',?,?,FALSE)", + requestId, requestNo, BANK_CODE, ORGANIZATION_ID, new BigDecimal("100.00"), CURRENCY, userId, userId); + } + + private void insertAttempt(String attemptId, String userId, String requestId) { + jdbc.update("INSERT INTO training_experiment_attempt " + + "(id,module_code,user_id,school_id,class_id,organization_id,attempt_no,status,current_step_code,current_action_code," + + "business_id,version,created_at,updated_at,completed_at) " + + "VALUES (?,'ISSUANCE',?,?,?,'',1,'COMPLETED','07','sign-bank-receipt',?,0,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", + attemptId, userId, SCHOOL_ID, CLASS_ID, requestId); + } + + private ExperimentSubject subject() { + return new ExperimentSubject(USER_ID, SCHOOL_ID, CLASS_ID, ""); + } + + private int count(String sql, Object... args) { + Integer value = jdbc.queryForObject(sql, Integer.class, args); + return value == null ? 0 : value; + } + + private BigDecimal decimal(String sql, Object... args) { + return jdbc.queryForObject(sql, BigDecimal.class, args); + } +} diff --git a/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingAttemptControllerTest.java b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingAttemptControllerTest.java index b232e3f..bcbd198 100644 --- a/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingAttemptControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/issuance/interfaces/rest/IssuanceTrainingAttemptControllerTest.java @@ -88,6 +88,10 @@ class IssuanceTrainingAttemptControllerTest { .andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptId").value(attemptId.toString())); mockMvc.perform(post("/api/v1/issuance/attempts/{id}/cancel", attemptId).with(jwtFor(USER_ID))) .andExpect(status().isOk()).andExpect(jsonPath("$.data.attemptStatus").value("CANCELLED")); + mockMvc.perform(post("/api/v1/issuance/training/reset").with(jwtFor(USER_ID))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.reset").value(true)) + .andExpect(jsonPath("$.data.deletedRecordCount").isNumber()); } private CurrentUser user(String userId) {