diff --git a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java index 93d0329..300323c 100644 --- a/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java +++ b/src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java @@ -1,14 +1,19 @@ package com.yau.digitalrmb; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.scheduling.annotation.EnableScheduling; +@Slf4j @SpringBootApplication @EnableScheduling public class DigitalRmbApplication { public static void main(String[] args) { - SpringApplication.run(DigitalRmbApplication.class, args); + ConfigurableApplicationContext context = SpringApplication.run(DigitalRmbApplication.class, args); + log.info("数字人民币教学仿真后端启动完成,端口:{}", + context.getEnvironment().getProperty("server.port")); } } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetResult.java new file mode 100644 index 0000000..45ca4bd --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetResult.java @@ -0,0 +1,21 @@ +package com.yau.digitalrmb.corporatewallet.application; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Getter +@Schema(description = "模块六创新应用场景重新实训结果") +public class CorporateInnovationTrainingResetResult { + @Schema(description = "是否重置成功") + private final boolean reset; + @Schema(description = "逻辑删除的业务记录总数") + private final int deletedRecordCount; + @Schema(description = "处理说明") + private final String message; + + public CorporateInnovationTrainingResetResult(int deletedRecordCount) { + this.reset = true; + this.deletedRecordCount = deletedRecordCount; + this.message = "数字人民币创新应用场景步骤数据已逻辑删除,可从模块六第一步重新开始"; + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetService.java new file mode 100644 index 0000000..f639cfc --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetService.java @@ -0,0 +1,100 @@ +package com.yau.digitalrmb.corporatewallet.application; + +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationReceiptEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationReceiptMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationSupervisionEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationSupervisionMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.SalaryBatchEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.SalaryBatchMapper; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; +import com.yau.digitalrmb.progress.application.UserStepProgressNewService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +@Service +public class CorporateInnovationTrainingResetService { + private static final String TRAINING_ID = "innovation-scene"; + + @Resource private CorporateWalletApplicationMapper corporateWalletMapper; + @Resource private RemoteWalletApplicationMapper remoteWalletMapper; + @Resource private SalaryBatchMapper salaryBatchMapper; + @Resource private DonationMapper donationMapper; + @Resource private DonationReceiptMapper donationReceiptMapper; + @Resource private DonationSupervisionMapper donationSupervisionMapper; + @Resource private StuModuleScoreDetailsMapper scoreDetailsMapper; + @Resource private UserStepProgressNewService stepProgressService; + + @Transactional + public CorporateInnovationTrainingResetResult reset(InstitutionKeySubject subject) { + int count = 0; + count += logicalDeleteCorporateWalletApplications(subject); + count += logicalDeleteRemoteWalletApplications(subject); + count += logicalDeleteSalaryBatches(subject); + count += logicalDeleteDonationRecords(subject); + count += logicalDeleteScore(subject.getUserId()); + stepProgressService.reset(subject.getUserId(), TRAINING_ID); + return new CorporateInnovationTrainingResetResult(count); + } + + private int logicalDeleteCorporateWalletApplications(InstitutionKeySubject subject) { + return corporateWalletMapper.update(null, new LambdaUpdateWrapper() + .eq(CorporateWalletApplicationEntity::getUserId, subject.getUserId()) + .eq(CorporateWalletApplicationEntity::getSchoolId, subject.getSchoolId()) + .eq(CorporateWalletApplicationEntity::getClassId, subject.getClassId()) + .set(CorporateWalletApplicationEntity::getDeleted, true)); + } + + private int logicalDeleteRemoteWalletApplications(InstitutionKeySubject subject) { + return remoteWalletMapper.update(null, new LambdaUpdateWrapper() + .eq(RemoteWalletApplicationEntity::getUserId, subject.getUserId()) + .eq(RemoteWalletApplicationEntity::getSchoolId, subject.getSchoolId()) + .eq(RemoteWalletApplicationEntity::getClassId, subject.getClassId()) + .set(RemoteWalletApplicationEntity::getDeleted, true)); + } + + private int logicalDeleteSalaryBatches(InstitutionKeySubject subject) { + return salaryBatchMapper.update(null, new LambdaUpdateWrapper() + .eq(SalaryBatchEntity::getUserId, subject.getUserId()) + .eq(SalaryBatchEntity::getSchoolId, subject.getSchoolId()) + .eq(SalaryBatchEntity::getClassId, subject.getClassId()) + .set(SalaryBatchEntity::getDeleted, true)); + } + + private int logicalDeleteDonationRecords(InstitutionKeySubject subject) { + int count = 0; + count += donationMapper.update(null, new LambdaUpdateWrapper() + .eq(DonationEntity::getUserId, subject.getUserId()) + .eq(DonationEntity::getSchoolId, subject.getSchoolId()) + .eq(DonationEntity::getClassId, subject.getClassId()) + .set(DonationEntity::getDeleted, true)); + count += donationReceiptMapper.update(null, new LambdaUpdateWrapper() + .eq(DonationReceiptEntity::getUserId, subject.getUserId()) + .eq(DonationReceiptEntity::getSchoolId, subject.getSchoolId()) + .eq(DonationReceiptEntity::getClassId, subject.getClassId()) + .set(DonationReceiptEntity::getDeleted, true)); + count += donationSupervisionMapper.update(null, new LambdaUpdateWrapper() + .eq(DonationSupervisionEntity::getUserId, subject.getUserId()) + .eq(DonationSupervisionEntity::getSchoolId, subject.getSchoolId()) + .eq(DonationSupervisionEntity::getClassId, subject.getClassId()) + .set(DonationSupervisionEntity::getDeleted, true)); + return count; + } + + private int logicalDeleteScore(String userId) { + return scoreDetailsMapper.update(null, new LambdaUpdateWrapper() + .eq(StuModuleScoreDetailsEntity::getUserId, userId) + .eq(StuModuleScoreDetailsEntity::getMoudule, SalaryScoreService.MODULE_NAME) + .set(StuModuleScoreDetailsEntity::getDeleted, true)); + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/DonationPageResult.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/DonationPageResult.java new file mode 100644 index 0000000..9b44269 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/DonationPageResult.java @@ -0,0 +1,44 @@ +package com.yau.digitalrmb.corporatewallet.application; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@Schema(description = "公益捐赠三步流程页面数据") +public class DonationPageResult { + private final String donorName; + private final String donorWalletId; + private final String recipientName; + private final String recipientWalletId; + private final String amount; + private final String projectName; + private final String memo; + private final String donationTimestamp; + private final String donationId; + private final String donationMessage; + private final String donationDigest; + private final String donorPrivateKey; + private final String donorSignature; + private final String donationOutputJson; + private final String receiptStatus; + private final String publicationTimestamp; + private final String publicationMessage; + private final String publicationDigest; + private final String publicationId; + private final String publicationTitle; + private final String publicationContent; + private final String publicationUrl; + private final String publicationOutputJson; + private final String supervisionTimestamp; + private final String supervisionMessage; + private final String supervisionDigest; + private final String regulatorPrivateKey; + private final String regulatorSignature; + private final String approvalId; + private final String approvalTime; + private final String recordNumber; + private final String supervisionOutputJson; + private final String status; +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java index 3bd15c2..d3fe05d 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchService.java @@ -73,14 +73,16 @@ public class SalaryBatchService { @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, "请先完成对公钱包开立(步骤一)"); } + // 每次调取区块信息都代表一个新的工资批次,避免复用上一批次的摘要、签名和状态。 + SalaryBatch batch = repository.save( + SalaryBatch.create(subject.getUserId(), subject.getSchoolId(), subject.getClassId())); + // 批次号随机生成 String batchId = generateBatchId(); // 时间戳 @@ -161,8 +163,8 @@ public class SalaryBatchService { throw new BusinessException(ErrorCode.VALIDATION_ERROR, "数币操作员私钥不正确,错误次数+1"); } - // 使用数币操作员私钥对摘要进行SM2签名 - String signature = cryptography.sign(storedPrivateKey, batch.getConcatenatedMessage()); + // 按原型要求,使用数币操作员私钥对SM3摘要值进行SM2签名。 + String signature = cryptography.sign(storedPrivateKey, batch.getDigestValue()); batch.sign(signature); repository.update(batch); @@ -516,12 +518,6 @@ public class SalaryBatchService { // ==================== 内部方法 ==================== - 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, diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java index 83a897e..eaf82b8 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/SalaryScoreService.java @@ -45,7 +45,7 @@ public class SalaryScoreService { /** * 标记步骤完成,更新进度和成绩 * @param userId 用户ID - * @param completedStep 完成的步骤编号(1-7) + * @param completedStep 完成的步骤编号(1-8) */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void markStepCompleted(String userId, int completedStep) { @@ -55,10 +55,10 @@ public class SalaryScoreService { StuModuleScoreDetailsEntity detail = getOrCreate(userId); int errorCount = errorCount(detail.getCompletionStatus()); - // 模块六共7个步骤: + // 模块六共8个步骤: // 1-临柜开立, 2-远程开立, 3-数币操作员导入, 4-数币复核员审核, - // 5-交易确认与工资发放, 6-捐赠企业发起, 7-受赠机构接收 - int totalSteps = 7; + // 5-交易确认与工资发放, 6-捐赠企业发起, 7-受赠机构接收, 8-第三方监管审核 + int totalSteps = 8; int normalizedStep = Math.max(0, Math.min(totalSteps, completedStep)); // 计算进度百分比:completedStep / totalSteps * 100 diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/DonationRecordEntity.java b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/DonationRecordEntity.java new file mode 100644 index 0000000..21688b2 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/DonationRecordEntity.java @@ -0,0 +1,48 @@ +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; + +import java.math.BigDecimal; + +@Getter +@Setter +@TableName("donation_record") +public class DonationRecordEntity extends AuditableEntity { + @TableField("user_id") private String userId; + @TableField("school_id") private Long schoolId; + @TableField("class_id") private Long classId; + @TableField("donation_id") private String donationId; + @TableField("donor_name") private String donorName; + @TableField("donor_wallet_id") private String donorWalletId; + @TableField("recipient_name") private String recipientName; + @TableField("recipient_wallet_id") private String recipientWalletId; + private BigDecimal amount; + @TableField("project_name") private String projectName; + private String memo; + @TableField("donation_timestamp") private String donationTimestamp; + @TableField("donation_message") private String donationMessage; + @TableField("donation_digest") private String donationDigest; + @TableField("donor_signature") private String donorSignature; + @TableField("donation_output_json") private String donationOutputJson; + @TableField("publication_timestamp") private String publicationTimestamp; + @TableField("publication_message") private String publicationMessage; + @TableField("publication_digest") private String publicationDigest; + @TableField("publication_id") private String publicationId; + @TableField("publication_title") private String publicationTitle; + @TableField("publication_content") private String publicationContent; + @TableField("publication_url") private String publicationUrl; + @TableField("publication_output_json") private String publicationOutputJson; + @TableField("supervision_timestamp") private String supervisionTimestamp; + @TableField("supervision_message") private String supervisionMessage; + @TableField("supervision_digest") private String supervisionDigest; + @TableField("regulator_signature") private String regulatorSignature; + @TableField("approval_id") private String approvalId; + @TableField("approval_time") private String approvalTime; + @TableField("record_number") private String recordNumber; + @TableField("supervision_output_json") private String supervisionOutputJson; + private String status; +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/DonationRecordMapper.java b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/DonationRecordMapper.java new file mode 100644 index 0000000..7968516 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/DonationRecordMapper.java @@ -0,0 +1,8 @@ +package com.yau.digitalrmb.corporatewallet.infrastructure; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DonationRecordMapper extends BaseMapper { +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java index eb38960..8c5d2ff 100644 --- a/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/infrastructure/MybatisSalaryBatchRepository.java @@ -30,6 +30,7 @@ public class MybatisSalaryBatchRepository implements SalaryBatchRepository { .eq(SalaryBatchEntity::getClassId, classId) .eq(SalaryBatchEntity::getDeleted, false) .orderByDesc(SalaryBatchEntity::getCreatedAt) + .orderByDesc(SalaryBatchEntity::getId) .last("LIMIT 1")); return entity == null ? Optional.empty() : Optional.of(toDomain(entity)); } diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/CorporateInnovationTrainingResetController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/CorporateInnovationTrainingResetController.java new file mode 100644 index 0000000..d7c0eaa --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/CorporateInnovationTrainingResetController.java @@ -0,0 +1,35 @@ +package com.yau.digitalrmb.corporatewallet.interfaces.rest; + +import com.yau.digitalrmb.corporatewallet.application.CorporateInnovationTrainingResetResult; +import com.yau.digitalrmb.corporatewallet.application.CorporateInnovationTrainingResetService; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/api/v1/corporate-wallet/training") +@Tag(name = "模块六:数字人民币创新应用场景实验管理") +public class CorporateInnovationTrainingResetController { + @Resource private CorporateInnovationTrainingResetService resetService; + + @PostMapping("/reset") + @Operation(summary = "重新开始数字人民币创新应用场景实验", + description = "逻辑删除当前主体的对公钱包申请、工资批次、模块六成绩和步骤进度;保留模块六密钥,可从模块六第一步重新开始。") + public ApiResponse reset() { + return ApiResponse.success(resetService.reset( + InstitutionKeySubject.from(AuthContextHolder.get())), traceId()); + } + + private String traceId() { + return MDC.get(TraceIdFilter.MDC_KEY); + } +} diff --git a/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/DonationCreateRequest.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/DonationCreateRequest.java new file mode 100644 index 0000000..2ddb238 --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/DonationCreateRequest.java @@ -0,0 +1,25 @@ +package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.Digits; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.math.BigDecimal; + +@Data +@Schema(description = "拼接捐赠原文请求") +public class DonationCreateRequest { + @NotBlank @Size(max = 200) private String donorName; + @NotBlank @Size(max = 128) private String donorWalletId; + @NotBlank @Size(max = 200) private String recipientName; + @NotBlank @Size(max = 128) private String recipientWalletId; + @NotNull @DecimalMin(value = "0.01") @Digits(integer = 18, fraction = 2) private BigDecimal amount; + @NotBlank @Size(max = 200) private String projectName; + @NotBlank @Size(max = 500) private String memo; + @NotBlank @Pattern(regexp = "\\d{14}", message = "时间戳格式应为yyyyMMddHHmmss") private String timestamp; +} diff --git a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java index 490738d..7e96443 100644 --- a/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java +++ b/src/main/java/com/yau/digitalrmb/payment/application/service/PaymentTrainingActionService.java @@ -86,8 +86,12 @@ public class PaymentTrainingActionService { switch (key) { case "01:refresh-payer": return completed(payments.payer(actor)); - case "01:refresh-payee": - return completed(payments.payee(required(request.getPayeeWalletId(), "收款钱包标识"))); + case "01:refresh-payee": { + String payeeWalletId = request.getPayeeWalletId(); + return completed(payeeWalletId == null || payeeWalletId.trim().isEmpty() + ? payments.context(null, actor).getPayee() + : payments.payee(payeeWalletId.trim())); + } case "02:generate-timestamp": return completed(value("timestamp", TIMESTAMP.format(Instant.now()))); case "02:concatenate-payment": @@ -149,8 +153,9 @@ public class PaymentTrainingActionService { private Map paymentSource(PaymentActionRequest request, ExperimentAttemptView attempt, PaymentActor actor) { - String payeeWalletId = required(request.getPayeeWalletId(), "收款钱包标识"); - PaymentContext context = payments.context(payeeWalletId, actor); + String payeeWalletId = request.getPayeeWalletId(); + PaymentContext context = payments.context(payeeWalletId == null || payeeWalletId.trim().isEmpty() + ? null : payeeWalletId.trim(), actor); if (request.getPayerWalletId() != null && !context.getPayer().getWalletId().equals(request.getPayerWalletId().trim())) { throw validation("付款钱包不属于当前用户"); @@ -237,11 +242,6 @@ public class PaymentTrainingActionService { } } - private String required(String value, String name) { - if (value == null || value.trim().isEmpty()) throw validation(name + "不能为空"); - return value.trim(); - } - private String fingerprint(String key, PaymentActionRequest request, ExperimentSubject subject) { Map values = new LinkedHashMap(); values.put("action", key); diff --git a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java index c0997c1..56bd367 100644 --- a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java +++ b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/JdbcPaymentResourceRepository.java @@ -52,14 +52,17 @@ public class JdbcPaymentResourceRepository implements PaymentResourceRepository @Override public PaymentParticipant loadPayer(PaymentActor actor) { - WalletPrerequisiteReference reference = walletProjection.referenceForSubject( + WalletPrerequisiteReference reference = walletProjection.ensureForSubject( new InstitutionKeySubject(actor.getUserId(), actor.getSchoolId(), actor.getClassId())); return loadParticipant(reference); } @Override public PaymentParticipant loadPayee(String payeeWalletId) { - return loadParticipant(walletProjection.referenceForWallet(payeeWalletId)); + if (teachingPayeeWhitelist.isTeachingWallet(payeeWalletId)) { + return loadParticipant(teachingPayeeWhitelist.resolve(payeeWalletId)); + } + return loadParticipant(walletProjection.ensureForWallet(payeeWalletId)); } @Override diff --git a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java index 5124cc5..1f9dd02 100644 --- a/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java +++ b/src/main/java/com/yau/digitalrmb/payment/infrastructure/persistence/TeachingPaymentPayeeWhitelist.java @@ -22,9 +22,16 @@ public class TeachingPaymentPayeeWhitelist { private static final String BANK_A = "BKCHCNBJ00001"; private static final String BANK_B = "BKCHCNBJ00002"; private static final String PAYEE_A_USER = "00000000-0000-0000-0000-000000000951"; - private static final String PAYEE_B_USER = "00000000-0000-0000-0000-000000000952"; + private static final String PAYEE_B_USER = "00000000-0000-0000-0000-000000000953"; private static final String PAYEE_A_WALLET = "WALLET_TRAINING_PAYEE_A"; - private static final String PAYEE_B_WALLET = "WALLET_TRAINING_PAYEE_B"; + private static final String PAYEE_B_WALLET = + "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6"; + private static final PayeeDefinition PAYEE_A = new PayeeDefinition( + PAYEE_A_USER, PAYEE_A_WALLET, "TYPE_II", "CERT_" + PAYEE_A_WALLET, + BANK_A, "教学商业银行A", "ORG_TRAINING_PAYEE_A"); + private static final PayeeDefinition PAYEE_B = new PayeeDefinition( + PAYEE_B_USER, PAYEE_B_WALLET, "一类钱包", "00C3D4E5F6A7B890", + BANK_B, "建设银行(BKCHCNBJ00002)", "ORG_5F6G7B8E9G0W"); private final JdbcTemplate jdbc; private final InstitutionIdentityCryptography cryptography; @@ -39,9 +46,21 @@ public class TeachingPaymentPayeeWhitelist { @Transactional public WalletPrerequisiteReference resolveForDifferentBank(String payerBankCode) { - PayeeDefinition payee = BANK_B.equals(payerBankCode) - ? new PayeeDefinition(PAYEE_A_USER, PAYEE_A_WALLET, BANK_A, "教学商业银行A", "ORG_TRAINING_PAYEE_A") - : new PayeeDefinition(PAYEE_B_USER, PAYEE_B_WALLET, BANK_B, "教学商业银行B", "ORG_TRAINING_PAYEE_B"); + return resolve(BANK_B.equals(payerBankCode) ? PAYEE_A : PAYEE_B); + } + + public boolean isTeachingWallet(String walletId) { + return definitionFor(walletId) != null; + } + + @Transactional + public WalletPrerequisiteReference resolve(String walletId) { + PayeeDefinition payee = definitionFor(walletId); + if (payee == null) throw conflict("教学收款方钱包不存在"); + return resolve(payee); + } + + private WalletPrerequisiteReference resolve(PayeeDefinition payee) { ensureWallet(payee); ensureCertificate(payee); ensureBankAccount(payee); @@ -50,6 +69,12 @@ public class TeachingPaymentPayeeWhitelist { payee.bankCode, payee.organizationId); } + private PayeeDefinition definitionFor(String walletId) { + if (PAYEE_A_WALLET.equals(walletId)) return PAYEE_A; + if (PAYEE_B_WALLET.equals(walletId)) return PAYEE_B; + return null; + } + private void ensureWallet(PayeeDefinition payee) { List wallets = jdbc.query("SELECT user_id,wallet_type,status FROM digital_wallet WHERE wallet_id=?", (rs, row) -> new WalletRow(rs.getString("user_id"), rs.getString("wallet_type"), @@ -58,15 +83,20 @@ public class TeachingPaymentPayeeWhitelist { try { jdbc.update("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 (?,?, 'TYPE_II','ACTIVE',0,0,'TEACHING_PAYEE_CONFIRMED',CURRENT_TIMESTAMP," + - "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", payee.walletId, payee.userId); + "VALUES (?,?, ?,'ACTIVE',0,0,'TEACHING_PAYEE_CONFIRMED',CURRENT_TIMESTAMP," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)", payee.walletId, payee.userId, payee.walletType); } catch (DuplicateKeyException exception) { + List afterConflict = jdbc.query( + "SELECT user_id,wallet_type,status FROM digital_wallet WHERE wallet_id=?", + (rs, row) -> new WalletRow(rs.getString("user_id"), rs.getString("wallet_type"), + rs.getString("status")), payee.walletId); + if (afterConflict.isEmpty()) throw conflict("教学收款方的钱包标识或用户标识已被占用"); ensureWallet(payee); } return; } WalletRow wallet = wallets.get(0); - if (!payee.userId.equals(wallet.userId) || !"TYPE_II".equals(wallet.walletType) + if (!payee.userId.equals(wallet.userId) || !payee.walletType.equals(wallet.walletType) || !"ACTIVE".equals(wallet.status)) { throw conflict("教学收款方钱包状态或归属异常"); } @@ -81,7 +111,7 @@ public class TeachingPaymentPayeeWhitelist { jdbc.update("INSERT INTO wallet_certificate (certificate_serial,wallet_id,public_key,encrypted_private_key," + "filing_status,central_bank_root_signature,status,issued_at) " + "VALUES (?,?,?,?, 'REGISTERED','TEACHING_PAYEE_ROOT','VALID',CURRENT_TIMESTAMP)", - "CERT_" + payee.walletId, payee.walletId, keyPair.getPublicKey(), + payee.certificateSerial, payee.walletId, keyPair.getPublicKey(), walletCipher.encrypt(keyPair.getPrivateKey())); } catch (DuplicateKeyException exception) { ensureCertificate(payee); @@ -137,14 +167,19 @@ public class TeachingPaymentPayeeWhitelist { private static final class PayeeDefinition { private final String userId; private final String walletId; + private final String walletType; + private final String certificateSerial; private final String bankCode; private final String bankName; private final String organizationId; - private PayeeDefinition(String userId, String walletId, String bankCode, String bankName, + private PayeeDefinition(String userId, String walletId, String walletType, String certificateSerial, + String bankCode, String bankName, String organizationId) { this.userId = userId; this.walletId = walletId; + this.walletType = walletType; + this.certificateSerial = certificateSerial; this.bankCode = bankCode; this.bankName = bankName; this.organizationId = organizationId; diff --git a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java index f48a933..706867a 100644 --- a/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java +++ b/src/main/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionService.java @@ -192,15 +192,23 @@ public class WalletPrerequisiteProjectionService { private void ensureWallet(SourceWallet source) { List existing = jdbc.query( - "SELECT wallet_id,user_id,wallet_type,central_bank_confirmation_signature " + + "SELECT wallet_id,user_id,wallet_type,central_bank_confirmation_signature,balance,frozen_amount " + "FROM digital_wallet WHERE wallet_id=? OR user_id=?", (rs, row) -> new Wallet(rs.getString("wallet_id"), rs.getString("user_id"), - rs.getString("wallet_type"), rs.getString("central_bank_confirmation_signature")), + rs.getString("wallet_type"), rs.getString("central_bank_confirmation_signature"), + rs.getBigDecimal("balance"), rs.getBigDecimal("frozen_amount")), source.walletId, source.userId); if (!existing.isEmpty()) { if (existing.size() != 1) throw conflict("共享钱包标识或用户归属冲突"); - validateWallet(existing.get(0), source); - return; + Wallet value = existing.get(0); + if (!isRetrainedUnusedWallet(value, source)) { + validateWallet(value, source); + return; + } + // Module 3 was redone after this wallet was first projected here, and the stale wallet + // was never actually used downstream, so retire it and let the fresh module 3 result + // through instead of surfacing a spurious "共享钱包与模块三最终结果不一致" conflict. + retireUnusedWalletChain(value.walletId); } try { jdbc.update("INSERT INTO digital_wallet " + @@ -213,6 +221,25 @@ public class WalletPrerequisiteProjectionService { } } + private boolean isRetrainedUnusedWallet(Wallet value, SourceWallet source) { + if (!source.userId.equals(value.userId) || source.walletId.equals(value.walletId)) return false; + if (value.balance.signum() != 0 || value.frozenAmount.signum() != 0) return false; + Integer usageCount = jdbc.queryForObject( + "SELECT (SELECT COUNT(*) FROM currency_exchange_order WHERE wallet_id=?) + " + + "(SELECT COUNT(*) FROM payment_order WHERE payer_wallet_id=? OR payee_wallet_id=?) + " + + "(SELECT COUNT(*) FROM wallet_balance_ledger WHERE wallet_id=?) + " + + "(SELECT COUNT(*) FROM payment_wallet_ledger WHERE wallet_id=?)", + Integer.class, value.walletId, value.walletId, value.walletId, value.walletId, value.walletId); + return usageCount != null && usageCount == 0; + } + + private void retireUnusedWalletChain(String staleWalletId) { + jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id=?", staleWalletId); + jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", staleWalletId); + jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id=?", staleWalletId); + jdbc.update("DELETE FROM digital_wallet WHERE wallet_id=?", staleWalletId); + } + private void ensureCertificate(SourceWallet source) { List existing = jdbc.query( "SELECT certificate_serial,wallet_id,public_key,central_bank_root_signature " + @@ -470,9 +497,13 @@ public class WalletPrerequisiteProjectionService { private final String userId; private final String walletType; private final String finalSignature; - private Wallet(String walletId, String userId, String walletType, String finalSignature) { + private final BigDecimal balance; + private final BigDecimal frozenAmount; + private Wallet(String walletId, String userId, String walletType, String finalSignature, + BigDecimal balance, BigDecimal frozenAmount) { this.walletId = walletId; this.userId = userId; this.walletType = walletType; this.finalSignature = finalSignature; + this.balance = balance; this.frozenAmount = frozenAmount; } } diff --git a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/WalletApplicationRequest.java b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/WalletApplicationRequest.java index d6e43a6..73fd882 100644 --- a/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/WalletApplicationRequest.java +++ b/src/main/java/com/yau/digitalrmb/walletopening/interfaces/rest/dto/WalletApplicationRequest.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; import java.math.BigDecimal; @Getter @@ -20,6 +21,7 @@ public class WalletApplicationRequest { private String inputPhone; @Schema(description = "身份证号", example = "110101199001011234") private String inputIdNumber; + @Size(max = 64, message = "银行卡号长度不能超过64位") @Schema(description = "银行卡号", example = "6216610100001234567") private String inputBankCardNumber; @Schema(description = "面签照片路径", example = "/2026/8/11/5cae82501de84bfba3693477ac55140c.pdf") @@ -28,6 +30,7 @@ public class WalletApplicationRequest { private String idNumber; @Schema(description = "开户银行", example = "中国银行北京分行") private String accountBank; + @Size(max = 64, message = "银行卡号长度不能超过64位") @Schema(description = "银行卡号(确认)", example = "6216610100001234567") private String bankCardNumber; @Schema(description = "账户余额", example = "500000.00") diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0043677..e917aad 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,6 +14,9 @@ spring: init: mode: always encoding: UTF-8 +logging: + file: + path: ./logs server: port: 8789 diff --git a/src/main/resources/db/manual/module_6_charity_upgrade.sql b/src/main/resources/db/manual/module_6_charity_upgrade.sql new file mode 100644 index 0000000..b96dadc --- /dev/null +++ b/src/main/resources/db/manual/module_6_charity_upgrade.sql @@ -0,0 +1,44 @@ +-- 模块六公益捐赠三步流程增量建表脚本。 +-- 已有环境升级时执行;全新环境由 schema.sql 自动创建。 +CREATE TABLE IF NOT EXISTS donation_record ( + id BIGINT PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + school_id BIGINT NOT NULL, + class_id BIGINT NOT NULL, + donation_id VARCHAR(64), + donor_name VARCHAR(200), + donor_wallet_id VARCHAR(128), + recipient_name VARCHAR(200), + recipient_wallet_id VARCHAR(128), + amount DECIMAL(20, 2), + project_name VARCHAR(200), + memo VARCHAR(500), + donation_timestamp VARCHAR(20), + donation_message TEXT, + donation_digest VARCHAR(128), + donor_signature TEXT, + donation_output_json TEXT, + publication_timestamp VARCHAR(20), + publication_message TEXT, + publication_digest VARCHAR(128), + publication_id VARCHAR(64), + publication_title VARCHAR(300), + publication_content VARCHAR(1000), + publication_url VARCHAR(500), + publication_output_json TEXT, + supervision_timestamp VARCHAR(20), + supervision_message TEXT, + supervision_digest VARCHAR(128), + regulator_signature TEXT, + approval_id VARCHAR(64), + approval_time VARCHAR(20), + record_number VARCHAR(64), + supervision_output_json TEXT, + status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', + 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, + INDEX idx_donation_subject (user_id, school_id, class_id, deleted, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-公益捐赠记录表'; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index b951527..bcaf69a 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -930,13 +930,13 @@ CREATE TABLE IF NOT EXISTS wallet_application ( id_number VARCHAR(20), phone VARCHAR(20), account_bank VARCHAR(100), - bank_card_number VARCHAR(30), + bank_card_number VARCHAR(64), account_balance DECIMAL(20, 2), selected_bank VARCHAR(100), wallet_type VARCHAR(20), input_phone VARCHAR(20), input_id_number VARCHAR(20), - input_bank_card_number VARCHAR(30), + input_bank_card_number VARCHAR(64), signature_photo_path VARCHAR(255), face_recognition_status INTEGER, application_id VARCHAR(64), @@ -964,7 +964,7 @@ CREATE TABLE IF NOT EXISTS bank_received_wallet_application ( user_name VARCHAR(100), id_number VARCHAR(20), phone VARCHAR(20), - bank_card_number VARCHAR(30), + bank_card_number VARCHAR(64), wallet_type VARCHAR(20), apply_time VARCHAR(20), created_at TIMESTAMP NOT NULL, @@ -1083,7 +1083,7 @@ CREATE TABLE IF NOT EXISTS wallet_registration_request ( wallet_id VARCHAR(128), user_name VARCHAR(100), id_number VARCHAR(20), - bank_card_number VARCHAR(30), + bank_card_number VARCHAR(64), wallet_type VARCHAR(20), org_code VARCHAR(20), cert_serial VARCHAR(64), @@ -1113,7 +1113,7 @@ CREATE TABLE IF NOT EXISTS central_wallet_registration ( wallet_id VARCHAR(128), user_name VARCHAR(100), id_number VARCHAR(20), - bank_card_number VARCHAR(30), + bank_card_number VARCHAR(64), wallet_type VARCHAR(20), org_code VARCHAR(20), cert_serial VARCHAR(64), @@ -1269,6 +1269,7 @@ CREATE TABLE IF NOT EXISTS salary_batch ( deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除标记:false-未删除, true-已删除' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-工资发放批次表'; +<<<<<<< HEAD -- 模块六:捐赠企业发起捐赠表 - 捐赠企业向受赠机构发起捐赠全流程 CREATE TABLE IF NOT EXISTS donation ( id BIGINT PRIMARY KEY COMMENT '主键ID(雪花算法生成)', @@ -1346,6 +1347,51 @@ CREATE TABLE IF NOT EXISTS donation_supervision ( updated_by VARCHAR(64) NOT NULL COMMENT '更新人', deleted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '逻辑删除标记:false-未删除, true-已删除' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-第三方监管监督与审核表'; +======= +-- 模块六:企业使用数字人民币进行公益捐赠 - 发起、公示、监管审核全流程 +CREATE TABLE IF NOT EXISTS donation_record ( + id BIGINT PRIMARY KEY COMMENT '主键ID(雪花算法生成)', + user_id VARCHAR(36) NOT NULL COMMENT '用户ID', + school_id BIGINT NOT NULL COMMENT '学校ID', + class_id BIGINT NOT NULL COMMENT '班级ID', + donation_id VARCHAR(64) COMMENT '捐赠编号', + donor_name VARCHAR(200) COMMENT '捐赠企业', + donor_wallet_id VARCHAR(128) COMMENT '付款钱包ID', + recipient_name VARCHAR(200) COMMENT '受赠机构', + recipient_wallet_id VARCHAR(128) COMMENT '收款钱包ID', + amount DECIMAL(20, 2) COMMENT '捐赠金额', + project_name VARCHAR(200) COMMENT '项目名称', + memo VARCHAR(500) COMMENT '捐赠备注', + donation_timestamp VARCHAR(20) COMMENT '捐赠时间戳', + donation_message TEXT COMMENT '捐赠拼接原文', + donation_digest VARCHAR(128) COMMENT '捐赠SM3摘要', + donor_signature TEXT COMMENT '企业法人SM2签名', + donation_output_json TEXT COMMENT '捐赠提交报文', + publication_timestamp VARCHAR(20) COMMENT '公示时间戳', + publication_message TEXT COMMENT '公示拼接原文', + publication_digest VARCHAR(128) COMMENT '公示SM3摘要', + publication_id VARCHAR(64) COMMENT '公示编号', + publication_title VARCHAR(300) COMMENT '公示标题', + publication_content VARCHAR(1000) COMMENT '公示内容', + publication_url VARCHAR(500) COMMENT '公示网址', + publication_output_json TEXT COMMENT '资金公示报文', + supervision_timestamp VARCHAR(20) COMMENT '监管审核时间戳', + supervision_message TEXT COMMENT '监管审核原文', + supervision_digest VARCHAR(128) COMMENT '监管审核SM3摘要', + regulator_signature TEXT COMMENT '监管机构SM2签名', + approval_id VARCHAR(64) COMMENT '审批编号', + approval_time VARCHAR(20) COMMENT '审批时间', + record_number VARCHAR(64) COMMENT '备案编号', + supervision_output_json TEXT COMMENT '监管审核备案报文', + status VARCHAR(32) NOT NULL DEFAULT 'DRAFT' COMMENT '流程状态', + 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, + INDEX idx_donation_subject (user_id, school_id, class_id, deleted, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块六-公益捐赠记录表'; +>>>>>>> 8af6f539bf42ee657bec4cc824118ff56d043811 -- 步骤四商业银行端:生成合约信息与开通确认 CREATE TABLE IF NOT EXISTS smart_contract_generation ( diff --git a/src/test/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetServiceTest.java b/src/test/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetServiceTest.java new file mode 100644 index 0000000..e601cbc --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetServiceTest.java @@ -0,0 +1,67 @@ +package com.yau.digitalrmb.corporatewallet.application; + +import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.CorporateWalletApplicationEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.RemoteWalletApplicationMapper; +import com.yau.digitalrmb.corporatewallet.infrastructure.SalaryBatchEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.SalaryBatchMapper; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper; +import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity; +import com.yau.digitalrmb.progress.application.UserStepProgressNewService; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CorporateInnovationTrainingResetServiceTest { + private static final InstitutionKeySubject SUBJECT = + new InstitutionKeySubject("reset-user", 1001L, 2001L); + + @Mock private CorporateWalletApplicationMapper corporateWalletMapper; + @Mock private RemoteWalletApplicationMapper remoteWalletMapper; + @Mock private SalaryBatchMapper salaryBatchMapper; + @Mock private DonationRecordMapper donationRecordMapper; + @Mock private StuModuleScoreDetailsMapper scoreDetailsMapper; + @Mock private UserStepProgressNewService stepProgressService; + @InjectMocks private CorporateInnovationTrainingResetService service; + + @BeforeAll + static void initializeMybatisLambdaMetadata() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, CorporateWalletApplicationEntity.class); + TableInfoHelper.initTableInfo(assistant, RemoteWalletApplicationEntity.class); + TableInfoHelper.initTableInfo(assistant, SalaryBatchEntity.class); + TableInfoHelper.initTableInfo(assistant, DonationRecordEntity.class); + TableInfoHelper.initTableInfo(assistant, StuModuleScoreDetailsEntity.class); + } + + @Test + void resetsModuleSixBusinessDataScoreAndNavigationProgress() { + when(corporateWalletMapper.update(any(), any())).thenReturn(2); + when(remoteWalletMapper.update(any(), any())).thenReturn(3); + when(salaryBatchMapper.update(any(), any())).thenReturn(4); + when(donationRecordMapper.update(any(), any())).thenReturn(5); + when(scoreDetailsMapper.update(any(), any())).thenReturn(1); + + CorporateInnovationTrainingResetResult result = service.reset(SUBJECT); + + assertThat(result.isReset()).isTrue(); + assertThat(result.getDeletedRecordCount()).isEqualTo(15); + verify(stepProgressService).reset("reset-user", "innovation-scene"); + } +} diff --git a/src/test/java/com/yau/digitalrmb/corporatewallet/application/DonationServiceTest.java b/src/test/java/com/yau/digitalrmb/corporatewallet/application/DonationServiceTest.java new file mode 100644 index 0000000..f486282 --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/corporatewallet/application/DonationServiceTest.java @@ -0,0 +1,119 @@ +package com.yau.digitalrmb.corporatewallet.application; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.fasterxml.jackson.databind.ObjectMapper; +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.DonationRecordEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper; +import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationCreateRequest; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DonationServiceTest { + private static final InstitutionKeySubject SUBJECT = new InstitutionKeySubject("u-1", 1L, 2L); + + @Mock private DonationRecordMapper mapper; + @Mock private CorporateWalletApplicationRepository corporateWalletRepository; + @Mock private RemoteWalletApplicationRepository remoteWalletRepository; + @Mock private CorporateWalletKeyService keyService; + @Mock private InstitutionIdentityCryptography cryptography; + @Mock private SalaryScoreService scoreService; + @Spy private ObjectMapper objectMapper = new ObjectMapper(); + @InjectMocks private DonationService service; + + private final AtomicReference latest = new AtomicReference<>(); + + @BeforeAll + static void initializeMybatisLambdaMetadata() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), + DonationRecordEntity.class); + } + + @BeforeEach + void setUp() { + CorporateWalletApplication donor = CorporateWalletApplication.create("u-1", 1L, 2L); + donor.fillForm("创新科技公司", "CREDIT-1", "张三", "10000000.00", "技术服务", "20260916090000"); + donor.activateWallet("CORP_DONOR", "20260916091000"); + RemoteWalletApplication recipient = RemoteWalletApplication.create("u-1", 1L, 2L); + recipient.fillForm("阳光公益基金", "CREDIT-2", "李四", "13900139000", "20260916092000"); + recipient.activateWallet("CORP_RECIPIENT", "20260916093000"); + + when(corporateWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(donor)); + when(remoteWalletRepository.findLatest("u-1", 1L, 2L)).thenReturn(Optional.of(recipient)); + when(mapper.selectOne(any())).thenAnswer(invocation -> latest.get()); + when(mapper.insert(any(DonationRecordEntity.class))).thenAnswer(invocation -> { + DonationRecordEntity record = invocation.getArgument(0); + record.setId(100L); + latest.set(record); + return 1; + }); + when(mapper.updateById(any(DonationRecordEntity.class))).thenAnswer(invocation -> { + latest.set(invocation.getArgument(0)); + return 1; + }); + when(keyService.getOrCreatePrivateKey(any(), anyString(), anyString())).thenAnswer(invocation -> + "监管机构私钥".equals(invocation.getArgument(1)) ? "REGULATOR_KEY" : "DONOR_KEY"); + when(cryptography.sm3(anyString())).thenAnswer(invocation -> "HASH_" + invocation.getArgument(0)); + when(cryptography.sign(anyString(), anyString())).thenAnswer(invocation -> + "SIGN_" + invocation.getArgument(1)); + } + + @Test + void completesPrototypeDonationPublicationAndSupervisionFlow() { + DonationCreateRequest request = new DonationCreateRequest(); + request.setDonorName("创新科技公司"); + request.setDonorWalletId("CORP_DONOR"); + request.setRecipientName("阳光公益基金"); + request.setRecipientWalletId("CORP_RECIPIENT"); + request.setAmount(new BigDecimal("100000.00")); + request.setProjectName("乡村教育支持计划"); + request.setMemo("乡村教育支持计划捐款"); + request.setTimestamp("20260916100000"); + + service.concatenate(SUBJECT, "张三", request); + service.digest(SUBJECT); + service.signDonation(SUBJECT, "DONOR_KEY"); + service.outputDonation(SUBJECT); + service.receiptDetails(SUBJECT); + service.extractPublication(SUBJECT); + service.digestPublication(SUBJECT); + service.publish(SUBJECT); + service.outputPublication(SUBJECT); + service.extractSupervision(SUBJECT); + service.digestSupervision(SUBJECT); + service.signSupervision(SUBJECT, "REGULATOR_KEY"); + DonationPageResult result = service.outputSupervision(SUBJECT); + + assertThat(result.getStatus()).isEqualTo("ARCHIVED"); + assertThat(result.getDonationOutputJson()).contains("DONATE_20260916_001", "已提交"); + assertThat(result.getPublicationOutputJson()).contains("PUBLISHED", "阳光公益基金"); + assertThat(result.getSupervisionOutputJson()).contains("APPROVED", "已备案"); + verify(scoreService).markStepCompleted("u-1", 6); + verify(scoreService).markStepCompleted("u-1", 7); + verify(scoreService).markStepCompleted("u-1", 8); + } +} diff --git a/src/test/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchServiceTest.java b/src/test/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchServiceTest.java new file mode 100644 index 0000000..6080d7c --- /dev/null +++ b/src/test/java/com/yau/digitalrmb/corporatewallet/application/SalaryBatchServiceTest.java @@ -0,0 +1,90 @@ +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.SalaryBatch; +import com.yau.digitalrmb.corporatewallet.domain.SalaryBatchRepository; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SalaryBatchServiceTest { + private static final InstitutionKeySubject SUBJECT = + new InstitutionKeySubject("salary-user", 1001L, 2001L); + private static final String PRIVATE_KEY = "OPERATOR_PRIVATE_KEY"; + + @Mock private SalaryBatchRepository repository; + @Mock private CorporateWalletApplicationRepository corporateWalletRepository; + @Mock private InstitutionIdentityCryptography cryptography; + @Mock private SalaryScoreService scoreService; + @Mock private CorporateWalletKeyService keyService; + @InjectMocks private SalaryBatchService service; + + @Test + void startsFreshBatchAndSignsCurrentDigestWhenWorkflowRunsAgain() { + AtomicReference latest = new AtomicReference<>(); + AtomicLong ids = new AtomicLong(10L); + when(repository.save(any(SalaryBatch.class))).thenAnswer(invocation -> { + SalaryBatch batch = invocation.getArgument(0); + batch.setId(ids.incrementAndGet()); + latest.set(batch); + return batch; + }); + when(repository.findLatest(SUBJECT.getUserId(), SUBJECT.getSchoolId(), SUBJECT.getClassId())) + .thenAnswer(invocation -> Optional.ofNullable(latest.get())); + + CorporateWalletApplication wallet = CorporateWalletApplication.create( + SUBJECT.getUserId(), SUBJECT.getSchoolId(), SUBJECT.getClassId()); + wallet.setWalletId("CORP_WALLET_001"); + when(corporateWalletRepository.findLatest( + SUBJECT.getUserId(), SUBJECT.getSchoolId(), SUBJECT.getClassId())) + .thenReturn(Optional.of(wallet)); + when(cryptography.sm3(any(String.class))).thenReturn("DIGEST_ONE", "DIGEST_TWO"); + when(keyService.getOrCreatePrivateKey( + any(InstitutionKeySubject.class), eq("数币操作员私钥"), eq("王五"))) + .thenReturn(PRIVATE_KEY); + when(cryptography.sign(PRIVATE_KEY, "DIGEST_ONE")).thenReturn("SIGNATURE_ONE"); + when(cryptography.sign(PRIVATE_KEY, "DIGEST_TWO")).thenReturn("SIGNATURE_TWO"); + + service.fetchBlockchain(SUBJECT, "tester"); + service.concatenate(SUBJECT); + SalaryDigestResult firstDigest = service.computeDigest(SUBJECT); + SalaryBatch firstBatch = latest.get(); + assertThat(firstBatch.getStatus()).isEqualTo(SalaryBatch.Status.DIGESTED); + SalarySignatureResult firstSignature = service.sign(SUBJECT, PRIVATE_KEY); + + service.fetchBlockchain(SUBJECT, "tester"); + service.concatenate(SUBJECT); + SalaryDigestResult secondDigest = service.computeDigest(SUBJECT); + SalaryBatch secondBatch = latest.get(); + assertThat(secondBatch.getStatus()).isEqualTo(SalaryBatch.Status.DIGESTED); + SalarySignatureResult secondSignature = service.sign(SUBJECT, PRIVATE_KEY); + + assertThat(secondBatch).isNotSameAs(firstBatch); + assertThat(secondBatch.getId()).isNotEqualTo(firstBatch.getId()); + assertThat(firstDigest.getDigestValue()).isEqualTo("DIGEST_ONE"); + assertThat(secondDigest.getDigestValue()).isEqualTo("DIGEST_TWO"); + assertThat(firstSignature.getSignature()).isEqualTo("SIGNATURE_ONE"); + assertThat(secondSignature.getSignature()).isEqualTo("SIGNATURE_TWO"); + + verify(repository, times(2)).save(any(SalaryBatch.class)); + verify(cryptography).sign(PRIVATE_KEY, "DIGEST_ONE"); + verify(cryptography).sign(PRIVATE_KEY, "DIGEST_TWO"); + } +} diff --git a/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java b/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java index 96e5a8c..8a3d4ae 100644 --- a/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java +++ b/src/test/java/com/yau/digitalrmb/payment/interfaces/rest/PaymentControllerTest.java @@ -204,11 +204,71 @@ class PaymentControllerTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.payer.walletId").value(PAYER_WALLET)) - .andExpect(jsonPath("$.data.payee.walletId").value("WALLET_TRAINING_PAYEE_B")) + .andExpect(jsonPath("$.data.payee.walletId").value( + "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")) .andExpect(jsonPath("$.data.payee.bankCode").value("BKCHCNBJ00002")); assertThat(jdbc.queryForObject("SELECT status FROM digital_wallet WHERE wallet_id=?", String.class, - "WALLET_TRAINING_PAYEE_B")).isEqualTo("ACTIVE"); + "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")).isEqualTo("ACTIVE"); + } + + @Test + void actionChainUsesTeachingPayeeWithoutASecondWallet() throws Exception { + String created = mockMvc.perform(post("/api/v1/payment/attempts").with(payer())) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + String attemptId = JsonPath.read(created, "$.data.attemptId"); + + executeAction(attemptId, "01", "refresh-payer", payer()); + mockMvc.perform(post("/api/v1/payment/attempts/{id}/steps/01/actions/refresh-payee", attemptId) + .with(payer()).contentType(MediaType.APPLICATION_JSON).content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.output.walletId").value( + "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")) + .andExpect(jsonPath("$.data.output.walletType").value("一类钱包")) + .andExpect(jsonPath("$.data.output.certificateSerial").value("00C3D4E5F6A7B890")) + .andExpect(jsonPath("$.data.output.bankName").value("建设银行(BKCHCNBJ00002)")) + .andExpect(jsonPath("$.data.output.organizationId").value("ORG_5F6G7B8E9G0W")); + executeAction(attemptId, "02", "generate-timestamp", payer()); + mockMvc.perform(post("/api/v1/payment/attempts/{id}/steps/02/actions/concatenate-payment", attemptId) + .with(payer()).contentType(MediaType.APPLICATION_JSON) + .content("{\"payerWalletId\":\"" + PAYER_WALLET + "\",\"payeeWalletId\":\"WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6\",\"amount\":50,\"note\":\"教学支付\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.output.payeeWalletId").value( + "WALLET_B_7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6")); + } + + @Test + void refreshActionsProjectCompletedModuleThreeWalletsWhenSharedRowsAreMissing() throws Exception { + clearSharedWalletProjection(); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", + Integer.class, PAYER_WALLET, PAYEE_WALLET)).isZero(); + + String created = mockMvc.perform(post("/api/v1/payment/attempts").with(payer())) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + String attemptId = JsonPath.read(created, "$.data.attemptId"); + + mockMvc.perform(post("/api/v1/payment/attempts/{id}/steps/01/actions/refresh-payer", attemptId) + .with(payer()).contentType(MediaType.APPLICATION_JSON).content("{}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.actionStatus").value("COMPLETED")) + .andExpect(jsonPath("$.data.output.walletId").value(PAYER_WALLET)); + mockMvc.perform(post("/api/v1/payment/attempts/{id}/steps/01/actions/refresh-payee", attemptId) + .with(payer()).contentType(MediaType.APPLICATION_JSON) + .content("{\"payeeWalletId\":\"" + PAYEE_WALLET + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.actionStatus").value("COMPLETED")) + .andExpect(jsonPath("$.data.output.walletId").value(PAYEE_WALLET)); + + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM digital_wallet WHERE wallet_id IN (?,?)", + Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_certificate WHERE wallet_id IN (?,?)", + Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_contract WHERE wallet_id IN (?,?)", + Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); + assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM wallet_bank_binding WHERE wallet_id IN (?,?)", + Integer.class, PAYER_WALLET, PAYEE_WALLET)).isEqualTo(2); } @Test @@ -400,13 +460,17 @@ class PaymentControllerTest { } private void cleanWalletState() { + clearSharedWalletProjection(); + WalletOpeningTestData.deleteWalletFacts(jdbc, PAYER_USER); + WalletOpeningTestData.deleteWalletFacts(jdbc, PAYEE_USER); + } + + private void clearSharedWalletProjection() { jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); jdbc.update("DELETE FROM wallet_contract WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); jdbc.update("DELETE FROM simulated_bank_account WHERE user_id IN (?,?)", PAYER_USER, PAYEE_USER); jdbc.update("DELETE FROM digital_wallet WHERE wallet_id IN (?,?)", PAYER_WALLET, PAYEE_WALLET); - WalletOpeningTestData.deleteWalletFacts(jdbc, PAYER_USER); - WalletOpeningTestData.deleteWalletFacts(jdbc, PAYEE_USER); } private void cleanPaymentData() { diff --git a/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java b/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java index e56d428..7fdf339 100644 --- a/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java +++ b/src/test/java/com/yau/digitalrmb/shared/wallet/WalletPrerequisiteProjectionServiceTest.java @@ -250,6 +250,55 @@ class WalletPrerequisiteProjectionServiceTest { return jdbc.queryForObject("SELECT account_id FROM simulated_bank_account WHERE user_id=?", String.class, SUBJECT_USER); } + @Test + void retrainedModuleThreeReplacesUnusedStaleWalletInsteadOfConflicting() { + seedCompleted(); + service.ensureForSubject(subject()); + + String newWalletId = "WALLET_PROJECTION_TEST_RETRAIN"; + WalletOpeningTestData.deleteWalletFacts(jdbc, SUBJECT_USER); + WalletOpeningTestData.insertCompletedWallet(jdbc, 980100L, SUBJECT_USER, SCHOOL_ID, CLASS_ID, + newWalletId, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行", + "6216610100001234567", new BigDecimal("50000.00")); + + WalletPrerequisiteReference reference = service.ensureForSubject(subject()); + + assertThat(reference.getWalletId()).isEqualTo(newWalletId); + assertThat(count("digital_wallet", "wallet_id", WALLET_ID)).isZero(); + assertThat(count("digital_wallet", "wallet_id", newWalletId)).isEqualTo(1); + assertThat(count("wallet_certificate", "wallet_id", newWalletId)).isEqualTo(1); + assertThat(count("wallet_contract", "wallet_id", newWalletId)).isEqualTo(1); + assertThat(count("wallet_bank_binding", "wallet_id", newWalletId)).isEqualTo(1); + jdbc.update("DELETE FROM wallet_bank_binding WHERE wallet_id=?", newWalletId); + jdbc.update("DELETE FROM wallet_contract WHERE wallet_id=?", newWalletId); + jdbc.update("DELETE FROM wallet_certificate WHERE wallet_id=?", newWalletId); + jdbc.update("DELETE FROM digital_wallet WHERE wallet_id=?", newWalletId); + } + + @Test + void retrainedModuleThreeStillConflictsWhenStaleWalletWasActuallyUsed() { + seedCompleted(); + service.ensureForSubject(subject()); + String accountId = jdbc.queryForObject( + "SELECT account_id FROM simulated_bank_account WHERE user_id=?", String.class, SUBJECT_USER); + jdbc.update("INSERT INTO currency_exchange_order (id,request_no,user_id,wallet_id,bank_account_id,bank_code," + + "organization_id,amount,currency,request_timestamp,message_text,signing_original_text,digest," + + "status,created_at,updated_at,created_by,updated_by) VALUES " + + "('00000000-0000-0000-0000-000000000880','PROJECTION_USED',?,?,?,?," + + "'ORG_EXPECTED',1,'CNY','20260904090000','TEST','TEST','TEST','CANCELLED'," + + "CURRENT_TIMESTAMP,CURRENT_TIMESTAMP,'test','test')", + SUBJECT_USER, WALLET_ID, accountId, BANK_CODE); + + String newWalletId = "WALLET_PROJECTION_TEST_RETRAIN_USED"; + WalletOpeningTestData.deleteWalletFacts(jdbc, SUBJECT_USER); + WalletOpeningTestData.insertCompletedWallet(jdbc, 980200L, SUBJECT_USER, SCHOOL_ID, CLASS_ID, + newWalletId, keyPair.getPrivateKey(), keyPair.getPublicKey(), "测试银行", + "6216610100001234567", new BigDecimal("50000.00")); + + assertThatThrownBy(() -> service.ensureForSubject(subject())) + .isInstanceOf(BusinessException.class).hasMessageContaining("共享钱包与模块三最终结果不一致"); + } + @Test void sameBankCodeFromAnotherScopeCannotSupplyOrganizationId() { seedCompleted(); diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index c16904b..fb43be5 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -536,9 +536,9 @@ CREATE TABLE IF NOT EXISTS wallet_application ( phone VARCHAR(20), input_phone VARCHAR(20), input_id_number VARCHAR(20), - input_bank_card_number VARCHAR(30), + input_bank_card_number VARCHAR(64), account_bank VARCHAR(100), - bank_card_number VARCHAR(30), + bank_card_number VARCHAR(64), account_balance DECIMAL(20,2), selected_bank VARCHAR(100), wallet_type VARCHAR(20), @@ -883,6 +883,24 @@ CREATE TABLE IF NOT EXISTS payment_order ( FOREIGN KEY (payee_wallet_id) REFERENCES digital_wallet(wallet_id) ); +CREATE TABLE IF NOT EXISTS donation_record ( + id BIGINT PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, school_id BIGINT NOT NULL, class_id BIGINT NOT NULL, + donation_id VARCHAR(64), donor_name VARCHAR(200), donor_wallet_id VARCHAR(128), + recipient_name VARCHAR(200), recipient_wallet_id VARCHAR(128), amount DECIMAL(20,2), + project_name VARCHAR(200), memo VARCHAR(500), donation_timestamp VARCHAR(20), + donation_message CLOB, donation_digest VARCHAR(128), donor_signature CLOB, donation_output_json CLOB, + publication_timestamp VARCHAR(20), publication_message CLOB, publication_digest VARCHAR(128), + publication_id VARCHAR(64), publication_title VARCHAR(300), publication_content VARCHAR(1000), + publication_url VARCHAR(500), publication_output_json CLOB, + supervision_timestamp VARCHAR(20), supervision_message CLOB, supervision_digest VARCHAR(128), + regulator_signature CLOB, approval_id VARCHAR(64), approval_time VARCHAR(20), + record_number VARCHAR(64), supervision_output_json CLOB, status VARCHAR(32) NOT NULL DEFAULT 'DRAFT', + 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 training_task_step_progress ( user_id VARCHAR(36) NOT NULL, module_code VARCHAR(64) NOT NULL,