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..2976a7b --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/CorporateInnovationTrainingResetService.java @@ -0,0 +1,82 @@ +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.DonationRecordEntity; +import com.yau.digitalrmb.corporatewallet.infrastructure.DonationRecordMapper; +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 DonationRecordMapper donationRecordMapper; + @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) { + return donationRecordMapper.update(null, new LambdaUpdateWrapper() + .eq(DonationRecordEntity::getUserId, subject.getUserId()) + .eq(DonationRecordEntity::getSchoolId, subject.getSchoolId()) + .eq(DonationRecordEntity::getClassId, subject.getClassId()) + .set(DonationRecordEntity::getDeleted, true)); + } + + 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/DonationService.java b/src/main/java/com/yau/digitalrmb/corporatewallet/application/DonationService.java new file mode 100644 index 0000000..363edcf --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/application/DonationService.java @@ -0,0 +1,418 @@ +package com.yau.digitalrmb.corporatewallet.application; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.core.JsonProcessingException; +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 com.yau.digitalrmb.shared.api.ErrorCode; +import com.yau.digitalrmb.shared.exception.BusinessException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Map; + +@Service +public class DonationService { + private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private static final String DONOR_KEY_TYPE = "企业法人私钥"; + private static final String REGULATOR_KEY_TYPE = "监管机构私钥"; + private static final String REGULATOR_NAME = "民政部公益监管中心"; + + private enum Status { + DRAFT, CONCATENATED, DIGESTED, SIGNED, SUBMITTED, RECEIVED, + PUBLICATION_EXTRACTED, PUBLICATION_DIGESTED, PUBLICATION_READY, PUBLISHED, + SUPERVISION_EXTRACTED, SUPERVISION_DIGESTED, SUPERVISION_SIGNED, ARCHIVED + } + + @Resource private DonationRecordMapper mapper; + @Resource private CorporateWalletApplicationRepository corporateWalletRepository; + @Resource private RemoteWalletApplicationRepository remoteWalletRepository; + @Resource private CorporateWalletKeyService keyService; + @Resource private InstitutionIdentityCryptography cryptography; + @Resource private SalaryScoreService scoreService; + @Resource private ObjectMapper objectMapper; + + @Transactional + public DonationPageResult page(InstitutionKeySubject subject, String operator) { + DonationRecordEntity record = findLatest(subject); + CorporateWalletApplication donor = donorWallet(subject); + RemoteWalletApplication recipient = recipientWallet(subject); + return result(subject, record, donor, recipient, operator); + } + + @Transactional + public DonationPageResult concatenate(InstitutionKeySubject subject, String operator, + DonationCreateRequest request) { + CorporateWalletApplication donor = requiredDonorWallet(subject); + RemoteWalletApplication recipient = requiredRecipientWallet(subject); + validateWalletInputs(donor, recipient, request); + + DonationRecordEntity record = findLatest(subject); + if (record == null || status(record).ordinal() >= Status.SUBMITTED.ordinal()) { + record = new DonationRecordEntity(); + record.setUserId(subject.getUserId()); + record.setSchoolId(subject.getSchoolId()); + record.setClassId(subject.getClassId()); + record.setDeleted(false); + record.setStatus(Status.DRAFT.name()); + } + + String amount = money(request.getAmount()); + String message = String.join("|", "DONATE", request.getDonorWalletId(), + request.getRecipientWalletId(), amount, request.getProjectName().trim(), + request.getMemo().trim(), request.getTimestamp()); + record.setDonationId("DONATE_" + request.getTimestamp().substring(0, 8) + "_001"); + record.setDonorName(request.getDonorName().trim()); + record.setDonorWalletId(request.getDonorWalletId().trim()); + record.setRecipientName(request.getRecipientName().trim()); + record.setRecipientWalletId(request.getRecipientWalletId().trim()); + record.setAmount(request.getAmount().setScale(2)); + record.setProjectName(request.getProjectName().trim()); + record.setMemo(request.getMemo().trim()); + record.setDonationTimestamp(request.getTimestamp()); + record.setDonationMessage(message); + record.setDonationDigest(null); + record.setDonorSignature(null); + record.setDonationOutputJson(null); + record.setStatus(Status.CONCATENATED.name()); + save(record); + return result(subject, record, donor, recipient, operator); + } + + @Transactional + public DonationPageResult digest(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.CONCATENATED, "请先拼接捐赠原文"); + if (record.getDonationDigest() == null) { + record.setDonationDigest(cryptography.sm3(record.getDonationMessage())); + record.setStatus(Status.DIGESTED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getDonorName()); + } + + @Transactional + public DonationPageResult signDonation(InstitutionKeySubject subject, String providedPrivateKey) { + DonationRecordEntity record = required(subject, Status.DIGESTED, "请先生成捐赠摘要"); + String privateKey = keyService.getOrCreatePrivateKey(subject, DONOR_KEY_TYPE, record.getDonorName()); + validatePrivateKey(subject, privateKey, providedPrivateKey, "企业法人私钥不正确,错误次数+1"); + if (record.getDonorSignature() == null) { + record.setDonorSignature(cryptography.sign(privateKey, record.getDonationDigest())); + record.setStatus(Status.SIGNED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getDonorName()); + } + + @Transactional + public DonationPageResult outputDonation(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.SIGNED, "请先完成企业法人SM2签名"); + if (record.getDonationOutputJson() == null) { + Map output = new LinkedHashMap<>(); + output.put("donationId", record.getDonationId()); + output.put("corpWalletId", record.getDonorWalletId()); + output.put("orgWalletId", record.getRecipientWalletId()); + output.put("amount", money(record.getAmount())); + output.put("project", record.getProjectName()); + output.put("memo", record.getMemo()); + output.put("digest", record.getDonationDigest()); + output.put("corpSignature", record.getDonorSignature()); + output.put("timestamp", record.getDonationTimestamp()); + output.put("status", "已提交"); + record.setDonationOutputJson(json(output)); + record.setStatus(Status.SUBMITTED.name()); + mapper.updateById(record); + scoreService.markStepCompleted(subject.getUserId(), 6); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getDonorName()); + } + + @Transactional + public DonationPageResult receiptDetails(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.SUBMITTED, "请先由捐赠企业提交捐赠报文"); + if (status(record).ordinal() < Status.RECEIVED.ordinal()) { + record.setStatus(Status.RECEIVED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName()); + } + + @Transactional + public DonationPageResult extractPublication(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.RECEIVED, "请先查看捐款明细"); + if (record.getPublicationMessage() == null) { + String timestamp = now(); + String title = publicationTitle(record, timestamp); + record.setPublicationTimestamp(timestamp); + record.setPublicationTitle(title); + record.setPublicationMessage(String.join("|", "PUBLIC", record.getRecipientWalletId(), + title, record.getDonorName(), money(record.getAmount()), + record.getProjectName(), timestamp)); + record.setStatus(Status.PUBLICATION_EXTRACTED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName()); + } + + @Transactional + public DonationPageResult digestPublication(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.PUBLICATION_EXTRACTED, "请先提取公示拼接原文"); + if (record.getPublicationDigest() == null) { + record.setPublicationDigest(cryptography.sm3(record.getPublicationMessage())); + record.setStatus(Status.PUBLICATION_DIGESTED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName()); + } + + @Transactional + public DonationPageResult publish(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.PUBLICATION_DIGESTED, "请先生成资金公示摘要"); + if (record.getPublicationId() == null) { + String month = record.getPublicationTimestamp().substring(0, 6); + record.setPublicationId("PUBLIC_" + record.getPublicationTimestamp().substring(0, 8) + "_001"); + record.setPublicationContent("本月收到" + record.getDonorName() + "捐赠" + moneyWithGrouping(record.getAmount()) + + "元,用于" + record.getProjectName()); + record.setPublicationUrl("https://www.sunshine.org/donate/" + month); + record.setStatus(Status.PUBLICATION_READY.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName()); + } + + @Transactional + public DonationPageResult outputPublication(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.PUBLICATION_READY, "请先发布资金公示"); + if (record.getPublicationOutputJson() == null) { + Map output = new LinkedHashMap<>(); + output.put("publicationId", record.getPublicationId()); + output.put("orgId", record.getRecipientWalletId()); + output.put("title", record.getPublicationTitle()); + output.put("content", record.getPublicationContent()); + output.put("url", record.getPublicationUrl()); + output.put("digest", record.getPublicationDigest()); + output.put("publishTime", record.getPublicationTimestamp()); + output.put("status", "PUBLISHED"); + record.setPublicationOutputJson(json(output)); + record.setStatus(Status.PUBLISHED.name()); + mapper.updateById(record); + scoreService.markStepCompleted(subject.getUserId(), 7); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), record.getRecipientName()); + } + + @Transactional + public DonationPageResult extractSupervision(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.PUBLISHED, "请先完成受赠机构资金公示"); + if (record.getSupervisionMessage() == null) { + String timestamp = now(); + record.setSupervisionTimestamp(timestamp); + record.setSupervisionMessage(String.join("|", "SUPERVISE", record.getDonationId(), + REGULATOR_NAME, money(record.getAmount()), record.getProjectName(), timestamp)); + record.setStatus(Status.SUPERVISION_EXTRACTED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME); + } + + @Transactional + public DonationPageResult digestSupervision(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.SUPERVISION_EXTRACTED, "请先提取监管审核原文"); + if (record.getSupervisionDigest() == null) { + record.setSupervisionDigest(cryptography.sm3(record.getSupervisionMessage())); + record.setStatus(Status.SUPERVISION_DIGESTED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME); + } + + @Transactional + public DonationPageResult signSupervision(InstitutionKeySubject subject, String providedPrivateKey) { + DonationRecordEntity record = required(subject, Status.SUPERVISION_DIGESTED, "请先生成监管审核摘要"); + String privateKey = keyService.getOrCreatePrivateKey(subject, REGULATOR_KEY_TYPE, REGULATOR_NAME); + validatePrivateKey(subject, privateKey, providedPrivateKey, "监管机构私钥不正确,错误次数+1"); + if (record.getRegulatorSignature() == null) { + record.setRegulatorSignature(cryptography.sign(privateKey, record.getSupervisionDigest())); + record.setStatus(Status.SUPERVISION_SIGNED.name()); + mapper.updateById(record); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME); + } + + @Transactional + public DonationPageResult outputSupervision(InstitutionKeySubject subject) { + DonationRecordEntity record = required(subject, Status.SUPERVISION_SIGNED, "请先完成监管机构SM2签名"); + if (record.getSupervisionOutputJson() == null) { + String date = record.getSupervisionTimestamp().substring(0, 8); + record.setApprovalId("APPROVE_" + date + "_001"); + record.setApprovalTime(record.getSupervisionTimestamp()); + record.setRecordNumber("RECORD_" + date + "_001"); + Map output = new LinkedHashMap<>(); + output.put("approvalId", record.getApprovalId()); + output.put("donationId", record.getDonationId()); + output.put("supervisor", REGULATOR_NAME); + output.put("approvalResult", "APPROVED"); + output.put("supervisorSignature", record.getRegulatorSignature()); + output.put("approvalTime", record.getApprovalTime()); + output.put("recordNumber", record.getRecordNumber()); + output.put("status", "已备案"); + record.setSupervisionOutputJson(json(output)); + record.setStatus(Status.ARCHIVED.name()); + mapper.updateById(record); + scoreService.markStepCompleted(subject.getUserId(), 8); + } + return result(subject, record, donorWallet(subject), recipientWallet(subject), REGULATOR_NAME); + } + + private DonationPageResult result(InstitutionKeySubject subject, DonationRecordEntity record, + CorporateWalletApplication donor, RemoteWalletApplication recipient, + String operator) { + String donorName = record != null ? record.getDonorName() : donor == null ? null : donor.getCorpName(); + String recipientName = record != null ? record.getRecipientName() : recipient == null ? null : recipient.getCorpName(); + String effectiveOperator = operator == null || operator.trim().isEmpty() ? "企业法人" : operator; + String donorKey = keyService.getOrCreatePrivateKey(subject, DONOR_KEY_TYPE, effectiveOperator); + String regulatorKey = keyService.getOrCreatePrivateKey(subject, REGULATOR_KEY_TYPE, REGULATOR_NAME); + return DonationPageResult.builder() + .donorName(donorName) + .donorWalletId(record != null ? record.getDonorWalletId() : donor == null ? null : donor.getWalletId()) + .recipientName(recipientName) + .recipientWalletId(record != null ? record.getRecipientWalletId() : recipient == null ? null : recipient.getWalletId()) + .amount(record == null || record.getAmount() == null ? "100000.00" : money(record.getAmount())) + .projectName(record == null ? "乡村教育支持计划" : record.getProjectName()) + .memo(record == null ? "乡村教育支持计划捐款" : record.getMemo()) + .donationTimestamp(record == null ? null : record.getDonationTimestamp()) + .donationId(record == null ? null : record.getDonationId()) + .donationMessage(record == null ? null : record.getDonationMessage()) + .donationDigest(record == null ? null : record.getDonationDigest()) + .donorPrivateKey(donorKey) + .donorSignature(record == null ? null : record.getDonorSignature()) + .donationOutputJson(record == null ? null : record.getDonationOutputJson()) + .receiptStatus(record != null && status(record).ordinal() >= Status.RECEIVED.ordinal() ? "已到账" : null) + .publicationTimestamp(record == null ? null : record.getPublicationTimestamp()) + .publicationMessage(record == null ? null : record.getPublicationMessage()) + .publicationDigest(record == null ? null : record.getPublicationDigest()) + .publicationId(record == null ? null : record.getPublicationId()) + .publicationTitle(record == null ? null : record.getPublicationTitle()) + .publicationContent(record == null ? null : record.getPublicationContent()) + .publicationUrl(record == null ? null : record.getPublicationUrl()) + .publicationOutputJson(record == null ? null : record.getPublicationOutputJson()) + .supervisionTimestamp(record == null ? null : record.getSupervisionTimestamp()) + .supervisionMessage(record == null ? null : record.getSupervisionMessage()) + .supervisionDigest(record == null ? null : record.getSupervisionDigest()) + .regulatorPrivateKey(regulatorKey) + .regulatorSignature(record == null ? null : record.getRegulatorSignature()) + .approvalId(record == null ? null : record.getApprovalId()) + .approvalTime(record == null ? null : record.getApprovalTime()) + .recordNumber(record == null ? null : record.getRecordNumber()) + .supervisionOutputJson(record == null ? null : record.getSupervisionOutputJson()) + .status(record == null ? null : record.getStatus()) + .build(); + } + + private DonationRecordEntity required(InstitutionKeySubject subject, Status minimum, String message) { + DonationRecordEntity record = findLatest(subject); + if (record == null || status(record).ordinal() < minimum.ordinal()) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, message); + } + return record; + } + + private DonationRecordEntity findLatest(InstitutionKeySubject subject) { + return mapper.selectOne(new LambdaQueryWrapper() + .eq(DonationRecordEntity::getUserId, subject.getUserId()) + .eq(DonationRecordEntity::getSchoolId, subject.getSchoolId()) + .eq(DonationRecordEntity::getClassId, subject.getClassId()) + .eq(DonationRecordEntity::getDeleted, false) + .orderByDesc(DonationRecordEntity::getCreatedAt) + .orderByDesc(DonationRecordEntity::getId) + .last("LIMIT 1")); + } + + private CorporateWalletApplication donorWallet(InstitutionKeySubject subject) { + return corporateWalletRepository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId()) + .orElse(null); + } + + private RemoteWalletApplication recipientWallet(InstitutionKeySubject subject) { + return remoteWalletRepository.findLatest(subject.getUserId(), subject.getSchoolId(), subject.getClassId()) + .orElse(null); + } + + private CorporateWalletApplication requiredDonorWallet(InstitutionKeySubject subject) { + CorporateWalletApplication wallet = donorWallet(subject); + if (wallet == null || wallet.getWalletId() == null || wallet.getStatus() != CorporateWalletApplication.Status.ACTIVATED) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成临柜开立对公钱包"); + } + return wallet; + } + + private RemoteWalletApplication requiredRecipientWallet(InstitutionKeySubject subject) { + RemoteWalletApplication wallet = recipientWallet(subject); + if (wallet == null || wallet.getWalletId() == null || wallet.getStatus() != RemoteWalletApplication.Status.ACTIVATED) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成远程开立受赠机构对公钱包"); + } + return wallet; + } + + private void validateWalletInputs(CorporateWalletApplication donor, RemoteWalletApplication recipient, + DonationCreateRequest request) { + if (!same(donor.getCorpName(), request.getDonorName()) + || !same(donor.getWalletId(), request.getDonorWalletId())) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "捐赠企业或付款钱包与对公钱包开立结果不一致"); + } + if (!same(recipient.getCorpName(), request.getRecipientName()) + || !same(recipient.getWalletId(), request.getRecipientWalletId())) { + throw new BusinessException(ErrorCode.VALIDATION_ERROR, "受赠机构或收款钱包与远程开立结果不一致"); + } + } + + private void validatePrivateKey(InstitutionKeySubject subject, String expected, String actual, String message) { + if (!same(expected, actual)) { + scoreService.recordError(subject.getUserId()); + throw new BusinessException(ErrorCode.VALIDATION_ERROR, message); + } + } + + private void save(DonationRecordEntity record) { + if (record.getId() == null) mapper.insert(record); else mapper.updateById(record); + } + + private String publicationTitle(DonationRecordEntity record, String timestamp) { + int month = Integer.parseInt(timestamp.substring(4, 6)); + return record.getRecipientName() + timestamp.substring(0, 4) + "年" + month + "月捐赠收入公示"; + } + + private String json(Map value) { + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value); + } catch (JsonProcessingException exception) { + throw new IllegalStateException("公益捐赠报文序列化失败", exception); + } + } + + private Status status(DonationRecordEntity record) { + return record.getStatus() == null ? Status.DRAFT : Status.valueOf(record.getStatus()); + } + + private String now() { return LocalDateTime.now().format(TIMESTAMP_FORMAT); } + + private String money(BigDecimal amount) { return amount.setScale(2).toPlainString(); } + + private String moneyWithGrouping(BigDecimal amount) { return String.format("%,.2f", amount); } + + private boolean same(String left, String right) { + return left != null && right != null && left.trim().equalsIgnoreCase(right.trim()); + } +} 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/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/DonationController.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/DonationController.java new file mode 100644 index 0000000..f9fb14e --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/DonationController.java @@ -0,0 +1,103 @@ +package com.yau.digitalrmb.corporatewallet.interfaces.rest; + +import com.yau.digitalrmb.corporatewallet.application.DonationPageResult; +import com.yau.digitalrmb.corporatewallet.application.DonationService; +import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationCreateRequest; +import com.yau.digitalrmb.corporatewallet.interfaces.rest.dto.DonationSignRequest; +import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject; +import com.yau.digitalrmb.security.context.AuthContextHolder; +import com.yau.digitalrmb.security.context.JwtUser; +import com.yau.digitalrmb.shared.api.ApiResponse; +import com.yau.digitalrmb.shared.web.TraceIdFilter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.MDC; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/v1/donations") +@Tag(name = "模块六-3-企业公益捐赠", description = "捐赠企业发起、受赠机构公示、第三方监管审核三步流程") +public class DonationController { + @Resource private DonationService donationService; + + @GetMapping("/page") + @Operation(summary = "查询公益捐赠三步流程页面数据") + public ApiResponse page() { + JwtUser user = AuthContextHolder.get(); + return success(donationService.page(InstitutionKeySubject.from(user), user.getUsername())); + } + + @PostMapping("/concatenate") + @Operation(summary = "拼接捐赠原文") + public ApiResponse concatenate(@Valid @RequestBody DonationCreateRequest request) { + JwtUser user = AuthContextHolder.get(); + return success(donationService.concatenate(InstitutionKeySubject.from(user), user.getUsername(), request)); + } + + @PostMapping("/digest") + @Operation(summary = "生成捐赠原文SM3摘要") + public ApiResponse digest() { return success(donationService.digest(subject())); } + + @PostMapping("/sign") + @Operation(summary = "使用企业法人私钥对捐赠摘要进行SM2签名") + public ApiResponse sign(@Valid @RequestBody DonationSignRequest request) { + return success(donationService.signDonation(subject(), request.getPrivateKey())); + } + + @PostMapping("/output") + @Operation(summary = "输出并提交捐赠报文") + public ApiResponse output() { return success(donationService.outputDonation(subject())); } + + @PostMapping("/receipt/details") + @Operation(summary = "受赠机构查看到账捐赠明细") + public ApiResponse receiptDetails() { return success(donationService.receiptDetails(subject())); } + + @PostMapping("/publication/extract") + @Operation(summary = "提取资金公示拼接原文") + public ApiResponse extractPublication() { return success(donationService.extractPublication(subject())); } + + @PostMapping("/publication/digest") + @Operation(summary = "生成资金公示原文SM3摘要") + public ApiResponse digestPublication() { return success(donationService.digestPublication(subject())); } + + @PostMapping("/publication/publish") + @Operation(summary = "发布资金公示") + public ApiResponse publish() { return success(donationService.publish(subject())); } + + @PostMapping("/publication/output") + @Operation(summary = "输出资金公示报文") + public ApiResponse outputPublication() { return success(donationService.outputPublication(subject())); } + + @PostMapping("/supervision/extract") + @Operation(summary = "提取监管审核捐赠原文") + public ApiResponse extractSupervision() { return success(donationService.extractSupervision(subject())); } + + @PostMapping("/supervision/digest") + @Operation(summary = "生成监管审核原文SM3摘要") + public ApiResponse digestSupervision() { return success(donationService.digestSupervision(subject())); } + + @PostMapping("/supervision/sign") + @Operation(summary = "使用监管机构私钥对审核摘要进行SM2签名") + public ApiResponse signSupervision(@Valid @RequestBody DonationSignRequest request) { + return success(donationService.signSupervision(subject(), request.getPrivateKey())); + } + + @PostMapping("/supervision/output") + @Operation(summary = "输出监管审核备案报文") + public ApiResponse outputSupervision() { return success(donationService.outputSupervision(subject())); } + + private InstitutionKeySubject subject() { + return InstitutionKeySubject.from(AuthContextHolder.get()); + } + + private ApiResponse success(DonationPageResult result) { + return ApiResponse.success(result, 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/corporatewallet/interfaces/rest/dto/DonationSignRequest.java b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/DonationSignRequest.java new file mode 100644 index 0000000..58a12be --- /dev/null +++ b/src/main/java/com/yau/digitalrmb/corporatewallet/interfaces/rest/dto/DonationSignRequest.java @@ -0,0 +1,13 @@ +package com.yau.digitalrmb.corporatewallet.interfaces.rest.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@Schema(description = "公益捐赠SM2签名请求") +public class DonationSignRequest { + @NotBlank(message = "私钥不能为空") + private String privateKey; +} 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 0e0d2d9..a02f362 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1269,6 +1269,50 @@ CREATE TABLE IF NOT EXISTS salary_batch ( 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='模块六-公益捐赠记录表'; + -- 步骤四商业银行端:生成合约信息与开通确认 CREATE TABLE IF NOT EXISTS smart_contract_generation ( id BIGINT PRIMARY KEY, 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/resources/schema.sql b/src/test/resources/schema.sql index c16904b..4996196 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -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,