feat:发行货币的功能开发
parent
c6f2a24293
commit
f92cb83566
@ -0,0 +1,29 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication;
|
||||
|
||||
public class CommercialBankInstitutionIdentifierResult {
|
||||
private final long applicationId;
|
||||
private final String applicationStatus;
|
||||
private final int status;
|
||||
private final String statusText;
|
||||
private final String institutionIdentifier;
|
||||
private final String fullInstitutionIdentifier;
|
||||
|
||||
public CommercialBankInstitutionIdentifierResult(InstitutionIdentifierApplication application) {
|
||||
boolean feedbacked = application.getStatus() == InstitutionIdentifierApplication.Status.FEEDBACKED;
|
||||
this.applicationId = application.getId();
|
||||
this.applicationStatus = application.getStatus().name();
|
||||
this.status = feedbacked ? 2 : 1;
|
||||
this.statusText = feedbacked ? "已确认" : "待确认";
|
||||
this.institutionIdentifier = feedbacked ? application.getInstitutionIdentifier() : null;
|
||||
this.fullInstitutionIdentifier = feedbacked ? application.getDigest() : null;
|
||||
}
|
||||
|
||||
public long getApplicationId() { return applicationId; }
|
||||
public String getApplicationStatus() { return applicationStatus; }
|
||||
public int getStatus() { return status; }
|
||||
public String getStatusText() { return statusText; }
|
||||
public String getInstitutionIdentifier() { return institutionIdentifier; }
|
||||
public String getFullInstitutionIdentifier() { return fullInstitutionIdentifier; }
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierRepository;
|
||||
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;
|
||||
|
||||
@Service
|
||||
public class InstitutionIdentifierFeedbackService {
|
||||
@Resource
|
||||
private InstitutionIdentifierRepository repository;
|
||||
|
||||
@Resource
|
||||
private InstitutionTrainingScoreService scoreService;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InstitutionIdentifierFinalResult finalIdentifier(InstitutionKeySubject subject) {
|
||||
InstitutionIdentifierApplication application = requireLatest(subject);
|
||||
if (application.getStatus() != InstitutionIdentifierApplication.Status.ISSUED
|
||||
&& application.getStatus() != InstitutionIdentifierApplication.Status.FEEDBACKED) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"央行尚未完成签名确权,不能查看最终机构标识,当前状态:"
|
||||
+ application.getStatus().name());
|
||||
}
|
||||
return new InstitutionIdentifierFinalResult(application);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InstitutionIdentifierFinalResult feedback(InstitutionKeySubject subject, String operator) {
|
||||
InstitutionIdentifierApplication application = requireLatest(subject);
|
||||
if (application.getStatus() != InstitutionIdentifierApplication.Status.ISSUED) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"当前没有待反馈的最终机构标识,当前状态:" + application.getStatus().name());
|
||||
}
|
||||
InstitutionIdentifierApplication.Status expectedStatus = application.getStatus();
|
||||
invokeDomain(application::feedback);
|
||||
repository.update(application, expectedStatus, "FEEDBACK",
|
||||
"央行将最终机构标识同步反馈到商业银行端", operator);
|
||||
scoreService.markStepCompleted(subject.getUserId(), 1);
|
||||
return new InstitutionIdentifierFinalResult(application);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CommercialBankInstitutionIdentifierResult commercialBankResult(InstitutionKeySubject subject) {
|
||||
InstitutionIdentifierApplication application = requireLatest(subject);
|
||||
if (!isVisibleToCommercialBank(application.getStatus())) {
|
||||
throw new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
|
||||
"商业银行第二私钥签名成功后才会出现待确认数据");
|
||||
}
|
||||
return new CommercialBankInstitutionIdentifierResult(application);
|
||||
}
|
||||
|
||||
private InstitutionIdentifierApplication requireLatest(InstitutionKeySubject subject) {
|
||||
return repository.findLatestBySubject(subject.getUserId(), subject.getSchoolId(), subject.getClassId())
|
||||
.orElseThrow(() -> new BusinessException(
|
||||
ErrorCode.RESOURCE_NOT_FOUND, "当前没有机构标识申请数据"));
|
||||
}
|
||||
|
||||
private static boolean isVisibleToCommercialBank(InstitutionIdentifierApplication.Status status) {
|
||||
return status == InstitutionIdentifierApplication.Status.SIGNED
|
||||
|| status == InstitutionIdentifierApplication.Status.PACKAGED
|
||||
|| status == InstitutionIdentifierApplication.Status.VERIFIED
|
||||
|| status == InstitutionIdentifierApplication.Status.ISSUED
|
||||
|| status == InstitutionIdentifierApplication.Status.FEEDBACKED;
|
||||
}
|
||||
|
||||
private static void invokeDomain(Runnable operation) {
|
||||
try {
|
||||
operation.run();
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentifierApplication;
|
||||
|
||||
public class InstitutionIdentifierFinalResult {
|
||||
private final long applicationId;
|
||||
private final String applicationStatus;
|
||||
private final int feedbackStatus;
|
||||
private final String feedbackStatusText;
|
||||
private final String institutionIdentifier;
|
||||
private final String fullInstitutionIdentifier;
|
||||
private final String centralBankSignature;
|
||||
|
||||
public InstitutionIdentifierFinalResult(InstitutionIdentifierApplication application) {
|
||||
boolean feedbacked = application.getStatus() == InstitutionIdentifierApplication.Status.FEEDBACKED;
|
||||
this.applicationId = application.getId();
|
||||
this.applicationStatus = application.getStatus().name();
|
||||
this.feedbackStatus = feedbacked ? 2 : 1;
|
||||
this.feedbackStatusText = feedbacked ? "已反馈" : "待反馈";
|
||||
this.institutionIdentifier = application.getInstitutionIdentifier();
|
||||
this.fullInstitutionIdentifier = application.getDigest();
|
||||
this.centralBankSignature = application.getCentralBankSignature();
|
||||
}
|
||||
|
||||
public long getApplicationId() { return applicationId; }
|
||||
public String getApplicationStatus() { return applicationStatus; }
|
||||
public int getFeedbackStatus() { return feedbackStatus; }
|
||||
public String getFeedbackStatusText() { return feedbackStatusText; }
|
||||
public String getInstitutionIdentifier() { return institutionIdentifier; }
|
||||
public String getFullInstitutionIdentifier() { return fullInstitutionIdentifier; }
|
||||
public String getCentralBankSignature() { return centralBankSignature; }
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 将本实验的错误次数、完成进度和项目得分同步到平台统一成绩明细表。
|
||||
*/
|
||||
@Service
|
||||
public class InstitutionTrainingScoreService {
|
||||
public static final String MODULE_NAME = "中国人民银行生成数字货币";
|
||||
private static final int TRAINING_SERIAL_NUMBER = 3;
|
||||
private static final double TOTAL_SCORE = 100.0;
|
||||
private static final int TOTAL_TRAINING_MODULES = 9;
|
||||
|
||||
@Resource
|
||||
private StuModuleScoreDetailsMapper scoreDetailsMapper;
|
||||
|
||||
@Value("${institution-training.score.wrong-deduct:0.50}")
|
||||
private BigDecimal wrongDeduct = new BigDecimal("0.50");
|
||||
|
||||
@Transactional
|
||||
public void recordError(String userId, int errorCount) {
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StuModuleScoreDetailsEntity detail = getOrCreate(userId);
|
||||
detail.setCompletionStatus(String.valueOf(errorCount));
|
||||
detail.setScoreProject(score(errorCount, progress(detail.getSchedule())));
|
||||
scoreDetailsMapper.updateById(detail);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markStepCompleted(String userId, int completedStep) {
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StuModuleScoreDetailsEntity detail = getOrCreate(userId);
|
||||
int errorCount = errorCount(detail.getCompletionStatus());
|
||||
int normalizedStep = Math.max(0, Math.min(TOTAL_TRAINING_MODULES, completedStep));
|
||||
double schedule = BigDecimal.valueOf(normalizedStep)
|
||||
.multiply(BigDecimal.valueOf(TOTAL_SCORE))
|
||||
.divide(BigDecimal.valueOf(TOTAL_TRAINING_MODULES), 2, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
detail.setCompletionStatus(String.valueOf(errorCount));
|
||||
detail.setSchedule(schedule);
|
||||
detail.setScoreProject(score(errorCount, schedule));
|
||||
scoreDetailsMapper.updateById(detail);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public StuModuleScoreDetailsEntity detail(String userId) {
|
||||
if (userId == null || userId.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return scoreDetailsMapper.selectOne(new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
|
||||
.eq(StuModuleScoreDetailsEntity::getUserId, userId)
|
||||
.eq(StuModuleScoreDetailsEntity::getMoudule, MODULE_NAME)
|
||||
.eq(StuModuleScoreDetailsEntity::getSerialNumber, TRAINING_SERIAL_NUMBER)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
private StuModuleScoreDetailsEntity getOrCreate(String userId) {
|
||||
StuModuleScoreDetailsEntity existing = detail(userId);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
StuModuleScoreDetailsEntity created = new StuModuleScoreDetailsEntity();
|
||||
created.setId(UUID.randomUUID().toString());
|
||||
created.setMoudule(MODULE_NAME);
|
||||
created.setLearningProjects("实验实训");
|
||||
created.setAssessmentItems("三步流程错误次数");
|
||||
created.setScoringCriteria(wrongDeduct.stripTrailingZeros().toPlainString());
|
||||
created.setCompletionStatus("0");
|
||||
created.setScoreProject(0.0);
|
||||
created.setUserId(userId);
|
||||
created.setTotalScore(TOTAL_SCORE);
|
||||
created.setSerialNumber(TRAINING_SERIAL_NUMBER);
|
||||
created.setSchedule(0.0);
|
||||
scoreDetailsMapper.insert(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private double score(int errorCount, double schedule) {
|
||||
BigDecimal base = BigDecimal.valueOf(TOTAL_SCORE)
|
||||
.subtract(wrongDeduct.multiply(BigDecimal.valueOf(Math.max(0, errorCount))))
|
||||
.max(BigDecimal.ZERO);
|
||||
return base.multiply(BigDecimal.valueOf(schedule))
|
||||
.divide(BigDecimal.valueOf(TOTAL_SCORE), 2, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
private double progress(Double schedule) {
|
||||
return schedule == null ? 0.0 : schedule;
|
||||
}
|
||||
|
||||
private int errorCount(String completionStatus) {
|
||||
if (completionStatus == null || completionStatus.trim().isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Math.max(0, Integer.parseInt(completionStatus));
|
||||
} catch (NumberFormatException exception) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class TransactionInformationIdentifierResult {
|
||||
private final long id;
|
||||
private final long verificationId;
|
||||
private final long requestId;
|
||||
private final long identifierApplicationId;
|
||||
private final BigDecimal amount;
|
||||
private final String institutionIdentifier;
|
||||
private final String transactionTimestamp;
|
||||
private final String originalText;
|
||||
private final String digestAlgorithm;
|
||||
private final String digest;
|
||||
private final String transactionIdentifier;
|
||||
private final String status;
|
||||
|
||||
public TransactionInformationIdentifierResult(TransactionInformationIdentifier value) {
|
||||
this.id = value.getId();
|
||||
this.verificationId = value.getVerificationId();
|
||||
this.requestId = value.getRequestId();
|
||||
this.identifierApplicationId = value.getIdentifierApplicationId();
|
||||
this.amount = value.getAmount();
|
||||
this.institutionIdentifier = value.getInstitutionIdentifier();
|
||||
this.transactionTimestamp = value.getTransactionTimestamp();
|
||||
this.originalText = value.getOriginalText();
|
||||
this.digestAlgorithm = value.getDigest() == null ? null : "SM3";
|
||||
this.digest = value.getDigest();
|
||||
this.transactionIdentifier = value.getTransactionIdentifier();
|
||||
this.status = value.getStatus().name();
|
||||
}
|
||||
|
||||
public long getId() { return id; }
|
||||
public long getVerificationId() { return verificationId; }
|
||||
public long getRequestId() { return requestId; }
|
||||
public long getIdentifierApplicationId() { return identifierApplicationId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public String getInstitutionIdentifier() { return institutionIdentifier; }
|
||||
public String getTransactionTimestamp() { return transactionTimestamp; }
|
||||
public String getOriginalText() { return originalText; }
|
||||
public String getDigestAlgorithm() { return digestAlgorithm; }
|
||||
public String getDigest() { return digest; }
|
||||
public String getTransactionIdentifier() { return transactionIdentifier; }
|
||||
public String getStatus() { return status; }
|
||||
}
|
||||
@ -0,0 +1,175 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.InstitutionIdentityCryptography;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifierRepository;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
|
||||
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.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Service
|
||||
public class TransactionInformationIdentifierService {
|
||||
private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
@Resource
|
||||
private CurrencyGenerationVerificationMapper verificationMapper;
|
||||
@Resource
|
||||
private CurrencyGenerationRequestMapper requestMapper;
|
||||
@Resource
|
||||
private InstitutionIdentifierApplicationMapper applicationMapper;
|
||||
@Resource
|
||||
private TransactionInformationIdentifierRepository repository;
|
||||
@Resource
|
||||
private InstitutionIdentityCryptography cryptography;
|
||||
@Resource
|
||||
private InstitutionTrainingScoreService scoreService;
|
||||
|
||||
private final Clock clock = Clock.systemUTC();
|
||||
|
||||
@Transactional
|
||||
public TransactionInformationIdentifierResult prepare(InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationVerificationEntity verification = requireCompletedVerification(subject);
|
||||
CurrencyGenerationRequestEntity request = requireCompletedRequest(verification.getRequestId(), subject);
|
||||
InstitutionIdentifierApplicationEntity application = requireCompletedIdentifier(
|
||||
request.getIdentifierApplicationId(), subject);
|
||||
|
||||
TransactionInformationIdentifier existing = repository.findLatestByVerificationAndSubject(
|
||||
verification.getId(), subject.getUserId(), subject.getSchoolId(), subject.getClassId()).orElse(null);
|
||||
if (existing != null) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"当前步骤三结果已生成模块四记录,不能重复准备");
|
||||
}
|
||||
|
||||
String timestamp = LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC).format(TIMESTAMP);
|
||||
TransactionInformationIdentifier value = invokeResult(() -> TransactionInformationIdentifier.prepare(
|
||||
verification.getId(), request.getId(), application.getId(), request.getAmount(),
|
||||
application.getInstitutionIdentifier(), timestamp));
|
||||
long id = repository.save(value, subject.getUserId(), subject.getSchoolId(), subject.getClassId(), operator);
|
||||
return new TransactionInformationIdentifierResult(TransactionInformationIdentifier.restore(id,
|
||||
value.getVerificationId(), value.getRequestId(), value.getIdentifierApplicationId(),
|
||||
value.getAmount(), value.getInstitutionIdentifier(), value.getTransactionTimestamp(),
|
||||
value.getOriginalText(), null, null, value.getStatus()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TransactionInformationIdentifierResult digest(InstitutionKeySubject subject, String operator) {
|
||||
TransactionInformationIdentifier value = requireStatus(subject,
|
||||
TransactionInformationIdentifier.Status.PREPARED, "请先获取前序数据并准备交易信息标识");
|
||||
TransactionInformationIdentifier.Status expected = value.getStatus();
|
||||
String digest = cryptography.sm3(value.getOriginalText());
|
||||
invoke(() -> value.recordDigest(digest));
|
||||
repository.update(value, expected, "DIGEST", "使用SM3计算交易信息标识摘要", operator);
|
||||
return new TransactionInformationIdentifierResult(value);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public TransactionInformationIdentifierResult assemble(InstitutionKeySubject subject, String operator) {
|
||||
TransactionInformationIdentifier value = requireStatus(subject,
|
||||
TransactionInformationIdentifier.Status.DIGESTED, "请先使用SM3生成摘要");
|
||||
TransactionInformationIdentifier.Status expected = value.getStatus();
|
||||
invoke(value::assemble);
|
||||
repository.update(value, expected, "ASSEMBLE", "将固定前缀TXN_与SM3摘要组装为交易信息标识", operator);
|
||||
scoreService.markStepCompleted(subject.getUserId(), 4);
|
||||
return new TransactionInformationIdentifierResult(value);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public TransactionInformationIdentifierResult detail(InstitutionKeySubject subject) {
|
||||
TransactionInformationIdentifier value = repository.findLatestBySubject(subject.getUserId(),
|
||||
subject.getSchoolId(), subject.getClassId()).orElseThrow(() ->
|
||||
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前没有交易信息标识记录"));
|
||||
return new TransactionInformationIdentifierResult(value);
|
||||
}
|
||||
|
||||
private CurrencyGenerationVerificationEntity requireCompletedVerification(InstitutionKeySubject subject) {
|
||||
CurrencyGenerationVerificationEntity value = verificationMapper.selectOne(
|
||||
new LambdaQueryWrapper<CurrencyGenerationVerificationEntity>()
|
||||
.eq(CurrencyGenerationVerificationEntity::getUserId, subject.getUserId())
|
||||
.eq(CurrencyGenerationVerificationEntity::getSchoolId, subject.getSchoolId())
|
||||
.eq(CurrencyGenerationVerificationEntity::getClassId, subject.getClassId())
|
||||
.eq(CurrencyGenerationVerificationEntity::getStatus, "RESPONSE_RETURNED")
|
||||
.eq(CurrencyGenerationVerificationEntity::getDeleted, false)
|
||||
.orderByDesc(CurrencyGenerationVerificationEntity::getCreatedAt)
|
||||
.last("LIMIT 1"));
|
||||
if (value == null) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"步骤三尚未完成确认报文返回,不能进入模块四");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private CurrencyGenerationRequestEntity requireCompletedRequest(long requestId, InstitutionKeySubject subject) {
|
||||
CurrencyGenerationRequestEntity value = requestMapper.selectOne(
|
||||
new LambdaQueryWrapper<CurrencyGenerationRequestEntity>()
|
||||
.eq(CurrencyGenerationRequestEntity::getId, requestId)
|
||||
.eq(CurrencyGenerationRequestEntity::getUserId, subject.getUserId())
|
||||
.eq(CurrencyGenerationRequestEntity::getSchoolId, subject.getSchoolId())
|
||||
.eq(CurrencyGenerationRequestEntity::getClassId, subject.getClassId())
|
||||
.eq(CurrencyGenerationRequestEntity::getStatus, "RECEIVED")
|
||||
.eq(CurrencyGenerationRequestEntity::getDeleted, false));
|
||||
if (value == null) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"步骤二尚未完成央行接收,不能进入模块四");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private InstitutionIdentifierApplicationEntity requireCompletedIdentifier(long applicationId,
|
||||
InstitutionKeySubject subject) {
|
||||
InstitutionIdentifierApplicationEntity value = applicationMapper.selectOne(
|
||||
new LambdaQueryWrapper<InstitutionIdentifierApplicationEntity>()
|
||||
.eq(InstitutionIdentifierApplicationEntity::getId, applicationId)
|
||||
.eq(InstitutionIdentifierApplicationEntity::getUserId, subject.getUserId())
|
||||
.eq(InstitutionIdentifierApplicationEntity::getSchoolId, subject.getSchoolId())
|
||||
.eq(InstitutionIdentifierApplicationEntity::getClassId, subject.getClassId())
|
||||
.eq(InstitutionIdentifierApplicationEntity::getStatus, "FEEDBACKED")
|
||||
.eq(InstitutionIdentifierApplicationEntity::getDeleted, false));
|
||||
if (value == null) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"步骤一尚未完成机构标识反馈,不能进入模块四");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private TransactionInformationIdentifier requireStatus(InstitutionKeySubject subject,
|
||||
TransactionInformationIdentifier.Status status,
|
||||
String message) {
|
||||
return repository.findLatestBySubjectAndStatus(subject.getUserId(), subject.getSchoolId(),
|
||||
subject.getClassId(), status).orElseThrow(() ->
|
||||
new BusinessException(ErrorCode.VALIDATION_ERROR, message));
|
||||
}
|
||||
|
||||
private static void invoke(Runnable operation) {
|
||||
try {
|
||||
operation.run();
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T invokeResult(Supplier<T> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private interface Supplier<T> {
|
||||
T get();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package com.yau.digitalrmb.institutionidentity.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TransactionInformationIdentifierRepository {
|
||||
long save(TransactionInformationIdentifier value, String userId, long schoolId, long classId, String operator);
|
||||
|
||||
Optional<TransactionInformationIdentifier> findByIdAndSubject(long id, String userId,
|
||||
long schoolId, long classId);
|
||||
|
||||
Optional<TransactionInformationIdentifier> findLatestByVerificationAndSubject(long verificationId,
|
||||
String userId,
|
||||
long schoolId,
|
||||
long classId);
|
||||
|
||||
Optional<TransactionInformationIdentifier> findLatestBySubject(String userId, long schoolId, long classId);
|
||||
|
||||
Optional<TransactionInformationIdentifier> findLatestBySubjectAndStatus(
|
||||
String userId, long schoolId, long classId, TransactionInformationIdentifier.Status status);
|
||||
|
||||
void update(TransactionInformationIdentifier value,
|
||||
TransactionInformationIdentifier.Status expectedStatus,
|
||||
String operation, String detail, String operator);
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
package com.yau.digitalrmb.institutionidentity.infrastructure;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifier;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.TransactionInformationIdentifierRepository;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class MybatisTransactionInformationIdentifierRepository
|
||||
implements TransactionInformationIdentifierRepository {
|
||||
@Resource
|
||||
private TransactionInformationIdentifierMapper mapper;
|
||||
@Resource
|
||||
private TransactionInformationIdentifierOperationLogMapper logMapper;
|
||||
|
||||
@Override
|
||||
public long save(TransactionInformationIdentifier value, String userId,
|
||||
long schoolId, long classId, String operator) {
|
||||
TransactionInformationIdentifierEntity entity = toEntity(value);
|
||||
entity.setUserId(userId);
|
||||
entity.setSchoolId(schoolId);
|
||||
entity.setClassId(classId);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
entity.setCreatedBy(operator);
|
||||
entity.setUpdatedBy(operator);
|
||||
entity.setDeleted(false);
|
||||
mapper.insert(entity);
|
||||
insertLog(entity.getId(), "PREPARE", null, value.getStatus().name(),
|
||||
"从已完成的步骤一至步骤三复制金额和机构标识并生成交易时间戳", operator);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TransactionInformationIdentifier> findByIdAndSubject(long id, String userId,
|
||||
long schoolId, long classId) {
|
||||
return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper<TransactionInformationIdentifierEntity>()
|
||||
.eq(TransactionInformationIdentifierEntity::getId, id)
|
||||
.eq(TransactionInformationIdentifierEntity::getUserId, userId)
|
||||
.eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId)
|
||||
.eq(TransactionInformationIdentifierEntity::getClassId, classId)
|
||||
.eq(TransactionInformationIdentifierEntity::getDeleted, false)))
|
||||
.map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TransactionInformationIdentifier> findLatestByVerificationAndSubject(
|
||||
long verificationId, String userId, long schoolId, long classId) {
|
||||
return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper<TransactionInformationIdentifierEntity>()
|
||||
.eq(TransactionInformationIdentifierEntity::getVerificationId, verificationId)
|
||||
.eq(TransactionInformationIdentifierEntity::getUserId, userId)
|
||||
.eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId)
|
||||
.eq(TransactionInformationIdentifierEntity::getClassId, classId)
|
||||
.eq(TransactionInformationIdentifierEntity::getDeleted, false)
|
||||
.orderByDesc(TransactionInformationIdentifierEntity::getCreatedAt)
|
||||
.last("LIMIT 1")))
|
||||
.map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TransactionInformationIdentifier> findLatestBySubject(String userId, long schoolId, long classId) {
|
||||
return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper<TransactionInformationIdentifierEntity>()
|
||||
.eq(TransactionInformationIdentifierEntity::getUserId, userId)
|
||||
.eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId)
|
||||
.eq(TransactionInformationIdentifierEntity::getClassId, classId)
|
||||
.eq(TransactionInformationIdentifierEntity::getDeleted, false)
|
||||
.orderByDesc(TransactionInformationIdentifierEntity::getCreatedAt)
|
||||
.last("LIMIT 1")))
|
||||
.map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TransactionInformationIdentifier> findLatestBySubjectAndStatus(
|
||||
String userId, long schoolId, long classId, TransactionInformationIdentifier.Status status) {
|
||||
return Optional.ofNullable(mapper.selectOne(new LambdaQueryWrapper<TransactionInformationIdentifierEntity>()
|
||||
.eq(TransactionInformationIdentifierEntity::getUserId, userId)
|
||||
.eq(TransactionInformationIdentifierEntity::getSchoolId, schoolId)
|
||||
.eq(TransactionInformationIdentifierEntity::getClassId, classId)
|
||||
.eq(TransactionInformationIdentifierEntity::getStatus, status.name())
|
||||
.eq(TransactionInformationIdentifierEntity::getDeleted, false)
|
||||
.orderByDesc(TransactionInformationIdentifierEntity::getCreatedAt)
|
||||
.last("LIMIT 1")))
|
||||
.map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(TransactionInformationIdentifier value,
|
||||
TransactionInformationIdentifier.Status expectedStatus,
|
||||
String operation, String detail, String operator) {
|
||||
LambdaUpdateWrapper<TransactionInformationIdentifierEntity> update =
|
||||
new LambdaUpdateWrapper<TransactionInformationIdentifierEntity>()
|
||||
.eq(TransactionInformationIdentifierEntity::getId, value.getId())
|
||||
.eq(TransactionInformationIdentifierEntity::getStatus, expectedStatus.name())
|
||||
.eq(TransactionInformationIdentifierEntity::getDeleted, false)
|
||||
.set(TransactionInformationIdentifierEntity::getDigest, value.getDigest())
|
||||
.set(TransactionInformationIdentifierEntity::getTransactionIdentifier,
|
||||
value.getTransactionIdentifier())
|
||||
.set(TransactionInformationIdentifierEntity::getStatus, value.getStatus().name())
|
||||
.set(TransactionInformationIdentifierEntity::getUpdatedAt, LocalDateTime.now())
|
||||
.set(TransactionInformationIdentifierEntity::getUpdatedBy, operator);
|
||||
if (mapper.update(null, update) != 1) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"交易信息标识状态已变化,不能重复或越级操作");
|
||||
}
|
||||
insertLog(value.getId(), operation, expectedStatus.name(), value.getStatus().name(), detail, operator);
|
||||
}
|
||||
|
||||
private TransactionInformationIdentifierEntity toEntity(TransactionInformationIdentifier value) {
|
||||
TransactionInformationIdentifierEntity entity = new TransactionInformationIdentifierEntity();
|
||||
entity.setId(value.getId());
|
||||
entity.setVerificationId(value.getVerificationId());
|
||||
entity.setRequestId(value.getRequestId());
|
||||
entity.setIdentifierApplicationId(value.getIdentifierApplicationId());
|
||||
entity.setAmount(value.getAmount());
|
||||
entity.setInstitutionIdentifier(value.getInstitutionIdentifier());
|
||||
entity.setTransactionTimestamp(value.getTransactionTimestamp());
|
||||
entity.setOriginalText(value.getOriginalText());
|
||||
entity.setDigest(value.getDigest());
|
||||
entity.setTransactionIdentifier(value.getTransactionIdentifier());
|
||||
entity.setStatus(value.getStatus().name());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private TransactionInformationIdentifier toDomain(TransactionInformationIdentifierEntity entity) {
|
||||
return TransactionInformationIdentifier.restore(entity.getId(), entity.getVerificationId(),
|
||||
entity.getRequestId(), entity.getIdentifierApplicationId(), entity.getAmount(),
|
||||
entity.getInstitutionIdentifier(), entity.getTransactionTimestamp(), entity.getOriginalText(),
|
||||
entity.getDigest(), entity.getTransactionIdentifier(),
|
||||
TransactionInformationIdentifier.Status.valueOf(entity.getStatus()));
|
||||
}
|
||||
|
||||
private void insertLog(Long id, String operation, String fromStatus, String toStatus,
|
||||
String detail, String operator) {
|
||||
TransactionInformationIdentifierOperationLogEntity log =
|
||||
new TransactionInformationIdentifierOperationLogEntity();
|
||||
log.setTransactionIdentifierId(id);
|
||||
log.setOperation(operation);
|
||||
log.setFromStatus(fromStatus);
|
||||
log.setToStatus(toStatus);
|
||||
log.setOperationDetail(detail);
|
||||
log.setCreatedAt(LocalDateTime.now());
|
||||
log.setCreatedBy(operator);
|
||||
logMapper.insert(log);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.yau.digitalrmb.institutionidentity.infrastructure;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("stu_module_score_details")
|
||||
public class StuModuleScoreDetailsEntity {
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
private String moudule;
|
||||
|
||||
@TableField("learning_projects")
|
||||
private String learningProjects;
|
||||
|
||||
@TableField("assessment_items")
|
||||
private String assessmentItems;
|
||||
|
||||
@TableField("scoring_criteria")
|
||||
private String scoringCriteria;
|
||||
|
||||
@TableField("completion_status")
|
||||
private String completionStatus;
|
||||
|
||||
@TableField("score_project")
|
||||
private Double scoreProject;
|
||||
|
||||
@TableField("user_id")
|
||||
private String userId;
|
||||
|
||||
@TableField("total_score")
|
||||
private Double totalScore;
|
||||
|
||||
@TableField("serial_number")
|
||||
private Integer serialNumber;
|
||||
|
||||
private Double schedule;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.yau.digitalrmb.institutionidentity.infrastructure;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface StuModuleScoreDetailsMapper extends BaseMapper<StuModuleScoreDetailsEntity> {
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.yau.digitalrmb.institutionidentity.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("transaction_information_identifier")
|
||||
public class TransactionInformationIdentifierEntity extends AuditableEntity {
|
||||
@TableField("verification_id") private Long verificationId;
|
||||
@TableField("request_id") private Long requestId;
|
||||
@TableField("identifier_application_id") private Long identifierApplicationId;
|
||||
private BigDecimal amount;
|
||||
@TableField("institution_identifier") private String institutionIdentifier;
|
||||
@TableField("transaction_timestamp") private String transactionTimestamp;
|
||||
@TableField("original_text") private String originalText;
|
||||
private String digest;
|
||||
@TableField("transaction_identifier") private String transactionIdentifier;
|
||||
private String status;
|
||||
@TableField("user_id") private String userId;
|
||||
@TableField("school_id") private Long schoolId;
|
||||
@TableField("class_id") private Long classId;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package com.yau.digitalrmb.institutionidentity.infrastructure;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface TransactionInformationIdentifierMapper extends BaseMapper<TransactionInformationIdentifierEntity> {
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package com.yau.digitalrmb.institutionidentity.infrastructure;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("transaction_information_identifier_operation_log")
|
||||
public class TransactionInformationIdentifierOperationLogEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
@TableField("transaction_identifier_id") private Long transactionIdentifierId;
|
||||
private String operation;
|
||||
@TableField("from_status") private String fromStatus;
|
||||
@TableField("to_status") private String toStatus;
|
||||
@TableField("operation_detail") private String operationDetail;
|
||||
@TableField("created_at") private LocalDateTime createdAt;
|
||||
@TableField("created_by") private String createdBy;
|
||||
@TableLogic
|
||||
private Boolean deleted = false;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.yau.digitalrmb.institutionidentity.infrastructure;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface TransactionInformationIdentifierOperationLogMapper
|
||||
extends BaseMapper<TransactionInformationIdentifierOperationLogEntity> {
|
||||
}
|
||||
@ -1,11 +1,8 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
public class ConfirmInstitutionIdentifierRequest {
|
||||
@NotBlank(message = "签名密钥不能为空")
|
||||
private String signingKeyId;
|
||||
private String privateKey;
|
||||
|
||||
public String getSigningKeyId() { return signingKeyId; }
|
||||
public void setSigningKeyId(String signingKeyId) { this.signingKeyId = signingKeyId; }
|
||||
public String getPrivateKey() { return privateKey; }
|
||||
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
|
||||
}
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
public class InstitutionIdentifierStepRequest {
|
||||
@Size(max = 64, message = "密钥标识长度不能超过64个字符")
|
||||
private String keyId;
|
||||
private String privateKey;
|
||||
|
||||
public String getKeyId() { return keyId; }
|
||||
public void setKeyId(String keyId) { this.keyId = keyId; }
|
||||
public String getPrivateKey() { return privateKey; }
|
||||
public void setPrivateKey(String privateKey) { this.privateKey = privateKey; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package com.yau.digitalrmb.platformintegration.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "platform-integration", name = "enabled", havingValue = "true")
|
||||
@EnableConfigurationProperties(PlatformIntegrationProperties.class)
|
||||
public class PlatformIntegrationConfig {
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
-- 为中国人民银行生成数字货币实验的过程记录表补齐逻辑删除字段。
|
||||
-- 已存在的历史记录默认视为未删除。
|
||||
|
||||
-- 本脚本为一次性升级脚本,请勿重复执行。
|
||||
ALTER TABLE institution_identifier_operation_log
|
||||
ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD INDEX idx_identifier_operation_application_deleted (application_id, deleted);
|
||||
|
||||
ALTER TABLE institution_identifier_training_error
|
||||
ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD INDEX idx_identifier_error_application_deleted (application_id, deleted);
|
||||
|
||||
ALTER TABLE institution_sm2_key_audit
|
||||
ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD INDEX idx_institution_key_audit_record_deleted (key_record_id, deleted);
|
||||
|
||||
ALTER TABLE currency_generation_request_operation_log
|
||||
ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD INDEX idx_currency_request_operation_deleted (request_id, deleted);
|
||||
|
||||
ALTER TABLE currency_generation_verification_operation_log
|
||||
ADD COLUMN deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||||
ADD INDEX idx_currency_verification_operation_deleted (verification_id, deleted);
|
||||
@ -0,0 +1,49 @@
|
||||
-- 模块四“生成交易信息标识”一次性 MySQL 升级脚本。
|
||||
-- 适用于已经存在步骤一至步骤三业务表的数据库;执行前请先备份。
|
||||
|
||||
CREATE TABLE transaction_information_identifier (
|
||||
id BIGINT PRIMARY KEY,
|
||||
verification_id BIGINT NOT NULL,
|
||||
request_id BIGINT NOT NULL,
|
||||
identifier_application_id BIGINT NOT NULL,
|
||||
amount DECIMAL(18, 2) NOT NULL,
|
||||
institution_identifier VARCHAR(32) NOT NULL,
|
||||
transaction_timestamp CHAR(14) NOT NULL,
|
||||
original_text VARCHAR(256) NOT NULL,
|
||||
digest CHAR(64),
|
||||
transaction_identifier VARCHAR(80),
|
||||
status VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
school_id BIGINT NOT NULL,
|
||||
class_id BIGINT NOT NULL,
|
||||
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,
|
||||
CONSTRAINT uk_transaction_identifier_verification UNIQUE
|
||||
(verification_id, user_id, school_id, class_id),
|
||||
CONSTRAINT fk_transaction_identifier_verification FOREIGN KEY (verification_id)
|
||||
REFERENCES currency_generation_verification(id),
|
||||
CONSTRAINT fk_transaction_identifier_request FOREIGN KEY (request_id)
|
||||
REFERENCES currency_generation_request(id),
|
||||
CONSTRAINT fk_transaction_identifier_application FOREIGN KEY (identifier_application_id)
|
||||
REFERENCES institution_identifier_application(id),
|
||||
INDEX idx_transaction_identifier_subject
|
||||
(user_id, school_id, class_id, status, deleted)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块四交易信息标识';
|
||||
|
||||
CREATE TABLE transaction_information_identifier_operation_log (
|
||||
id BIGINT PRIMARY KEY,
|
||||
transaction_identifier_id BIGINT NOT NULL,
|
||||
operation VARCHAR(32) NOT NULL,
|
||||
from_status VARCHAR(32),
|
||||
to_status VARCHAR(32) NOT NULL,
|
||||
operation_detail VARCHAR(1024) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
CONSTRAINT fk_transaction_identifier_operation FOREIGN KEY (transaction_identifier_id)
|
||||
REFERENCES transaction_information_identifier(id),
|
||||
INDEX idx_transaction_identifier_operation (transaction_identifier_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模块四交易信息标识操作日志';
|
||||
@ -0,0 +1,266 @@
|
||||
package com.yau.digitalrmb.institutionidentity;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequest;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerification;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationRequestMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.CurrencyGenerationVerificationMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.StuModuleScoreDetailsMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierOperationLogEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.TransactionInformationIdentifierOperationLogMapper;
|
||||
import com.yau.digitalrmb.security.application.JwtTokenService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class TransactionInformationIdentifierControllerTest {
|
||||
private static final String BASE_PATH =
|
||||
"/api/v1/institution-identifiers/transaction-information-identifiers";
|
||||
private static final String USER_ID = "00000000-0000-0000-0000-000000009004";
|
||||
private static final long SCHOOL_ID = 1001L;
|
||||
private static final long CLASS_ID = 2001L;
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
@Autowired private JwtTokenService jwtTokenService;
|
||||
@Autowired private JdbcTemplate jdbcTemplate;
|
||||
@Autowired private InstitutionIdentifierApplicationMapper applicationMapper;
|
||||
@Autowired private CurrencyGenerationRequestMapper requestMapper;
|
||||
@Autowired private CurrencyGenerationVerificationMapper verificationMapper;
|
||||
@Autowired private TransactionInformationIdentifierMapper transactionMapper;
|
||||
@Autowired private TransactionInformationIdentifierOperationLogMapper transactionLogMapper;
|
||||
@Autowired private StuModuleScoreDetailsMapper scoreMapper;
|
||||
|
||||
private String authorization;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
jdbcTemplate.update("DELETE FROM transaction_information_identifier_operation_log");
|
||||
jdbcTemplate.update("DELETE FROM transaction_information_identifier");
|
||||
jdbcTemplate.update("DELETE FROM currency_generation_verification_operation_log");
|
||||
jdbcTemplate.update("DELETE FROM currency_generation_verification");
|
||||
jdbcTemplate.update("DELETE FROM currency_generation_request_operation_log");
|
||||
jdbcTemplate.update("DELETE FROM currency_generation_request");
|
||||
jdbcTemplate.update("DELETE FROM institution_identifier_training_error");
|
||||
jdbcTemplate.update("DELETE FROM institution_identifier_operation_log");
|
||||
jdbcTemplate.update("DELETE FROM institution_identifier_application");
|
||||
jdbcTemplate.update("DELETE FROM institution_sm2_key_audit");
|
||||
jdbcTemplate.update("DELETE FROM institution_sm2_key");
|
||||
jdbcTemplate.update("DELETE FROM stu_module_score_details");
|
||||
authorization = "Bearer " + jwtTokenService.issueFor(USER_ID, SCHOOL_ID, CLASS_ID,
|
||||
"module-four-tester", Collections.singleton("STUDENT")).accessToken();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsModuleFourWhenStepThreeHasNotReturnedResponse() throws Exception {
|
||||
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
|
||||
"步骤三尚未完成确认报文返回")));
|
||||
assertThat(transactionMapper.selectList(null)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void flowsCompletedPreviousDataAndPersistsEveryModuleFourStep() throws Exception {
|
||||
PrecedingData preceding = insertCompletedPrecedingData();
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("PREPARED"))
|
||||
.andExpect(jsonPath("$.data.verificationId").value(preceding.verificationId))
|
||||
.andExpect(jsonPath("$.data.requestId").value(preceding.requestId))
|
||||
.andExpect(jsonPath("$.data.identifierApplicationId").value(preceding.applicationId))
|
||||
.andExpect(jsonPath("$.data.amount").value(1589.85))
|
||||
.andExpect(jsonPath("$.data.institutionIdentifier").value("ORG_3A4B5C6D7E8F"))
|
||||
.andExpect(jsonPath("$.data.transactionTimestamp")
|
||||
.value(org.hamcrest.Matchers.matchesPattern("\\d{14}")))
|
||||
.andExpect(jsonPath("$.data.originalText")
|
||||
.value(org.hamcrest.Matchers.matchesPattern(
|
||||
"1589\\.85\\|ORG_3A4B5C6D7E8F\\|\\d{14}")))
|
||||
.andExpect(jsonPath("$.data.digest").value(org.hamcrest.Matchers.nullValue()))
|
||||
.andExpect(jsonPath("$.data.transactionIdentifier")
|
||||
.value(org.hamcrest.Matchers.nullValue()));
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
|
||||
"请先使用SM3生成摘要")));
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/digest").header("Authorization", authorization))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("DIGESTED"))
|
||||
.andExpect(jsonPath("$.data.digestAlgorithm").value("SM3"))
|
||||
.andExpect(jsonPath("$.data.digest")
|
||||
.value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/assemble").header("Authorization", authorization))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("ASSEMBLED"))
|
||||
.andExpect(jsonPath("$.data.transactionIdentifier")
|
||||
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")));
|
||||
|
||||
mvc.perform(get(BASE_PATH).header("Authorization", authorization))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("ASSEMBLED"))
|
||||
.andExpect(jsonPath("$.data.transactionIdentifier")
|
||||
.value(org.hamcrest.Matchers.matchesPattern("TXN_[0-9A-F]{64}")));
|
||||
|
||||
List<TransactionInformationIdentifierEntity> records = transactionMapper.selectList(null);
|
||||
assertThat(records).hasSize(1);
|
||||
TransactionInformationIdentifierEntity persisted = records.get(0);
|
||||
assertThat(persisted.getVerificationId()).isEqualTo(preceding.verificationId);
|
||||
assertThat(persisted.getRequestId()).isEqualTo(preceding.requestId);
|
||||
assertThat(persisted.getIdentifierApplicationId()).isEqualTo(preceding.applicationId);
|
||||
assertThat(persisted.getAmount()).isEqualByComparingTo("1589.85");
|
||||
assertThat(persisted.getInstitutionIdentifier()).isEqualTo("ORG_3A4B5C6D7E8F");
|
||||
assertThat(persisted.getOriginalText()).isEqualTo("1589.85|ORG_3A4B5C6D7E8F|"
|
||||
+ persisted.getTransactionTimestamp());
|
||||
assertThat(persisted.getDigest()).matches("[0-9A-F]{64}");
|
||||
assertThat(persisted.getTransactionIdentifier()).isEqualTo("TXN_" + persisted.getDigest());
|
||||
assertThat(persisted.getStatus()).isEqualTo("ASSEMBLED");
|
||||
|
||||
assertThat(transactionLogMapper.selectList(null))
|
||||
.extracting(TransactionInformationIdentifierOperationLogEntity::getOperation)
|
||||
.containsExactly("PREPARE", "DIGEST", "ASSEMBLE");
|
||||
|
||||
StuModuleScoreDetailsEntity score = scoreMapper.selectOne(
|
||||
new LambdaQueryWrapper<StuModuleScoreDetailsEntity>()
|
||||
.eq(StuModuleScoreDetailsEntity::getUserId, USER_ID)
|
||||
.eq(StuModuleScoreDetailsEntity::getSerialNumber, 3));
|
||||
assertThat(score).isNotNull();
|
||||
assertThat(score.getSchedule()).isEqualTo(44.44);
|
||||
assertThat(score.getScoreProject()).isEqualTo(44.44);
|
||||
}
|
||||
|
||||
@Test
|
||||
void preventsDuplicatePreparationForSameCompletedVerification() throws Exception {
|
||||
insertCompletedPrecedingData();
|
||||
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
|
||||
.andExpect(status().isOk());
|
||||
mvc.perform(post(BASE_PATH + "/steps/prepare").header("Authorization", authorization))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString(
|
||||
"不能重复准备")));
|
||||
assertThat(transactionMapper.selectList(null)).hasSize(1);
|
||||
}
|
||||
|
||||
private PrecedingData insertCompletedPrecedingData() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InstitutionIdentifierApplicationEntity application = new InstitutionIdentifierApplicationEntity();
|
||||
application.setBankCode("BKCHCNBJ40001");
|
||||
application.setUserId(USER_ID);
|
||||
application.setSchoolId(SCHOOL_ID);
|
||||
application.setClassId(CLASS_ID);
|
||||
application.setRequestTimestamp("20260805180000");
|
||||
application.setDigest("3A4B5C6D7E8F0000000000000000000000000000000000000000000000000000");
|
||||
application.setBankSignature("3044AABB");
|
||||
application.setStatus("FEEDBACKED");
|
||||
application.setVerificationKeyId(InstitutionKeyService.BANK_SECOND_KEY);
|
||||
application.setSignatureValid(true);
|
||||
application.setDigestMatches(true);
|
||||
application.setSigningKeyId(InstitutionKeyService.CENTRAL_FIRST_KEY);
|
||||
application.setInstitutionIdentifier("ORG_3A4B5C6D7E8F");
|
||||
application.setCentralBankSignature("3044CCDD");
|
||||
application.setTrainingRound(1);
|
||||
application.setScoringCriteria(0);
|
||||
application.setCreatedAt(now);
|
||||
application.setUpdatedAt(now);
|
||||
application.setCreatedBy("module-four-tester");
|
||||
application.setUpdatedBy("module-four-tester");
|
||||
application.setDeleted(false);
|
||||
applicationMapper.insert(application);
|
||||
|
||||
CurrencyGenerationRequestEntity request = new CurrencyGenerationRequestEntity();
|
||||
request.setIdentifierApplicationId(application.getId());
|
||||
request.setInstitutionIdentifier(application.getInstitutionIdentifier());
|
||||
request.setFullInstitutionIdentifier(application.getDigest());
|
||||
request.setAmount(new BigDecimal("1589.85"));
|
||||
request.setDeliveryNodeCode("SYS_DC_001");
|
||||
request.setRequestTimestamp("20260805180100");
|
||||
request.setRequestOriginalText("ORG_3A4B5C6D7E8F|1589.85|SYS_DC_001|20260805180100");
|
||||
request.setRequestDigest("7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D5E6F");
|
||||
request.setBankSignature("3044AABB");
|
||||
request.setSigningKeyId(InstitutionKeyService.BANK_SECOND_KEY);
|
||||
request.setRequestMessage("{request:true}");
|
||||
request.setStatus(CurrencyGenerationRequest.Status.RECEIVED.name());
|
||||
request.setUserId(USER_ID);
|
||||
request.setSchoolId(SCHOOL_ID);
|
||||
request.setClassId(CLASS_ID);
|
||||
request.setCreatedAt(now);
|
||||
request.setUpdatedAt(now);
|
||||
request.setCreatedBy("module-four-tester");
|
||||
request.setUpdatedBy("module-four-tester");
|
||||
request.setDeleted(false);
|
||||
requestMapper.insert(request);
|
||||
|
||||
CurrencyGenerationVerificationEntity verification = new CurrencyGenerationVerificationEntity();
|
||||
verification.setRequestId(request.getId());
|
||||
verification.setRequestMessage(request.getRequestMessage());
|
||||
verification.setRequestOriginalText(request.getRequestOriginalText());
|
||||
verification.setRequestDigest(request.getRequestDigest());
|
||||
verification.setBankSignature(request.getBankSignature());
|
||||
verification.setBankKeyId(request.getSigningKeyId());
|
||||
verification.setAmount(request.getAmount());
|
||||
verification.setSignatureValid(true);
|
||||
verification.setRecomputedDigest(request.getRequestDigest());
|
||||
verification.setDigestMatches(true);
|
||||
verification.setTotalQuota(new BigDecimal("500000000.00"));
|
||||
verification.setUsedQuota(new BigDecimal("495000000.00"));
|
||||
verification.setRemainingQuota(new BigDecimal("5000000.00"));
|
||||
verification.setQuotaSufficient(true);
|
||||
verification.setCentralBankKeyId(InstitutionKeyService.CENTRAL_FIRST_KEY);
|
||||
verification.setCentralBankSignature("3044CCDD");
|
||||
verification.setConfirmationTime("20260805180200");
|
||||
verification.setConfirmationMessage("{confirmed:true}");
|
||||
verification.setStatus(CurrencyGenerationVerification.Status.RESPONSE_RETURNED.name());
|
||||
verification.setUserId(USER_ID);
|
||||
verification.setSchoolId(SCHOOL_ID);
|
||||
verification.setClassId(CLASS_ID);
|
||||
verification.setCreatedAt(now);
|
||||
verification.setUpdatedAt(now);
|
||||
verification.setCreatedBy("module-four-tester");
|
||||
verification.setUpdatedBy("module-four-tester");
|
||||
verification.setDeleted(false);
|
||||
verificationMapper.insert(verification);
|
||||
return new PrecedingData(application.getId(), request.getId(), verification.getId());
|
||||
}
|
||||
|
||||
private static class PrecedingData {
|
||||
private final long applicationId;
|
||||
private final long requestId;
|
||||
private final long verificationId;
|
||||
|
||||
private PrecedingData(long applicationId, long requestId, long verificationId) {
|
||||
this.applicationId = applicationId;
|
||||
this.requestId = requestId;
|
||||
this.verificationId = verificationId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.yau.digitalrmb.platformintegration.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformIntegrationConfigTest {
|
||||
private final ApplicationContextRunner contextRunner =
|
||||
new ApplicationContextRunner().withUserConfiguration(PlatformIntegrationConfig.class);
|
||||
|
||||
@Test
|
||||
void doesNotBindOrValidatePropertiesWhenIntegrationIsDisabled() {
|
||||
contextRunner
|
||||
.withPropertyValues("platform-integration.enabled=false")
|
||||
.run(context -> {
|
||||
assertThat(context).hasNotFailed();
|
||||
assertThat(context).doesNotHaveBean(PlatformIntegrationProperties.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesRequiredPropertiesWhenIntegrationIsEnabled() {
|
||||
contextRunner
|
||||
.withPropertyValues("platform-integration.enabled=true")
|
||||
.run(context -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure()).hasMessageContaining("platform-integration");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package com.yau.digitalrmb.shared;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@TestPropertySource(properties = {
|
||||
"springdoc.swagger-ui.enabled=true",
|
||||
"knife4j.enable=true"
|
||||
})
|
||||
class SwaggerDocumentationTest {
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void docHtmlAndOpenApiJsonArePubliclyAvailable() throws Exception {
|
||||
mvc.perform(get("/doc.html"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith("text/html"))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("knife4j")));
|
||||
|
||||
mvc.perform(get("/v3/api-docs"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith("application/json"))
|
||||
.andExpect(jsonPath("$.info.title").value("数字人民币教学仿真后端"))
|
||||
.andExpect(jsonPath("$.components.securitySchemes.Authorization.type").value("apiKey"))
|
||||
.andExpect(jsonPath("$.components.securitySchemes.Authorization.in").value("header"))
|
||||
.andExpect(jsonPath("$.components.securitySchemes.Authorization.name").value("Authorization"))
|
||||
.andExpect(jsonPath("$.security[0].Authorization").isArray());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue