Merge branch 'master' of http://118.31.7.2:3000/chenyuan1/digital-rmb-backend
# Conflicts: # src/main/java/com/yau/digitalrmb/DigitalRmbApplication.java # src/main/java/com/yau/digitalrmb/platformintegration/application/PlatformTokenVerifier.java # src/main/java/com/yau/digitalrmb/security/interfaces/AuthController.java # src/main/resources/schema.sqlmaster
commit
07b41797fe
@ -0,0 +1,30 @@
|
||||
package com.yau.digitalrmb.identity.domain;
|
||||
|
||||
public enum UserRole {
|
||||
ADMIN(1),
|
||||
TEACHER(3),
|
||||
STUDENT(4);
|
||||
|
||||
private final int id;
|
||||
|
||||
UserRole(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getAuthority() {
|
||||
return name();
|
||||
}
|
||||
|
||||
public static UserRole fromId(int id) {
|
||||
for (UserRole role : values()) {
|
||||
if (role.id == id) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported user role id: " + id);
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
package com.yau.digitalrmb.identity.infrastructure.persistence.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("sys_role")
|
||||
public class RoleEntity extends AuditableEntity {
|
||||
private String name;
|
||||
}
|
||||
@ -1,19 +1,63 @@
|
||||
package com.yau.digitalrmb.identity.infrastructure.persistence.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.yau.digitalrmb.shared.infrastructure.persistence.AuditableEntity;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("sys_user")
|
||||
public class UserEntity extends AuditableEntity {
|
||||
private String username;
|
||||
public class UserEntity {
|
||||
@TableId(value = "user_id", type = IdType.INPUT)
|
||||
private String userId = UUID.randomUUID().toString();
|
||||
|
||||
@TableField("student_id")
|
||||
private String studentId;
|
||||
|
||||
@TableField("password")
|
||||
private String password;
|
||||
|
||||
@TableField("user_name")
|
||||
private String userName;
|
||||
|
||||
@TableField("class_id")
|
||||
private String classId;
|
||||
|
||||
@TableField("class_name")
|
||||
private String className;
|
||||
|
||||
private String phone;
|
||||
|
||||
@TableField("school_name")
|
||||
private String schoolName;
|
||||
|
||||
@TableField("school_id")
|
||||
private String schoolId;
|
||||
|
||||
@TableField("authorize_time")
|
||||
private LocalDate authorizeTime;
|
||||
|
||||
@TableField("authorize_end_time")
|
||||
private LocalDate authorizeEndTime;
|
||||
|
||||
@TableField("role_id")
|
||||
private Integer roleId;
|
||||
|
||||
@TableField("is_deleted")
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
private Boolean isDeleted;
|
||||
|
||||
@TableField("password_hash")
|
||||
private String passwordHash;
|
||||
@TableField("zy_user_id")
|
||||
private String zyUserId;
|
||||
|
||||
private Boolean enabled;
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
package com.yau.digitalrmb.identity.infrastructure.persistence.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.RoleEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface RoleMapper extends BaseMapper<RoleEntity> {
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequest;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequestRepository;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
||||
import com.yau.digitalrmb.institutionidentity.application.IssuedInstitutionIdentifierQueryService;
|
||||
import com.yau.digitalrmb.institutionidentity.application.IssuedInstitutionIdentifierResult;
|
||||
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.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Service
|
||||
public class CurrencyGenerationRequestService {
|
||||
private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
@Resource
|
||||
private CurrencyGenerationRequestRepository repository;
|
||||
@Resource
|
||||
private IssuedInstitutionIdentifierQueryService identifierQueryService;
|
||||
@Resource
|
||||
private InstitutionIdentityCryptography cryptography;
|
||||
@Resource
|
||||
private InstitutionKeyService keyService;
|
||||
|
||||
private final Clock clock = Clock.systemUTC();
|
||||
|
||||
@Transactional
|
||||
public CurrencyRequestResult prepare(long identifierApplicationId, BigDecimal amount, String deliveryNodeCode,
|
||||
InstitutionKeySubject subject, String operator) {
|
||||
IssuedInstitutionIdentifierResult identifier = identifierQueryService.requireIssued(identifierApplicationId,
|
||||
subject);
|
||||
String timestamp = LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC).format(TIMESTAMP);
|
||||
CurrencyGenerationRequest request = invokeDomainWithResult(() -> CurrencyGenerationRequest.prepare(
|
||||
identifierApplicationId, identifier.getInstitutionIdentifier(),
|
||||
identifier.getFullInstitutionIdentifier(), amount, deliveryNodeCode, timestamp));
|
||||
long requestId = repository.save(request, subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
|
||||
operator);
|
||||
CurrencyGenerationRequest persisted = CurrencyGenerationRequest.restore(requestId,
|
||||
request.getIdentifierApplicationId(), request.getInstitutionIdentifier(),
|
||||
request.getFullInstitutionIdentifier(), request.getAmount(), request.getDeliveryNodeCode(),
|
||||
request.getRequestTimestamp(), null, null, null, null, null, request.getStatus());
|
||||
return new CurrencyRequestResult(persisted);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyRequestResult concatenate(long requestId, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationRequest request = requireRequest(requestId, subject);
|
||||
CurrencyGenerationRequest.Status expected = request.getStatus();
|
||||
invokeDomain(request::concatenate);
|
||||
repository.update(request, expected, "CONCATENATE", "拼接机构标识、金额、投放节点和时间戳",
|
||||
operator);
|
||||
return new CurrencyRequestResult(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyRequestResult digest(long requestId, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationRequest request = requireRequest(requestId, subject);
|
||||
CurrencyGenerationRequest.Status expected = request.getStatus();
|
||||
String digest = cryptography.sm3(requireOriginalText(request));
|
||||
invokeDomain(() -> request.recordDigest(digest));
|
||||
repository.update(request, expected, "DIGEST", "使用SM3计算并保存请求摘要", operator);
|
||||
return new CurrencyRequestResult(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyRequestResult sign(long requestId, String keyId, InstitutionKeySubject subject, String operator) {
|
||||
if (!InstitutionKeyService.BANK_SECOND_KEY.equals(keyId)) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "必须选择商业银行第二私钥完成请求签名");
|
||||
}
|
||||
CurrencyGenerationRequest request = requireRequest(requestId, subject);
|
||||
CurrencyGenerationRequest.Status expected = request.getStatus();
|
||||
String signature = keyService.signCommercialBank(subject, requireDigest(request));
|
||||
invokeDomain(() -> request.recordSignature(keyId, signature));
|
||||
repository.update(request, expected, "SIGN", "使用商业银行第二私钥完成SM2签名", operator);
|
||||
return new CurrencyRequestResult(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyRequestResult packageMessage(long requestId, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationRequest request = requireRequest(requestId, subject);
|
||||
CurrencyGenerationRequest.Status expected = request.getStatus();
|
||||
String message = "{\"originalText\":\"" + request.getRequestOriginalText()
|
||||
+ "\",\"digest\":\"" + request.getRequestDigest()
|
||||
+ "\",\"signature\":\"" + request.getBankSignature()
|
||||
+ "\",\"keyId\":\"" + request.getSigningKeyId() + "\"}";
|
||||
invokeDomain(() -> request.packageMessage(message));
|
||||
repository.update(request, expected, "PACKAGE", "组装包含原文、SM3摘要和SM2签名的标准请求报文", operator);
|
||||
return new CurrencyRequestResult(request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyRequestResult send(long requestId, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationRequest request = requireRequest(requestId, subject);
|
||||
CurrencyGenerationRequest.Status expected = request.getStatus();
|
||||
invokeDomain(request::send);
|
||||
repository.update(request, expected, "SEND", "商业银行发送货币生成请求至央行", operator);
|
||||
|
||||
expected = request.getStatus();
|
||||
invokeDomain(request::receive);
|
||||
repository.update(request, expected, "RECEIVE", "央行成功接收货币生成请求", operator);
|
||||
return new CurrencyRequestResult(request);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CurrencyRequestResult detail(long requestId, InstitutionKeySubject subject) {
|
||||
return new CurrencyRequestResult(requireRequest(requestId, subject));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CurrencyGenerationRequest requireReceived(long requestId, InstitutionKeySubject subject) {
|
||||
CurrencyGenerationRequest request = requireRequest(requestId, subject);
|
||||
if (request.getStatus() != CurrencyGenerationRequest.Status.RECEIVED) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "步骤二尚未完成央行接收,不能进入步骤三");
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private CurrencyGenerationRequest requireRequest(long requestId, InstitutionKeySubject subject) {
|
||||
return repository.findByIdAndSubject(requestId, subject.getUserId(), subject.getSchoolId(),
|
||||
subject.getClassId()).orElseThrow(() ->
|
||||
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "当前实训主体下的货币生成请求不存在"));
|
||||
}
|
||||
|
||||
private static String requireOriginalText(CurrencyGenerationRequest request) {
|
||||
if (request.getRequestOriginalText() == null) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成请求原文拼接");
|
||||
}
|
||||
return request.getRequestOriginalText();
|
||||
}
|
||||
|
||||
private static String requireDigest(CurrencyGenerationRequest request) {
|
||||
if (request.getRequestDigest() == null) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请先完成SM3摘要计算");
|
||||
}
|
||||
return request.getRequestDigest();
|
||||
}
|
||||
|
||||
private static void invokeDomain(Runnable operation) {
|
||||
try {
|
||||
operation.run();
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T invokeDomainWithResult(DomainSupplier<T> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private interface DomainSupplier<T> {
|
||||
T get();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerification;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CurrencyGenerationVerificationResult {
|
||||
private final Long verificationId;
|
||||
private final long requestId;
|
||||
private final String status;
|
||||
private final String requestMessage;
|
||||
private final String requestOriginalText;
|
||||
private final String requestDigest;
|
||||
private final String bankSignature;
|
||||
private final String bankKeyId;
|
||||
private final BigDecimal amount;
|
||||
private final Boolean signatureValid;
|
||||
private final String recomputedDigest;
|
||||
private final Boolean digestMatches;
|
||||
private final BigDecimal totalQuota;
|
||||
private final BigDecimal usedQuota;
|
||||
private final BigDecimal remainingQuota;
|
||||
private final Boolean quotaSufficient;
|
||||
private final String centralBankKeyId;
|
||||
private final String centralBankSignature;
|
||||
private final String confirmationTime;
|
||||
private final String confirmationMessage;
|
||||
|
||||
public CurrencyGenerationVerificationResult(CurrencyGenerationVerification v) {
|
||||
verificationId = v.getId(); requestId = v.getRequestId(); status = v.getStatus().name();
|
||||
requestMessage = v.getRequestMessage(); requestOriginalText = v.getRequestOriginalText();
|
||||
requestDigest = v.getRequestDigest(); bankSignature = v.getBankSignature(); bankKeyId = v.getBankKeyId();
|
||||
amount = v.getAmount(); signatureValid = v.getSignatureValid(); recomputedDigest = v.getRecomputedDigest();
|
||||
digestMatches = v.getDigestMatches(); totalQuota = v.getTotalQuota(); usedQuota = v.getUsedQuota();
|
||||
remainingQuota = v.getRemainingQuota(); quotaSufficient = v.getQuotaSufficient();
|
||||
centralBankKeyId = v.getCentralBankKeyId(); centralBankSignature = v.getCentralBankSignature();
|
||||
confirmationTime = v.getConfirmationTime(); confirmationMessage = v.getConfirmationMessage();
|
||||
}
|
||||
|
||||
public Long getVerificationId() { return verificationId; }
|
||||
public long getRequestId() { return requestId; }
|
||||
public String getStatus() { return status; }
|
||||
public String getRequestMessage() { return requestMessage; }
|
||||
public String getRequestOriginalText() { return requestOriginalText; }
|
||||
public String getRequestDigest() { return requestDigest; }
|
||||
public String getBankSignature() { return bankSignature; }
|
||||
public String getBankKeyId() { return bankKeyId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public Boolean getSignatureValid() { return signatureValid; }
|
||||
public String getRecomputedDigest() { return recomputedDigest; }
|
||||
public Boolean getDigestMatches() { return digestMatches; }
|
||||
public BigDecimal getTotalQuota() { return totalQuota; }
|
||||
public BigDecimal getUsedQuota() { return usedQuota; }
|
||||
public BigDecimal getRemainingQuota() { return remainingQuota; }
|
||||
public Boolean getQuotaSufficient() { return quotaSufficient; }
|
||||
public String getCentralBankKeyId() { return centralBankKeyId; }
|
||||
public String getCentralBankSignature() { return centralBankSignature; }
|
||||
public String getConfirmationTime() { return confirmationTime; }
|
||||
public String getConfirmationMessage() { return confirmationMessage; }
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequest;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerification;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerificationRepository;
|
||||
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;
|
||||
|
||||
@Service
|
||||
public class CurrencyGenerationVerificationService {
|
||||
private static final BigDecimal TOTAL_QUOTA = new BigDecimal("500000000.00");
|
||||
private static final BigDecimal USED_QUOTA = new BigDecimal("495000000.00");
|
||||
private static final BigDecimal REMAINING_QUOTA = new BigDecimal("5000000.00");
|
||||
private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
@Resource private CurrencyGenerationVerificationRepository repository;
|
||||
@Resource private CurrencyGenerationRequestService requestService;
|
||||
@Resource private InstitutionIdentityCryptography cryptography;
|
||||
@Resource private InstitutionKeyService keyService;
|
||||
|
||||
@Transactional
|
||||
public CurrencyGenerationVerificationResult receive(long requestId, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationRequest request = requestService.requireReceived(requestId, subject);
|
||||
CurrencyGenerationVerification value = invokeResult(() -> CurrencyGenerationVerification.receive(requestId,
|
||||
request.getRequestMessage(), request.getRequestOriginalText(), request.getRequestDigest(),
|
||||
request.getBankSignature(), request.getSigningKeyId(), request.getAmount()));
|
||||
long id = repository.save(value, subject.getUserId(), subject.getSchoolId(), subject.getClassId(), operator);
|
||||
return new CurrencyGenerationVerificationResult(CurrencyGenerationVerification.restore(id, requestId,
|
||||
value.getRequestMessage(), value.getRequestOriginalText(), value.getRequestDigest(),
|
||||
value.getBankSignature(), value.getBankKeyId(), value.getAmount(), null, null, null,
|
||||
null, null, null, null, null, null, null, null, value.getStatus()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyGenerationVerificationResult verifySignature(long id, String keyId,
|
||||
InstitutionKeySubject subject, String operator) {
|
||||
if (!InstitutionKeyService.BANK_SECOND_KEY.equals(keyId)) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "必须选择商业银行第二公钥进行SM2验签");
|
||||
}
|
||||
CurrencyGenerationVerification value = require(id, subject);
|
||||
CurrencyGenerationVerification.Status expected = value.getStatus();
|
||||
boolean valid = keyService.verifyCommercialBank(subject, value.getRequestDigest(), value.getBankSignature());
|
||||
invoke(() -> value.verifySignature(valid));
|
||||
repository.update(value, expected, "VERIFY_SIGNATURE", "使用商业银行第二公钥验证SM2签名", operator);
|
||||
return new CurrencyGenerationVerificationResult(value);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyGenerationVerificationResult verifyDigest(long id, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationVerification value = require(id, subject);
|
||||
CurrencyGenerationVerification.Status expected = value.getStatus();
|
||||
String digest = cryptography.sm3(value.getRequestOriginalText());
|
||||
invoke(() -> value.verifyDigest(digest, digest.equals(value.getRequestDigest())));
|
||||
repository.update(value, expected, "VERIFY_DIGEST", "重新计算SM3并与请求摘要比对", operator);
|
||||
return new CurrencyGenerationVerificationResult(value);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyGenerationVerificationResult verifyQuota(long id, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationVerification value = require(id, subject);
|
||||
CurrencyGenerationVerification.Status expected = value.getStatus();
|
||||
invoke(() -> value.verifyQuota(TOTAL_QUOTA, USED_QUOTA, REMAINING_QUOTA));
|
||||
repository.update(value, expected, "VERIFY_QUOTA", "检查申请总额度、已用额度和剩余额度", operator);
|
||||
return new CurrencyGenerationVerificationResult(value);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyGenerationVerificationResult confirm(long id, String keyId,
|
||||
InstitutionKeySubject subject, String operator) {
|
||||
if (!InstitutionKeyService.CENTRAL_FIRST_KEY.equals(keyId)) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "必须选择中央银行第一私钥完成确认签名");
|
||||
}
|
||||
CurrencyGenerationVerification value = require(id, subject);
|
||||
CurrencyGenerationVerification.Status expected = value.getStatus();
|
||||
String signature = keyService.signCentralBank(subject, value.getRequestDigest());
|
||||
String time = LocalDateTime.now().format(TIME);
|
||||
String message = "{\"requestId\":\"" + value.getRequestId() + "\",\"status\":\"CONFIRMED\","
|
||||
+ "\"cbSignature\":\"" + signature + "\",\"confirmTime\":\"" + time + "\"}";
|
||||
invoke(() -> value.confirm(keyId, signature, time, message));
|
||||
repository.update(value, expected, "CONFIRM", "使用央行第一私钥签名并生成确认报文", operator);
|
||||
return new CurrencyGenerationVerificationResult(value);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CurrencyGenerationVerificationResult returnResponse(long id, InstitutionKeySubject subject, String operator) {
|
||||
CurrencyGenerationVerification value = require(id, subject);
|
||||
CurrencyGenerationVerification.Status expected = value.getStatus();
|
||||
invoke(value::returnResponse);
|
||||
repository.update(value, expected, "RETURN_RESPONSE", "向商业银行返回确认报文", operator);
|
||||
return new CurrencyGenerationVerificationResult(value);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CurrencyGenerationVerificationResult detail(long id, InstitutionKeySubject subject) {
|
||||
return new CurrencyGenerationVerificationResult(require(id, subject));
|
||||
}
|
||||
|
||||
private CurrencyGenerationVerification require(long id, InstitutionKeySubject subject) {
|
||||
return repository.findByIdAndSubject(id, subject.getUserId(), subject.getSchoolId(), subject.getClassId())
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.RESOURCE_NOT_FOUND,
|
||||
"当前实训主体下的步骤三验证记录不存在"));
|
||||
}
|
||||
|
||||
private static void invoke(Runnable runnable) {
|
||||
try { runnable.run(); } catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T invokeResult(Supplier<T> supplier) {
|
||||
try { return supplier.get(); } catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private interface Supplier<T> { T get(); }
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CurrencyRequestResult {
|
||||
private final long requestId;
|
||||
private final long identifierApplicationId;
|
||||
private final String status;
|
||||
private final String institutionIdentifier;
|
||||
private final String fullInstitutionIdentifier;
|
||||
private final BigDecimal amount;
|
||||
private final String deliveryNodeCode;
|
||||
private final String requestTimestamp;
|
||||
private final String requestOriginalText;
|
||||
private final String digestAlgorithm;
|
||||
private final String requestDigest;
|
||||
private final String signatureAlgorithm;
|
||||
private final String signatureHashAlgorithm;
|
||||
private final String signingKeyId;
|
||||
private final String bankSignature;
|
||||
private final String requestMessage;
|
||||
private final String centralBankReceiveStatus;
|
||||
|
||||
public CurrencyRequestResult(CurrencyGenerationRequest request) {
|
||||
this.requestId = request.getId();
|
||||
this.identifierApplicationId = request.getIdentifierApplicationId();
|
||||
this.status = request.getStatus().name();
|
||||
this.institutionIdentifier = request.getInstitutionIdentifier();
|
||||
this.fullInstitutionIdentifier = request.getFullInstitutionIdentifier();
|
||||
this.amount = request.getAmount();
|
||||
this.deliveryNodeCode = request.getDeliveryNodeCode();
|
||||
this.requestTimestamp = request.getRequestTimestamp();
|
||||
this.requestOriginalText = request.getRequestOriginalText();
|
||||
this.digestAlgorithm = request.getRequestDigest() == null ? null : "SM3";
|
||||
this.requestDigest = request.getRequestDigest();
|
||||
this.signatureAlgorithm = request.getBankSignature() == null ? null : "SM2_WITH_SM3";
|
||||
this.signatureHashAlgorithm = request.getBankSignature() == null ? null : "SM3";
|
||||
this.signingKeyId = request.getSigningKeyId();
|
||||
this.bankSignature = request.getBankSignature();
|
||||
this.requestMessage = request.getRequestMessage();
|
||||
this.centralBankReceiveStatus = request.getStatus() == CurrencyGenerationRequest.Status.RECEIVED
|
||||
? "RECEIVED" : "NOT_RECEIVED";
|
||||
}
|
||||
|
||||
public long getRequestId() { return requestId; }
|
||||
public long getIdentifierApplicationId() { return identifierApplicationId; }
|
||||
public String getStatus() { return status; }
|
||||
public String getInstitutionIdentifier() { return institutionIdentifier; }
|
||||
public String getFullInstitutionIdentifier() { return fullInstitutionIdentifier; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public String getDeliveryNodeCode() { return deliveryNodeCode; }
|
||||
public String getRequestTimestamp() { return requestTimestamp; }
|
||||
public String getRequestOriginalText() { return requestOriginalText; }
|
||||
public String getDigestAlgorithm() { return digestAlgorithm; }
|
||||
public String getRequestDigest() { return requestDigest; }
|
||||
public String getSignatureAlgorithm() { return signatureAlgorithm; }
|
||||
public String getSignatureHashAlgorithm() { return signatureHashAlgorithm; }
|
||||
public String getSigningKeyId() { return signingKeyId; }
|
||||
public String getBankSignature() { return bankSignature; }
|
||||
public String getRequestMessage() { return requestMessage; }
|
||||
public String getCentralBankReceiveStatus() { return centralBankReceiveStatus; }
|
||||
}
|
||||
@ -1,38 +1,37 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
import com.yau.digitalrmb.security.context.JwtUser;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
public class InstitutionKeySubject {
|
||||
private final long userId;
|
||||
private final String userId;
|
||||
private final long schoolId;
|
||||
private final long classId;
|
||||
|
||||
public InstitutionKeySubject(long userId, long schoolId, long classId) {
|
||||
public InstitutionKeySubject(String userId, long schoolId, long classId) {
|
||||
this.userId = userId;
|
||||
this.schoolId = schoolId;
|
||||
this.classId = classId;
|
||||
}
|
||||
|
||||
public static InstitutionKeySubject from(Jwt jwt) {
|
||||
return new InstitutionKeySubject(requiredLong(jwt, "userId"), requiredLong(jwt, "schoolId"),
|
||||
requiredLong(jwt, "classId"));
|
||||
public static InstitutionKeySubject from(JwtUser jwtUser) {
|
||||
return new InstitutionKeySubject(jwtUser.getUserId(), requiredLong(jwtUser.getSchoolId(), "schoolId"),
|
||||
requiredLong(jwtUser.getClassId(), "classId"));
|
||||
}
|
||||
|
||||
private static long requiredLong(Jwt jwt, String claim) {
|
||||
Object value = jwt.getClaims().get(claim);
|
||||
private static long requiredLong(String value, String claim) {
|
||||
if (value == null) {
|
||||
throw new BusinessException(ErrorCode.UNAUTHORIZED, "Token缺少" + claim);
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
return Long.parseLong(value);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BusinessException(ErrorCode.UNAUTHORIZED, "Token中的" + claim + "格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
public long getUserId() { return userId; }
|
||||
public String getUserId() { return userId; }
|
||||
public long getSchoolId() { return schoolId; }
|
||||
public long getClassId() { return classId; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
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 IssuedInstitutionIdentifierQueryService {
|
||||
@Resource
|
||||
private InstitutionIdentifierRepository repository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public IssuedInstitutionIdentifierResult requireIssued(long applicationId, InstitutionKeySubject subject) {
|
||||
InstitutionIdentifierApplication application = repository.findById(applicationId).orElseThrow(() ->
|
||||
new BusinessException(ErrorCode.RESOURCE_NOT_FOUND, "机构标识申请不存在"));
|
||||
if (application.getStatus() != InstitutionIdentifierApplication.Status.ISSUED) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR,
|
||||
"任务一尚未签发机构标识,不能发起货币生成请求,当前状态:" + application.getStatus().name());
|
||||
}
|
||||
return new IssuedInstitutionIdentifierResult(applicationId, application.getInstitutionIdentifier(),
|
||||
application.getDigest());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.yau.digitalrmb.institutionidentity.application;
|
||||
|
||||
public class IssuedInstitutionIdentifierResult {
|
||||
private final long applicationId;
|
||||
private final String institutionIdentifier;
|
||||
private final String fullInstitutionIdentifier;
|
||||
|
||||
public IssuedInstitutionIdentifierResult(long applicationId, String institutionIdentifier,
|
||||
String fullInstitutionIdentifier) {
|
||||
this.applicationId = applicationId;
|
||||
this.institutionIdentifier = institutionIdentifier;
|
||||
this.fullInstitutionIdentifier = fullInstitutionIdentifier;
|
||||
}
|
||||
|
||||
public long getApplicationId() { return applicationId; }
|
||||
public String getInstitutionIdentifier() { return institutionIdentifier; }
|
||||
public String getFullInstitutionIdentifier() { return fullInstitutionIdentifier; }
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.yau.digitalrmb.institutionidentity.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CurrencyGenerationRequestRepository {
|
||||
long save(CurrencyGenerationRequest request, String userId, long schoolId, long classId, String operator);
|
||||
|
||||
Optional<CurrencyGenerationRequest> findByIdAndSubject(long requestId, String userId, long schoolId, long classId);
|
||||
|
||||
void update(CurrencyGenerationRequest request, CurrencyGenerationRequest.Status expectedStatus,
|
||||
String operation, String operationDetail, String operator);
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
package com.yau.digitalrmb.institutionidentity.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CurrencyGenerationVerification {
|
||||
public enum Status {
|
||||
RECEIVED, SIGNATURE_VERIFIED, DIGEST_VERIFIED, QUOTA_VERIFIED, CONFIRMED, RESPONSE_RETURNED
|
||||
}
|
||||
|
||||
private final Long id;
|
||||
private final long requestId;
|
||||
private final String requestMessage;
|
||||
private final String requestOriginalText;
|
||||
private final String requestDigest;
|
||||
private final String bankSignature;
|
||||
private final String bankKeyId;
|
||||
private final BigDecimal amount;
|
||||
private Boolean signatureValid;
|
||||
private String recomputedDigest;
|
||||
private Boolean digestMatches;
|
||||
private BigDecimal totalQuota;
|
||||
private BigDecimal usedQuota;
|
||||
private BigDecimal remainingQuota;
|
||||
private Boolean quotaSufficient;
|
||||
private String centralBankKeyId;
|
||||
private String centralBankSignature;
|
||||
private String confirmationTime;
|
||||
private String confirmationMessage;
|
||||
private Status status;
|
||||
|
||||
private CurrencyGenerationVerification(Long id, long requestId, String requestMessage,
|
||||
String requestOriginalText, String requestDigest,
|
||||
String bankSignature, String bankKeyId, BigDecimal amount,
|
||||
Boolean signatureValid, String recomputedDigest,
|
||||
Boolean digestMatches, BigDecimal totalQuota,
|
||||
BigDecimal usedQuota, BigDecimal remainingQuota,
|
||||
Boolean quotaSufficient, String centralBankKeyId,
|
||||
String centralBankSignature, String confirmationTime,
|
||||
String confirmationMessage, Status status) {
|
||||
this.id = id;
|
||||
this.requestId = requestId;
|
||||
this.requestMessage = requestMessage;
|
||||
this.requestOriginalText = requestOriginalText;
|
||||
this.requestDigest = requestDigest;
|
||||
this.bankSignature = bankSignature;
|
||||
this.bankKeyId = bankKeyId;
|
||||
this.amount = amount;
|
||||
this.signatureValid = signatureValid;
|
||||
this.recomputedDigest = recomputedDigest;
|
||||
this.digestMatches = digestMatches;
|
||||
this.totalQuota = totalQuota;
|
||||
this.usedQuota = usedQuota;
|
||||
this.remainingQuota = remainingQuota;
|
||||
this.quotaSufficient = quotaSufficient;
|
||||
this.centralBankKeyId = centralBankKeyId;
|
||||
this.centralBankSignature = centralBankSignature;
|
||||
this.confirmationTime = confirmationTime;
|
||||
this.confirmationMessage = confirmationMessage;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public static CurrencyGenerationVerification receive(long requestId, String requestMessage,
|
||||
String requestOriginalText, String requestDigest,
|
||||
String bankSignature, String bankKeyId,
|
||||
BigDecimal amount) {
|
||||
if (requestId <= 0 || requestMessage == null || requestOriginalText == null
|
||||
|| requestDigest == null || bankSignature == null || bankKeyId == null || amount == null) {
|
||||
throw new IllegalArgumentException("步骤二请求数据不完整,不能进入步骤三");
|
||||
}
|
||||
return new CurrencyGenerationVerification(null, requestId, requestMessage, requestOriginalText,
|
||||
requestDigest, bankSignature, bankKeyId, amount, null, null, null, null, null, null,
|
||||
null, null, null, null, null, Status.RECEIVED);
|
||||
}
|
||||
|
||||
public static CurrencyGenerationVerification restore(Long id, long requestId, String requestMessage,
|
||||
String requestOriginalText, String requestDigest,
|
||||
String bankSignature, String bankKeyId, BigDecimal amount,
|
||||
Boolean signatureValid, String recomputedDigest,
|
||||
Boolean digestMatches, BigDecimal totalQuota,
|
||||
BigDecimal usedQuota, BigDecimal remainingQuota,
|
||||
Boolean quotaSufficient, String centralBankKeyId,
|
||||
String centralBankSignature, String confirmationTime,
|
||||
String confirmationMessage, Status status) {
|
||||
return new CurrencyGenerationVerification(id, requestId, requestMessage, requestOriginalText, requestDigest,
|
||||
bankSignature, bankKeyId, amount, signatureValid, recomputedDigest, digestMatches, totalQuota,
|
||||
usedQuota, remainingQuota, quotaSufficient, centralBankKeyId, centralBankSignature,
|
||||
confirmationTime, confirmationMessage, status);
|
||||
}
|
||||
|
||||
public void verifySignature(boolean valid) {
|
||||
requireStatus(Status.RECEIVED, "请先接收步骤二请求报文");
|
||||
if (!valid) throw new IllegalArgumentException("商业银行SM2签名验证未通过");
|
||||
signatureValid = true;
|
||||
status = Status.SIGNATURE_VERIFIED;
|
||||
}
|
||||
|
||||
public void verifyDigest(String digest, boolean matches) {
|
||||
requireStatus(Status.SIGNATURE_VERIFIED, "请先完成商业银行SM2签名验证");
|
||||
if (!matches) throw new IllegalArgumentException("请求原文SM3完整性验证未通过");
|
||||
recomputedDigest = digest;
|
||||
digestMatches = true;
|
||||
status = Status.DIGEST_VERIFIED;
|
||||
}
|
||||
|
||||
public void verifyQuota(BigDecimal total, BigDecimal used, BigDecimal remaining) {
|
||||
requireStatus(Status.DIGEST_VERIFIED, "请先完成请求SM3完整性验证");
|
||||
if (amount.compareTo(remaining) > 0) throw new IllegalArgumentException("本次申请金额超过剩余投放额度");
|
||||
totalQuota = total;
|
||||
usedQuota = used;
|
||||
remainingQuota = remaining;
|
||||
quotaSufficient = true;
|
||||
status = Status.QUOTA_VERIFIED;
|
||||
}
|
||||
|
||||
public void confirm(String keyId, String signature, String time, String message) {
|
||||
requireStatus(Status.QUOTA_VERIFIED, "请先完成额度检查");
|
||||
centralBankKeyId = keyId;
|
||||
centralBankSignature = signature;
|
||||
confirmationTime = time;
|
||||
confirmationMessage = message;
|
||||
status = Status.CONFIRMED;
|
||||
}
|
||||
|
||||
public void returnResponse() {
|
||||
requireStatus(Status.CONFIRMED, "请先完成央行签名确认");
|
||||
status = Status.RESPONSE_RETURNED;
|
||||
}
|
||||
|
||||
private void requireStatus(Status expected, String message) {
|
||||
if (status != expected) throw new IllegalStateException(message + ",当前状态:" + status.name());
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public long getRequestId() { return requestId; }
|
||||
public String getRequestMessage() { return requestMessage; }
|
||||
public String getRequestOriginalText() { return requestOriginalText; }
|
||||
public String getRequestDigest() { return requestDigest; }
|
||||
public String getBankSignature() { return bankSignature; }
|
||||
public String getBankKeyId() { return bankKeyId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public Boolean getSignatureValid() { return signatureValid; }
|
||||
public String getRecomputedDigest() { return recomputedDigest; }
|
||||
public Boolean getDigestMatches() { return digestMatches; }
|
||||
public BigDecimal getTotalQuota() { return totalQuota; }
|
||||
public BigDecimal getUsedQuota() { return usedQuota; }
|
||||
public BigDecimal getRemainingQuota() { return remainingQuota; }
|
||||
public Boolean getQuotaSufficient() { return quotaSufficient; }
|
||||
public String getCentralBankKeyId() { return centralBankKeyId; }
|
||||
public String getCentralBankSignature() { return centralBankSignature; }
|
||||
public String getConfirmationTime() { return confirmationTime; }
|
||||
public String getConfirmationMessage() { return confirmationMessage; }
|
||||
public Status getStatus() { return status; }
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package com.yau.digitalrmb.institutionidentity.domain;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface CurrencyGenerationVerificationRepository {
|
||||
long save(CurrencyGenerationVerification verification, String userId, long schoolId, long classId, String operator);
|
||||
Optional<CurrencyGenerationVerification> findByIdAndSubject(long verificationId, String userId,
|
||||
long schoolId, long classId);
|
||||
void update(CurrencyGenerationVerification verification,
|
||||
CurrencyGenerationVerification.Status expectedStatus,
|
||||
String operation, String detail, String operator);
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
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("currency_generation_request")
|
||||
public class CurrencyGenerationRequestEntity extends AuditableEntity {
|
||||
@TableField("identifier_application_id") private Long identifierApplicationId;
|
||||
@TableField("institution_identifier") private String institutionIdentifier;
|
||||
@TableField("full_institution_identifier") private String fullInstitutionIdentifier;
|
||||
private BigDecimal amount;
|
||||
@TableField("delivery_node_code") private String deliveryNodeCode;
|
||||
@TableField("request_timestamp") private String requestTimestamp;
|
||||
@TableField("request_original_text") private String requestOriginalText;
|
||||
@TableField("request_digest") private String requestDigest;
|
||||
@TableField("bank_signature") private String bankSignature;
|
||||
@TableField("signing_key_id") private String signingKeyId;
|
||||
@TableField("request_message") private String requestMessage;
|
||||
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 CurrencyGenerationRequestMapper extends BaseMapper<CurrencyGenerationRequestEntity> {
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
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;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("currency_generation_request_operation_log")
|
||||
public class CurrencyGenerationRequestOperationLogEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
@TableField("request_id") private Long requestId;
|
||||
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;
|
||||
}
|
||||
@ -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 CurrencyGenerationRequestOperationLogMapper
|
||||
extends BaseMapper<CurrencyGenerationRequestOperationLogEntity> {
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
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("currency_generation_verification")
|
||||
public class CurrencyGenerationVerificationEntity extends AuditableEntity {
|
||||
@TableField("request_id") private Long requestId;
|
||||
@TableField("request_message") private String requestMessage;
|
||||
@TableField("request_original_text") private String requestOriginalText;
|
||||
@TableField("request_digest") private String requestDigest;
|
||||
@TableField("bank_signature") private String bankSignature;
|
||||
@TableField("bank_key_id") private String bankKeyId;
|
||||
private BigDecimal amount;
|
||||
@TableField("signature_valid") private Boolean signatureValid;
|
||||
@TableField("recomputed_digest") private String recomputedDigest;
|
||||
@TableField("digest_matches") private Boolean digestMatches;
|
||||
@TableField("total_quota") private BigDecimal totalQuota;
|
||||
@TableField("used_quota") private BigDecimal usedQuota;
|
||||
@TableField("remaining_quota") private BigDecimal remainingQuota;
|
||||
@TableField("quota_sufficient") private Boolean quotaSufficient;
|
||||
@TableField("central_bank_key_id") private String centralBankKeyId;
|
||||
@TableField("central_bank_signature") private String centralBankSignature;
|
||||
@TableField("confirmation_time") private String confirmationTime;
|
||||
@TableField("confirmation_message") private String confirmationMessage;
|
||||
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 CurrencyGenerationVerificationMapper extends BaseMapper<CurrencyGenerationVerificationEntity> {
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
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;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("currency_generation_verification_operation_log")
|
||||
public class CurrencyGenerationVerificationOperationLogEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
@TableField("verification_id") private Long verificationId;
|
||||
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;
|
||||
}
|
||||
@ -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 CurrencyGenerationVerificationOperationLogMapper
|
||||
extends BaseMapper<CurrencyGenerationVerificationOperationLogEntity> {
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
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.CurrencyGenerationRequest;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationRequestRepository;
|
||||
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 MybatisCurrencyGenerationRequestRepository implements CurrencyGenerationRequestRepository {
|
||||
@Resource
|
||||
private CurrencyGenerationRequestMapper mapper;
|
||||
|
||||
@Resource
|
||||
private CurrencyGenerationRequestOperationLogMapper logMapper;
|
||||
|
||||
@Override
|
||||
public long save(CurrencyGenerationRequest request, String userId, long schoolId, long classId, String operator) {
|
||||
CurrencyGenerationRequestEntity entity = toEntity(request);
|
||||
entity.setUserId(userId);
|
||||
entity.setSchoolId(schoolId);
|
||||
entity.setClassId(classId);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setCreatedAt(now);
|
||||
entity.setUpdatedAt(now);
|
||||
entity.setCreatedBy(operator);
|
||||
entity.setUpdatedBy(operator);
|
||||
entity.setDeleted(false);
|
||||
mapper.insert(entity);
|
||||
insertLog(entity.getId(), "PREPARE", null, request.getStatus().name(),
|
||||
"从任务一已签发申请复制简洁和完整机构标识,保存请求信息", operator);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CurrencyGenerationRequest> findByIdAndSubject(long requestId, String userId,
|
||||
long schoolId, long classId) {
|
||||
CurrencyGenerationRequestEntity entity = mapper.selectOne(
|
||||
new LambdaQueryWrapper<CurrencyGenerationRequestEntity>()
|
||||
.eq(CurrencyGenerationRequestEntity::getId, requestId)
|
||||
.eq(CurrencyGenerationRequestEntity::getUserId, userId)
|
||||
.eq(CurrencyGenerationRequestEntity::getSchoolId, schoolId)
|
||||
.eq(CurrencyGenerationRequestEntity::getClassId, classId)
|
||||
.eq(CurrencyGenerationRequestEntity::getDeleted, false));
|
||||
return Optional.ofNullable(entity).map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(CurrencyGenerationRequest request, CurrencyGenerationRequest.Status expectedStatus,
|
||||
String operation, String operationDetail, String operator) {
|
||||
LambdaUpdateWrapper<CurrencyGenerationRequestEntity> update =
|
||||
new LambdaUpdateWrapper<CurrencyGenerationRequestEntity>()
|
||||
.eq(CurrencyGenerationRequestEntity::getId, request.getId())
|
||||
.eq(CurrencyGenerationRequestEntity::getStatus, expectedStatus.name())
|
||||
.eq(CurrencyGenerationRequestEntity::getDeleted, false)
|
||||
.set(CurrencyGenerationRequestEntity::getRequestOriginalText,
|
||||
request.getRequestOriginalText())
|
||||
.set(CurrencyGenerationRequestEntity::getRequestDigest, request.getRequestDigest())
|
||||
.set(CurrencyGenerationRequestEntity::getBankSignature, request.getBankSignature())
|
||||
.set(CurrencyGenerationRequestEntity::getSigningKeyId, request.getSigningKeyId())
|
||||
.set(CurrencyGenerationRequestEntity::getRequestMessage, request.getRequestMessage())
|
||||
.set(CurrencyGenerationRequestEntity::getStatus, request.getStatus().name())
|
||||
.set(CurrencyGenerationRequestEntity::getUpdatedAt, LocalDateTime.now())
|
||||
.set(CurrencyGenerationRequestEntity::getUpdatedBy, operator);
|
||||
if (mapper.update(null, update) != 1) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "请求状态已变化,不能重复或越级操作");
|
||||
}
|
||||
insertLog(request.getId(), operation, expectedStatus.name(), request.getStatus().name(),
|
||||
operationDetail, operator);
|
||||
}
|
||||
|
||||
private CurrencyGenerationRequestEntity toEntity(CurrencyGenerationRequest request) {
|
||||
CurrencyGenerationRequestEntity entity = new CurrencyGenerationRequestEntity();
|
||||
entity.setId(request.getId());
|
||||
entity.setIdentifierApplicationId(request.getIdentifierApplicationId());
|
||||
entity.setInstitutionIdentifier(request.getInstitutionIdentifier());
|
||||
entity.setFullInstitutionIdentifier(request.getFullInstitutionIdentifier());
|
||||
entity.setAmount(request.getAmount());
|
||||
entity.setDeliveryNodeCode(request.getDeliveryNodeCode());
|
||||
entity.setRequestTimestamp(request.getRequestTimestamp());
|
||||
entity.setRequestOriginalText(request.getRequestOriginalText());
|
||||
entity.setRequestDigest(request.getRequestDigest());
|
||||
entity.setBankSignature(request.getBankSignature());
|
||||
entity.setSigningKeyId(request.getSigningKeyId());
|
||||
entity.setRequestMessage(request.getRequestMessage());
|
||||
entity.setStatus(request.getStatus().name());
|
||||
return entity;
|
||||
}
|
||||
|
||||
private CurrencyGenerationRequest toDomain(CurrencyGenerationRequestEntity entity) {
|
||||
return CurrencyGenerationRequest.restore(entity.getId(), entity.getIdentifierApplicationId(),
|
||||
entity.getInstitutionIdentifier(), entity.getFullInstitutionIdentifier(), entity.getAmount(),
|
||||
entity.getDeliveryNodeCode(), entity.getRequestTimestamp(), entity.getRequestOriginalText(),
|
||||
entity.getRequestDigest(), entity.getBankSignature(), entity.getSigningKeyId(),
|
||||
entity.getRequestMessage(), CurrencyGenerationRequest.Status.valueOf(entity.getStatus()));
|
||||
}
|
||||
|
||||
private void insertLog(Long requestId, String operation, String fromStatus, String toStatus,
|
||||
String operationDetail, String operator) {
|
||||
CurrencyGenerationRequestOperationLogEntity log = new CurrencyGenerationRequestOperationLogEntity();
|
||||
log.setRequestId(requestId);
|
||||
log.setOperation(operation);
|
||||
log.setFromStatus(fromStatus);
|
||||
log.setToStatus(toStatus);
|
||||
log.setOperationDetail(operationDetail);
|
||||
log.setCreatedAt(LocalDateTime.now());
|
||||
log.setCreatedBy(operator);
|
||||
logMapper.insert(log);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
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.CurrencyGenerationVerification;
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerificationRepository;
|
||||
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 MybatisCurrencyGenerationVerificationRepository implements CurrencyGenerationVerificationRepository {
|
||||
@Resource private CurrencyGenerationVerificationMapper mapper;
|
||||
@Resource private CurrencyGenerationVerificationOperationLogMapper logMapper;
|
||||
|
||||
@Override
|
||||
public long save(CurrencyGenerationVerification value, String userId, long schoolId, long classId, String operator) {
|
||||
CurrencyGenerationVerificationEntity entity = toEntity(value);
|
||||
entity.setUserId(userId);
|
||||
entity.setSchoolId(schoolId);
|
||||
entity.setClassId(classId);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
entity.setCreatedAt(now);
|
||||
entity.setUpdatedAt(now);
|
||||
entity.setCreatedBy(operator);
|
||||
entity.setUpdatedBy(operator);
|
||||
entity.setDeleted(false);
|
||||
mapper.insert(entity);
|
||||
log(entity.getId(), "RECEIVE", null, value.getStatus().name(), "接收步骤二货币生成请求报文", operator);
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CurrencyGenerationVerification> findByIdAndSubject(long id, String userId,
|
||||
long schoolId, long classId) {
|
||||
CurrencyGenerationVerificationEntity entity = mapper.selectOne(
|
||||
new LambdaQueryWrapper<CurrencyGenerationVerificationEntity>()
|
||||
.eq(CurrencyGenerationVerificationEntity::getId, id)
|
||||
.eq(CurrencyGenerationVerificationEntity::getUserId, userId)
|
||||
.eq(CurrencyGenerationVerificationEntity::getSchoolId, schoolId)
|
||||
.eq(CurrencyGenerationVerificationEntity::getClassId, classId)
|
||||
.eq(CurrencyGenerationVerificationEntity::getDeleted, false));
|
||||
return Optional.ofNullable(entity).map(this::toDomain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(CurrencyGenerationVerification value,
|
||||
CurrencyGenerationVerification.Status expectedStatus,
|
||||
String operation, String detail, String operator) {
|
||||
LambdaUpdateWrapper<CurrencyGenerationVerificationEntity> update =
|
||||
new LambdaUpdateWrapper<CurrencyGenerationVerificationEntity>()
|
||||
.eq(CurrencyGenerationVerificationEntity::getId, value.getId())
|
||||
.eq(CurrencyGenerationVerificationEntity::getStatus, expectedStatus.name())
|
||||
.eq(CurrencyGenerationVerificationEntity::getDeleted, false)
|
||||
.set(CurrencyGenerationVerificationEntity::getSignatureValid, value.getSignatureValid())
|
||||
.set(CurrencyGenerationVerificationEntity::getRecomputedDigest, value.getRecomputedDigest())
|
||||
.set(CurrencyGenerationVerificationEntity::getDigestMatches, value.getDigestMatches())
|
||||
.set(CurrencyGenerationVerificationEntity::getTotalQuota, value.getTotalQuota())
|
||||
.set(CurrencyGenerationVerificationEntity::getUsedQuota, value.getUsedQuota())
|
||||
.set(CurrencyGenerationVerificationEntity::getRemainingQuota, value.getRemainingQuota())
|
||||
.set(CurrencyGenerationVerificationEntity::getQuotaSufficient, value.getQuotaSufficient())
|
||||
.set(CurrencyGenerationVerificationEntity::getCentralBankKeyId, value.getCentralBankKeyId())
|
||||
.set(CurrencyGenerationVerificationEntity::getCentralBankSignature,
|
||||
value.getCentralBankSignature())
|
||||
.set(CurrencyGenerationVerificationEntity::getConfirmationTime, value.getConfirmationTime())
|
||||
.set(CurrencyGenerationVerificationEntity::getConfirmationMessage,
|
||||
value.getConfirmationMessage())
|
||||
.set(CurrencyGenerationVerificationEntity::getStatus, value.getStatus().name())
|
||||
.set(CurrencyGenerationVerificationEntity::getUpdatedAt, LocalDateTime.now())
|
||||
.set(CurrencyGenerationVerificationEntity::getUpdatedBy, operator);
|
||||
if (mapper.update(null, update) != 1) {
|
||||
throw new BusinessException(ErrorCode.VALIDATION_ERROR, "验证状态已变化,不能重复或越级操作");
|
||||
}
|
||||
log(value.getId(), operation, expectedStatus.name(), value.getStatus().name(), detail, operator);
|
||||
}
|
||||
|
||||
private CurrencyGenerationVerificationEntity toEntity(CurrencyGenerationVerification value) {
|
||||
CurrencyGenerationVerificationEntity e = new CurrencyGenerationVerificationEntity();
|
||||
e.setId(value.getId()); e.setRequestId(value.getRequestId()); e.setRequestMessage(value.getRequestMessage());
|
||||
e.setRequestOriginalText(value.getRequestOriginalText()); e.setRequestDigest(value.getRequestDigest());
|
||||
e.setBankSignature(value.getBankSignature()); e.setBankKeyId(value.getBankKeyId()); e.setAmount(value.getAmount());
|
||||
e.setSignatureValid(value.getSignatureValid()); e.setRecomputedDigest(value.getRecomputedDigest());
|
||||
e.setDigestMatches(value.getDigestMatches()); e.setTotalQuota(value.getTotalQuota());
|
||||
e.setUsedQuota(value.getUsedQuota()); e.setRemainingQuota(value.getRemainingQuota());
|
||||
e.setQuotaSufficient(value.getQuotaSufficient()); e.setCentralBankKeyId(value.getCentralBankKeyId());
|
||||
e.setCentralBankSignature(value.getCentralBankSignature()); e.setConfirmationTime(value.getConfirmationTime());
|
||||
e.setConfirmationMessage(value.getConfirmationMessage()); e.setStatus(value.getStatus().name());
|
||||
return e;
|
||||
}
|
||||
|
||||
private CurrencyGenerationVerification toDomain(CurrencyGenerationVerificationEntity e) {
|
||||
return CurrencyGenerationVerification.restore(e.getId(), e.getRequestId(), e.getRequestMessage(),
|
||||
e.getRequestOriginalText(), e.getRequestDigest(), e.getBankSignature(), e.getBankKeyId(), e.getAmount(),
|
||||
e.getSignatureValid(), e.getRecomputedDigest(), e.getDigestMatches(), e.getTotalQuota(), e.getUsedQuota(),
|
||||
e.getRemainingQuota(), e.getQuotaSufficient(), e.getCentralBankKeyId(), e.getCentralBankSignature(),
|
||||
e.getConfirmationTime(), e.getConfirmationMessage(),
|
||||
CurrencyGenerationVerification.Status.valueOf(e.getStatus()));
|
||||
}
|
||||
|
||||
private void log(Long id, String operation, String from, String to, String detail, String operator) {
|
||||
CurrencyGenerationVerificationOperationLogEntity e = new CurrencyGenerationVerificationOperationLogEntity();
|
||||
e.setVerificationId(id); e.setOperation(operation); e.setFromStatus(from); e.setToStatus(to);
|
||||
e.setOperationDetail(detail); e.setCreatedAt(LocalDateTime.now()); e.setCreatedBy(operator); logMapper.insert(e);
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,15 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class ConfirmInstitutionIdentifierRequest {
|
||||
@NotNull(message = "申请编号不能为空")
|
||||
private Long applicationId;
|
||||
|
||||
@NotBlank(message = "签名密钥不能为空")
|
||||
private String signingKeyId;
|
||||
|
||||
public Long getApplicationId() {
|
||||
return applicationId;
|
||||
}
|
||||
|
||||
public void setApplicationId(Long applicationId) {
|
||||
this.applicationId = applicationId;
|
||||
}
|
||||
|
||||
public String getSigningKeyId() {
|
||||
return signingKeyId;
|
||||
}
|
||||
|
||||
public void setSigningKeyId(String signingKeyId) {
|
||||
this.signingKeyId = signingKeyId;
|
||||
}
|
||||
public Long getApplicationId() { return applicationId; }
|
||||
public void setApplicationId(Long applicationId) { this.applicationId = applicationId; }
|
||||
public String getSigningKeyId() { return signingKeyId; }
|
||||
public void setSigningKeyId(String signingKeyId) { this.signingKeyId = signingKeyId; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Positive;
|
||||
|
||||
public class CurrencyRequestStepRequest {
|
||||
@NotNull(message = "货币生成请求编号不能为空")
|
||||
@Positive(message = "货币生成请求编号必须大于0")
|
||||
private Long requestId;
|
||||
|
||||
private String keyId;
|
||||
|
||||
public Long getRequestId() { return requestId; }
|
||||
public void setRequestId(Long requestId) { this.requestId = requestId; }
|
||||
public String getKeyId() { return keyId; }
|
||||
public void setKeyId(String keyId) { this.keyId = keyId; }
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Positive;
|
||||
|
||||
public class CurrencyVerificationStepRequest {
|
||||
@NotNull(message = "步骤三验证编号不能为空")
|
||||
@Positive(message = "步骤三验证编号必须大于0")
|
||||
private Long verificationId;
|
||||
private String keyId;
|
||||
|
||||
public Long getVerificationId() { return verificationId; }
|
||||
public void setVerificationId(Long verificationId) { this.verificationId = verificationId; }
|
||||
public String getKeyId() { return keyId; }
|
||||
public void setKeyId(String keyId) { this.keyId = keyId; }
|
||||
}
|
||||
@ -1,26 +1,15 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
public class InstitutionIdentifierStepRequest {
|
||||
@NotNull(message = "申请编号不能为空")
|
||||
private Long applicationId;
|
||||
|
||||
@Size(max = 64, message = "密钥标识长度不能超过64个字符")
|
||||
private String keyId;
|
||||
|
||||
public Long getApplicationId() {
|
||||
return applicationId;
|
||||
}
|
||||
|
||||
public void setApplicationId(Long applicationId) {
|
||||
this.applicationId = applicationId;
|
||||
}
|
||||
|
||||
public String getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public void setKeyId(String keyId) {
|
||||
this.keyId = keyId;
|
||||
}
|
||||
public Long getApplicationId() { return applicationId; }
|
||||
public void setApplicationId(Long applicationId) { this.applicationId = applicationId; }
|
||||
public String getKeyId() { return keyId; }
|
||||
public void setKeyId(String keyId) { this.keyId = keyId; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.DecimalMax;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
import javax.validation.constraints.Digits;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Positive;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class PrepareCurrencyRequest {
|
||||
@NotNull(message = "机构标识申请编号不能为空")
|
||||
@Positive(message = "机构标识申请编号必须大于0")
|
||||
private Long identifierApplicationId;
|
||||
|
||||
@NotNull(message = "申请金额不能为空")
|
||||
@DecimalMin(value = "0.01", message = "申请金额必须大于0")
|
||||
@DecimalMax(value = "50000.00", message = "申请金额不能超过50000.00元")
|
||||
@Digits(integer = 16, fraction = 2, message = "申请金额最多保留2位小数")
|
||||
private BigDecimal amount;
|
||||
|
||||
@NotBlank(message = "投放节点编号不能为空")
|
||||
private String deliveryNodeCode;
|
||||
|
||||
public Long getIdentifierApplicationId() { return identifierApplicationId; }
|
||||
public void setIdentifierApplicationId(Long identifierApplicationId) {
|
||||
this.identifierApplicationId = identifierApplicationId;
|
||||
}
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
public String getDeliveryNodeCode() { return deliveryNodeCode; }
|
||||
public void setDeliveryNodeCode(String deliveryNodeCode) { this.deliveryNodeCode = deliveryNodeCode; }
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Positive;
|
||||
|
||||
public class ReceiveCurrencyVerificationRequest {
|
||||
@NotNull(message = "步骤二货币生成请求编号不能为空")
|
||||
@Positive(message = "步骤二货币生成请求编号必须大于0")
|
||||
private Long requestId;
|
||||
|
||||
public Long getRequestId() { return requestId; }
|
||||
public void setRequestId(Long requestId) { this.requestId = requestId; }
|
||||
}
|
||||
@ -1,28 +1,15 @@
|
||||
package com.yau.digitalrmb.institutionidentity.interfaces.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class VerifyInstitutionApplicationRequest {
|
||||
@NotNull(message = "申请编号不能为空")
|
||||
private Long applicationId;
|
||||
|
||||
@NotBlank(message = "验证密钥不能为空")
|
||||
private String verificationKeyId;
|
||||
|
||||
public Long getApplicationId() {
|
||||
return applicationId;
|
||||
}
|
||||
|
||||
public void setApplicationId(Long applicationId) {
|
||||
this.applicationId = applicationId;
|
||||
}
|
||||
|
||||
public String getVerificationKeyId() {
|
||||
return verificationKeyId;
|
||||
}
|
||||
|
||||
public void setVerificationKeyId(String verificationKeyId) {
|
||||
this.verificationKeyId = verificationKeyId;
|
||||
}
|
||||
public Long getApplicationId() { return applicationId; }
|
||||
public void setApplicationId(Long applicationId) { this.applicationId = applicationId; }
|
||||
public String getVerificationKeyId() { return verificationKeyId; }
|
||||
public void setVerificationKeyId(String verificationKeyId) { this.verificationKeyId = verificationKeyId; }
|
||||
}
|
||||
|
||||
@ -1,50 +1,71 @@
|
||||
package com.yau.digitalrmb.security.application;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.PlatformUserSnapshotEntity;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.entity.UserEntity;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.PlatformUserSnapshotMapper;
|
||||
import com.yau.digitalrmb.identity.infrastructure.persistence.mapper.UserMapper;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@Service
|
||||
public class LocalAccountAuthenticationService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LocalAccountAuthenticationService.class);
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final PlatformUserSnapshotMapper snapshotMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtTokenService jwtTokenService;
|
||||
|
||||
public LocalAccountAuthenticationService(UserMapper userMapper,
|
||||
PlatformUserSnapshotMapper snapshotMapper,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JwtTokenService jwtTokenService) {
|
||||
this.userMapper = userMapper;
|
||||
this.snapshotMapper = snapshotMapper;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtTokenService = jwtTokenService;
|
||||
}
|
||||
|
||||
public JwtTokenService.Token login(String username, String rawPassword) {
|
||||
UserEntity user = userMapper.selectOne(new LambdaQueryWrapper<UserEntity>()
|
||||
.eq(UserEntity::getUsername, username)
|
||||
.eq(UserEntity::getEnabled, true)
|
||||
.last("LIMIT 1"));
|
||||
if (user == null || !passwordEncoder.matches(rawPassword, user.getPasswordHash())) {
|
||||
public JwtTokenService.Token login(String studentId, String rawPassword) {
|
||||
UserEntity user = userMapper.selectActiveByStudentId(studentId);
|
||||
if (user == null) {
|
||||
LOGGER.warn("Local login rejected: studentId does not exist or user is deleted, studentId={}",
|
||||
studentId);
|
||||
throw invalidCredentials();
|
||||
}
|
||||
PlatformUserSnapshotEntity snapshot = snapshotMapper.selectById(user.getId());
|
||||
if (snapshot == null) {
|
||||
if (!passwordMatchesAndUpgrade(user, rawPassword)) {
|
||||
LOGGER.warn("Local login rejected: password does not match, studentId={}", studentId);
|
||||
throw invalidCredentials();
|
||||
}
|
||||
return jwtTokenService.issueFor(user.getId(), snapshot.getAccount(), Collections.singleton(snapshot.getRoleKey()));
|
||||
if (user.getRoleId() == null) {
|
||||
LOGGER.error("Local login rejected: role is not configured, userId={}, studentId={}",
|
||||
user.getUserId(), studentId);
|
||||
throw new BusinessException(ErrorCode.UNAUTHORIZED, "用户角色未配置");
|
||||
}
|
||||
return jwtTokenService.issueFor(user);
|
||||
}
|
||||
|
||||
private boolean passwordMatchesAndUpgrade(UserEntity user, String rawPassword) {
|
||||
String storedPassword = user.getPassword();
|
||||
if (storedPassword == null || rawPassword == null) {
|
||||
return false;
|
||||
}
|
||||
if (isBcryptPassword(storedPassword)) {
|
||||
return passwordEncoder.matches(rawPassword, storedPassword);
|
||||
}
|
||||
if (!storedPassword.equals(rawPassword)) {
|
||||
return false;
|
||||
}
|
||||
userMapper.upgradeLegacyPassword(user.getUserId(), passwordEncoder.encode(rawPassword));
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isBcryptPassword(String password) {
|
||||
return password.startsWith("$2a$")
|
||||
|| password.startsWith("$2b$")
|
||||
|| password.startsWith("$2y$");
|
||||
}
|
||||
|
||||
private BusinessException invalidCredentials() {
|
||||
return new BusinessException(ErrorCode.UNAUTHORIZED, "用户名或密码错误");
|
||||
return new BusinessException(ErrorCode.UNAUTHORIZED, "学号或密码错误");
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
package com.yau.digitalrmb.security.context;
|
||||
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AuthContextHolder {
|
||||
private AuthContextHolder() {
|
||||
}
|
||||
|
||||
public static JwtUser get() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt)) {
|
||||
throw unauthorized("用户身份无效");
|
||||
}
|
||||
|
||||
Jwt jwt = (Jwt) authentication.getPrincipal();
|
||||
String userId = requiredUuid(jwt.getClaims().get("userId"), jwt.getSubject());
|
||||
String studentId = text(jwt.getClaims().get("studentId"));
|
||||
if (studentId == null) {
|
||||
studentId = text(jwt.getClaims().get("preferred_username"));
|
||||
}
|
||||
if (studentId == null) {
|
||||
studentId = authentication.getName();
|
||||
}
|
||||
|
||||
return new JwtUser(userId, studentId, text(jwt.getClaims().get("userName")),
|
||||
text(jwt.getClaims().get("classId")), text(jwt.getClaims().get("className")),
|
||||
text(jwt.getClaims().get("phone")), text(jwt.getClaims().get("schoolName")),
|
||||
text(jwt.getClaims().get("schoolId")), text(jwt.getClaims().get("authorizeTime")),
|
||||
text(jwt.getClaims().get("authorizeEndTime")), optionalInteger(jwt, "roleId"),
|
||||
text(jwt.getClaims().get("createTime")), optionalBoolean(jwt, "isDeleted"),
|
||||
text(jwt.getClaims().get("zyUserId")), roles(jwt.getClaims().get("roles")));
|
||||
}
|
||||
|
||||
private static Integer optionalInteger(Jwt jwt, String claim) {
|
||||
Object value = jwt.getClaims().get(claim);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(String.valueOf(value));
|
||||
} catch (NumberFormatException exception) {
|
||||
throw unauthorized("Token中的" + claim + "格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean optionalBoolean(Jwt jwt, String claim) {
|
||||
Object value = jwt.getClaims().get(claim);
|
||||
return value == null ? null : Boolean.valueOf(String.valueOf(value));
|
||||
}
|
||||
|
||||
private static String requiredUuid(Object claimValue, String fallbackValue) {
|
||||
String value = text(claimValue);
|
||||
if (value != null && isUuid(value)) {
|
||||
return UUID.fromString(value).toString();
|
||||
}
|
||||
if (fallbackValue != null && isUuid(fallbackValue)) {
|
||||
return UUID.fromString(fallbackValue).toString();
|
||||
}
|
||||
if (claimValue == null) {
|
||||
throw unauthorized("Token缺少userId");
|
||||
}
|
||||
throw unauthorized("Token中的userId格式不正确");
|
||||
}
|
||||
|
||||
private static boolean isUuid(String value) {
|
||||
try {
|
||||
UUID.fromString(value);
|
||||
return true;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String text(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static Set<String> roles(Object value) {
|
||||
if (value == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> roles = new LinkedHashSet<>();
|
||||
if (value instanceof Collection) {
|
||||
for (Object role : (Collection<?>) value) {
|
||||
String text = text(role);
|
||||
if (text != null) {
|
||||
roles.add(text);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String text = text(value);
|
||||
if (text != null) {
|
||||
roles.add(text);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(roles);
|
||||
}
|
||||
|
||||
private static BusinessException unauthorized(String message) {
|
||||
return new BusinessException(ErrorCode.UNAUTHORIZED, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.yau.digitalrmb.security.context;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public final class JwtUser {
|
||||
private final String userId;
|
||||
private final String studentId;
|
||||
private final String userName;
|
||||
private final String classId;
|
||||
private final String className;
|
||||
private final String phone;
|
||||
private final String schoolName;
|
||||
private final String schoolId;
|
||||
private final String authorizeTime;
|
||||
private final String authorizeEndTime;
|
||||
private final Integer roleId;
|
||||
private final String createTime;
|
||||
private final Boolean isDeleted;
|
||||
private final String zyUserId;
|
||||
private final Set<String> roles;
|
||||
|
||||
JwtUser(String userId, String studentId, String userName, String classId, String className, String phone,
|
||||
String schoolName, String schoolId, String authorizeTime, String authorizeEndTime, Integer roleId,
|
||||
String createTime, Boolean isDeleted, String zyUserId, Set<String> roles) {
|
||||
this.userId = userId;
|
||||
this.studentId = studentId;
|
||||
this.userName = userName;
|
||||
this.classId = classId;
|
||||
this.className = className;
|
||||
this.phone = phone;
|
||||
this.schoolName = schoolName;
|
||||
this.schoolId = schoolId;
|
||||
this.authorizeTime = authorizeTime;
|
||||
this.authorizeEndTime = authorizeEndTime;
|
||||
this.roleId = roleId;
|
||||
this.createTime = createTime;
|
||||
this.isDeleted = isDeleted;
|
||||
this.zyUserId = zyUserId;
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return studentId;
|
||||
}
|
||||
|
||||
public String getStudentId() {
|
||||
return studentId;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public String getSchoolId() {
|
||||
return schoolId;
|
||||
}
|
||||
|
||||
public String getClassId() {
|
||||
return classId;
|
||||
}
|
||||
|
||||
public String getClassName() { return className; }
|
||||
public String getPhone() { return phone; }
|
||||
public String getSchoolName() { return schoolName; }
|
||||
public String getAuthorizeTime() { return authorizeTime; }
|
||||
public String getAuthorizeEndTime() { return authorizeEndTime; }
|
||||
public Integer getRoleId() { return roleId; }
|
||||
public String getCreateTime() { return createTime; }
|
||||
public Boolean getIsDeleted() { return isDeleted; }
|
||||
public String getZyUserId() { return zyUserId; }
|
||||
|
||||
public Set<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,30 @@
|
||||
package com.yau.digitalrmb.security.interfaces;
|
||||
import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.validation.constraints.NotBlank;
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor
|
||||
public class LoginRequest { @NotBlank private String username; @NotBlank private String password; public String username() { return username; } public String password() { return password; } }
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginRequest {
|
||||
@NotBlank
|
||||
@JsonAlias("username")
|
||||
private String studentId;
|
||||
|
||||
@NotBlank
|
||||
private String password;
|
||||
|
||||
public String studentId() {
|
||||
return studentId;
|
||||
}
|
||||
|
||||
public String password() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package com.yau.digitalrmb.security.interfaces;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class UserLoginResponse {
|
||||
private final String userId;
|
||||
private final String name;
|
||||
private final String accessToken;
|
||||
private final String tokenType;
|
||||
private final long expiresIn;
|
||||
private final Integer roleId;
|
||||
private final String classId;
|
||||
private final String className;
|
||||
private final String schoolId;
|
||||
private final String schoolName;
|
||||
private final String collegeId;
|
||||
private final String collegeName;
|
||||
private final String majorId;
|
||||
private final String majorName;
|
||||
private final String username;
|
||||
private final String studentId;
|
||||
private final List<String> authorityCodes;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.yau.digitalrmb.security.interfaces;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class UserLoginResult<T> {
|
||||
private final int code;
|
||||
private final String msg;
|
||||
private final T data;
|
||||
|
||||
private UserLoginResult(int code, String msg, T data) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public static <T> UserLoginResult<T> success(T data) {
|
||||
return new UserLoginResult<T>(200, null, data);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,206 @@
|
||||
CREATE TABLE IF NOT EXISTS institution_identifier_application (
|
||||
id BIGINT PRIMARY KEY,
|
||||
bank_code VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
school_id BIGINT NOT NULL,
|
||||
class_id BIGINT NOT NULL,
|
||||
request_timestamp CHAR(14) NOT NULL,
|
||||
digest CHAR(64),
|
||||
bank_signature VARCHAR(512),
|
||||
status VARCHAR(32) NOT NULL,
|
||||
verification_key_id VARCHAR(64),
|
||||
signature_valid BOOLEAN,
|
||||
digest_matches BOOLEAN,
|
||||
signing_key_id VARCHAR(64),
|
||||
institution_identifier VARCHAR(32),
|
||||
central_bank_signature VARCHAR(512),
|
||||
training_round INT NOT NULL DEFAULT 1,
|
||||
scoring_criteria INT NOT NULL DEFAULT 0,
|
||||
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_identifier_training_round UNIQUE (bank_code, created_by, training_round)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_institution_identifier_status
|
||||
ON institution_identifier_application(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_identifier_training_user
|
||||
ON institution_identifier_application(created_by, training_round);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS institution_identifier_operation_log (
|
||||
id BIGINT PRIMARY KEY,
|
||||
application_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,
|
||||
CONSTRAINT fk_identifier_operation_application
|
||||
FOREIGN KEY (application_id) REFERENCES institution_identifier_application(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_identifier_operation_application
|
||||
ON institution_identifier_operation_log(application_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS institution_identifier_training_error (
|
||||
id BIGINT PRIMARY KEY,
|
||||
application_id BIGINT NOT NULL,
|
||||
training_round INT NOT NULL,
|
||||
error_sequence INT NOT NULL,
|
||||
error_type VARCHAR(32) NOT NULL,
|
||||
operation_step VARCHAR(32) NOT NULL,
|
||||
application_status VARCHAR(32) NOT NULL,
|
||||
provided_key_id VARCHAR(64),
|
||||
error_message VARCHAR(512) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
CONSTRAINT uk_identifier_training_error UNIQUE (application_id, error_sequence),
|
||||
CONSTRAINT fk_identifier_error_application
|
||||
FOREIGN KEY (application_id) REFERENCES institution_identifier_application(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_identifier_error_application
|
||||
ON institution_identifier_training_error(application_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS institution_sm2_key (
|
||||
id BIGINT PRIMARY KEY,
|
||||
key_id VARCHAR(64) NOT NULL,
|
||||
key_name VARCHAR(64) NOT NULL,
|
||||
key_owner VARCHAR(32) NOT NULL,
|
||||
key_purpose VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
school_id BIGINT NOT NULL,
|
||||
class_id BIGINT NOT NULL,
|
||||
public_key VARCHAR(256) NOT NULL,
|
||||
encrypted_private_key VARCHAR(512) 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
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_institution_sm2_key_subject
|
||||
ON institution_sm2_key(user_id, school_id, class_id, key_owner, deleted);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS institution_sm2_key_audit (
|
||||
id BIGINT PRIMARY KEY,
|
||||
key_record_id BIGINT NOT NULL,
|
||||
key_id VARCHAR(64) NOT NULL,
|
||||
operation VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
school_id BIGINT NOT NULL,
|
||||
class_id BIGINT NOT NULL,
|
||||
operation_detail VARCHAR(512) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
CONSTRAINT fk_institution_key_audit_key
|
||||
FOREIGN KEY (key_record_id) REFERENCES institution_sm2_key(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_institution_key_audit_record
|
||||
ON institution_sm2_key_audit(key_record_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS currency_generation_request (
|
||||
id BIGINT PRIMARY KEY,
|
||||
identifier_application_id BIGINT NOT NULL,
|
||||
institution_identifier VARCHAR(32) NOT NULL,
|
||||
full_institution_identifier CHAR(64) NOT NULL,
|
||||
amount DECIMAL(18, 2) NOT NULL,
|
||||
delivery_node_code VARCHAR(32) NOT NULL,
|
||||
request_timestamp CHAR(14) NOT NULL,
|
||||
request_original_text VARCHAR(256),
|
||||
request_digest CHAR(64),
|
||||
bank_signature VARCHAR(512),
|
||||
signing_key_id VARCHAR(64),
|
||||
request_message VARCHAR(2048),
|
||||
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 fk_currency_request_identifier_application
|
||||
FOREIGN KEY (identifier_application_id) REFERENCES institution_identifier_application(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_currency_request_identifier_application
|
||||
ON currency_generation_request(identifier_application_id, user_id, school_id, class_id, deleted);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_currency_request_status
|
||||
ON currency_generation_request(status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS currency_generation_request_operation_log (
|
||||
id BIGINT PRIMARY KEY,
|
||||
request_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,
|
||||
CONSTRAINT fk_currency_request_operation_request
|
||||
FOREIGN KEY (request_id) REFERENCES currency_generation_request(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_currency_request_operation_request
|
||||
ON currency_generation_request_operation_log(request_id, created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS currency_generation_verification (
|
||||
id BIGINT PRIMARY KEY,
|
||||
request_id BIGINT NOT NULL,
|
||||
request_message VARCHAR(2048) NOT NULL,
|
||||
request_original_text VARCHAR(256) NOT NULL,
|
||||
request_digest CHAR(64) NOT NULL,
|
||||
bank_signature VARCHAR(512) NOT NULL,
|
||||
bank_key_id VARCHAR(64) NOT NULL,
|
||||
amount DECIMAL(18, 2) NOT NULL,
|
||||
signature_valid BOOLEAN,
|
||||
recomputed_digest CHAR(64),
|
||||
digest_matches BOOLEAN,
|
||||
total_quota DECIMAL(18, 2),
|
||||
used_quota DECIMAL(18, 2),
|
||||
remaining_quota DECIMAL(18, 2),
|
||||
quota_sufficient BOOLEAN,
|
||||
central_bank_key_id VARCHAR(64),
|
||||
central_bank_signature VARCHAR(512),
|
||||
confirmation_time CHAR(14),
|
||||
confirmation_message VARCHAR(2048),
|
||||
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 fk_currency_verification_request
|
||||
FOREIGN KEY (request_id) REFERENCES currency_generation_request(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_currency_verification_subject
|
||||
ON currency_generation_verification(request_id, user_id, school_id, class_id, deleted);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS currency_generation_verification_operation_log (
|
||||
id BIGINT PRIMARY KEY,
|
||||
verification_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,
|
||||
CONSTRAINT fk_currency_verification_operation
|
||||
FOREIGN KEY (verification_id) REFERENCES currency_generation_verification(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_currency_verification_operation
|
||||
ON currency_generation_verification_operation_log(verification_id, created_at);
|
||||
@ -0,0 +1,58 @@
|
||||
package com.yau.digitalrmb.institutionidentity;
|
||||
|
||||
import com.yau.digitalrmb.institutionidentity.domain.CurrencyGenerationVerification;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class CurrencyGenerationVerificationDomainTest {
|
||||
@Test
|
||||
void completesAllStepThreeStatesInOrder() {
|
||||
CurrencyGenerationVerification value = value();
|
||||
assertThat(value.getStatus()).isEqualTo(CurrencyGenerationVerification.Status.RECEIVED);
|
||||
value.verifySignature(true);
|
||||
value.verifyDigest("A123456789012345678901234567890123456789012345678901234567890123", true);
|
||||
value.verifyQuota(new BigDecimal("500000000.00"), new BigDecimal("495000000.00"),
|
||||
new BigDecimal("5000000.00"));
|
||||
value.confirm("CENTRAL_FIRST_SM2_KEY", "3044AABB", "20260804210000", "{confirmed:true}");
|
||||
value.returnResponse();
|
||||
assertThat(value.getStatus()).isEqualTo(CurrencyGenerationVerification.Status.RESPONSE_RETURNED);
|
||||
assertThat(value.getSignatureValid()).isTrue();
|
||||
assertThat(value.getDigestMatches()).isTrue();
|
||||
assertThat(value.getQuotaSufficient()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSkippedAndRepeatedOperations() {
|
||||
CurrencyGenerationVerification value = value();
|
||||
assertThatThrownBy(() -> value.verifyDigest(
|
||||
"A123456789012345678901234567890123456789012345678901234567890123", true))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
value.verifySignature(true);
|
||||
assertThatThrownBy(() -> value.verifySignature(true)).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidSignatureDigestAndInsufficientQuotaWithoutAdvancing() {
|
||||
CurrencyGenerationVerification value = value();
|
||||
assertThatThrownBy(() -> value.verifySignature(false)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(value.getStatus()).isEqualTo(CurrencyGenerationVerification.Status.RECEIVED);
|
||||
value.verifySignature(true);
|
||||
assertThatThrownBy(() -> value.verifyDigest("BAD", false)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(value.getStatus()).isEqualTo(CurrencyGenerationVerification.Status.SIGNATURE_VERIFIED);
|
||||
value.verifyDigest("A123456789012345678901234567890123456789012345678901234567890123", true);
|
||||
assertThatThrownBy(() -> value.verifyQuota(new BigDecimal("500000000.00"),
|
||||
new BigDecimal("499999999.00"), new BigDecimal("1.00")))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(value.getStatus()).isEqualTo(CurrencyGenerationVerification.Status.DIGEST_VERIFIED);
|
||||
}
|
||||
|
||||
private CurrencyGenerationVerification value() {
|
||||
return CurrencyGenerationVerification.receive(1L, "message", "ORG_123456789ABC|1598.85|SYS_DC_002|20260804210000",
|
||||
"A123456789012345678901234567890123456789012345678901234567890123",
|
||||
"3044AABB", "BANK_SECOND_SM2_KEY", new BigDecimal("1598.85"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,331 @@
|
||||
package com.yau.digitalrmb.institutionidentity;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
|
||||
import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierOperationLogEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierOperationLogMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierTrainingErrorMapper;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionPrivateKeyCipher;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyEntity;
|
||||
import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionSm2KeyMapper;
|
||||
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.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
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 InstitutionIdentifierControllerTest {
|
||||
private static final String BASE_PATH = "/api/v1/institution-identifiers";
|
||||
private static final String USER_ID = "00000000-0000-0000-0000-000000009001";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private JwtTokenService jwtTokenService;
|
||||
|
||||
@Autowired
|
||||
private InstitutionIdentifierApplicationMapper applicationMapper;
|
||||
|
||||
@Autowired
|
||||
private InstitutionIdentifierOperationLogMapper operationLogMapper;
|
||||
|
||||
@Autowired
|
||||
private InstitutionIdentifierTrainingErrorMapper trainingErrorMapper;
|
||||
|
||||
@Autowired
|
||||
private InstitutionSm2KeyMapper keyMapper;
|
||||
|
||||
@Autowired
|
||||
private InstitutionPrivateKeyCipher privateKeyCipher;
|
||||
|
||||
@Autowired
|
||||
private InstitutionKeyService keyService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String authorization;
|
||||
private String keyAuthorization;
|
||||
private String tokenWithoutSubjectClaims;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
trainingErrorMapper.delete(null);
|
||||
operationLogMapper.delete(null);
|
||||
applicationMapper.delete(null);
|
||||
jdbcTemplate.update("DELETE FROM institution_sm2_key_audit");
|
||||
jdbcTemplate.update("DELETE FROM institution_sm2_key");
|
||||
keyAuthorization = "Bearer " + jwtTokenService.issueFor(USER_ID, 1001L, 2001L,
|
||||
"institution-key-tester", Collections.singleton("STUDENT")).accessToken();
|
||||
authorization = keyAuthorization;
|
||||
tokenWithoutSubjectClaims = "Bearer " + jwtTokenService.issueFor("institution-api-tester").accessToken();
|
||||
InstitutionKeySubject subject = new InstitutionKeySubject(USER_ID, 1001L, 2001L);
|
||||
keyService.commercialBankPublicKey(subject, "institution-key-tester");
|
||||
keyService.centralBankPublicKey(subject, "institution-key-tester");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bankAndCentralKeyEndpointsPersistEncryptedPrivateKeysForTokenSubject() throws Exception {
|
||||
String bankResponse = mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
|
||||
.header("Authorization", keyAuthorization))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.keyId").value(InstitutionKeyService.BANK_SECOND_KEY))
|
||||
.andExpect(jsonPath("$.data.owner").value("COMMERCIAL_BANK"))
|
||||
.andExpect(jsonPath("$.data.userId").value(USER_ID))
|
||||
.andExpect(jsonPath("$.data.schoolId").value(1001))
|
||||
.andExpect(jsonPath("$.data.classId").value(2001))
|
||||
.andExpect(jsonPath("$.data.publicKey").value(org.hamcrest.Matchers.matchesPattern("04[0-9A-F]{128}")))
|
||||
.andExpect(jsonPath("$.data.privateKey").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/keys/central-bank")
|
||||
.header("Authorization", keyAuthorization))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.keyId").value(InstitutionKeyService.CENTRAL_FIRST_KEY))
|
||||
.andExpect(jsonPath("$.data.owner").value("CENTRAL_BANK"))
|
||||
.andExpect(jsonPath("$.data.publicKey").value(org.hamcrest.Matchers.matchesPattern("04[0-9A-F]{128}")))
|
||||
.andExpect(jsonPath("$.data.privateKey").value(org.hamcrest.Matchers.matchesPattern("[0-9A-F]{64}")));
|
||||
|
||||
List<InstitutionSm2KeyEntity> keys = keyMapper.selectList(null);
|
||||
assertThat(keys).hasSize(2);
|
||||
String returnedPrivateKey = JsonPath.read(bankResponse, "$.data.privateKey");
|
||||
InstitutionSm2KeyEntity bankKey = keys.stream()
|
||||
.filter(key -> InstitutionKeyService.BANK_SECOND_KEY.equals(key.getKeyId()))
|
||||
.findFirst().orElseThrow(AssertionError::new);
|
||||
assertThat(bankKey.getEncryptedPrivateKey()).isNotEqualTo(returnedPrivateKey);
|
||||
assertThat(privateKeyCipher.decrypt(bankKey.getEncryptedPrivateKey())).isEqualTo(returnedPrivateKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
void logicalDeleteIsScopedToTokenSubjectAndAllowsRegeneration() throws Exception {
|
||||
String firstResponse = mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
|
||||
.header("Authorization", keyAuthorization))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
Number firstRecordId = JsonPath.read(firstResponse, "$.data.recordId");
|
||||
|
||||
mvc.perform(delete(BASE_PATH + "/keys/commercial-bank")
|
||||
.header("Authorization", keyAuthorization))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").doesNotExist());
|
||||
|
||||
Integer deleted = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM institution_sm2_key WHERE id = ? AND deleted = TRUE",
|
||||
Integer.class, firstRecordId.longValue());
|
||||
assertThat(deleted).isEqualTo(1);
|
||||
|
||||
String regeneratedResponse = mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
|
||||
.header("Authorization", keyAuthorization))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
Number regeneratedRecordId = JsonPath.read(regeneratedResponse, "$.data.recordId");
|
||||
assertThat(regeneratedRecordId.longValue()).isNotEqualTo(firstRecordId.longValue());
|
||||
assertThat(keyMapper.selectList(null)).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyEndpointRejectsTokenWithoutSchoolAndClassClaims() throws Exception {
|
||||
mvc.perform(post(BASE_PATH + "/keys/commercial-bank")
|
||||
.header("Authorization", tokenWithoutSubjectClaims))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.message").value("Token缺少userId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void protectedStepRejectsMissingJwt() throws Exception {
|
||||
mvc.perform(post(BASE_PATH + "/steps/prepare")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"bankCode\":\"BKCHCNBJ20001\"}"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.code").value("UNAUTHORIZED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void skippedStepReturnsBadRequestAndDoesNotChangePersistedState() throws Exception {
|
||||
long applicationId = prepare("BKCHCNBJ20002");
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/sign")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepRequest(applicationId, InstitutionKeyService.BANK_SECOND_KEY)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
|
||||
.andExpect(jsonPath("$.message")
|
||||
.value(org.hamcrest.Matchers.containsString("当前步骤不是商业银行签名,不能操作")));
|
||||
|
||||
InstitutionIdentifierApplicationEntity application = applicationMapper.selectById(applicationId);
|
||||
assertThat(application.getStatus()).isEqualTo("PREPARED");
|
||||
assertThat(application.getDigest()).isNull();
|
||||
assertThat(application.getBankSignature()).isNull();
|
||||
assertThat(application.getScoringCriteria()).isEqualTo(1);
|
||||
|
||||
List<InstitutionIdentifierTrainingErrorEntity> errors = trainingErrorMapper.selectList(null);
|
||||
assertThat(errors).filteredOn(error -> error.getApplicationId().equals(applicationId))
|
||||
.extracting(InstitutionIdentifierTrainingErrorEntity::getErrorSequence,
|
||||
InstitutionIdentifierTrainingErrorEntity::getErrorType)
|
||||
.containsExactly(org.assertj.core.groups.Tuple.tuple(1, "INVALID_STEP"));
|
||||
|
||||
List<InstitutionIdentifierOperationLogEntity> logs = operationLogMapper.selectList(null);
|
||||
assertThat(logs).filteredOn(log -> log.getApplicationId().equals(applicationId))
|
||||
.extracting(InstitutionIdentifierOperationLogEntity::getOperation)
|
||||
.containsExactly("PREPARE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrongKeysIncreaseScoringCriteriaAndRecordErrorSequence() throws Exception {
|
||||
long applicationId = prepare("BKCHCNBJ20004");
|
||||
mvc.perform(post(BASE_PATH + "/steps/digest")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepRequest(applicationId, null)))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/sign")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepRequest(applicationId, "WRONG_PRIVATE_KEY")))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第1次错误")));
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/sign")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepRequest(applicationId, "ANOTHER_WRONG_KEY")))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("第2次错误")));
|
||||
|
||||
InstitutionIdentifierApplicationEntity application = applicationMapper.selectById(applicationId);
|
||||
assertThat(application.getStatus()).isEqualTo("DIGESTED");
|
||||
assertThat(application.getScoringCriteria()).isEqualTo(2);
|
||||
assertThat(trainingErrorMapper.selectList(null))
|
||||
.filteredOn(error -> error.getApplicationId().equals(applicationId))
|
||||
.extracting(InstitutionIdentifierTrainingErrorEntity::getErrorSequence,
|
||||
InstitutionIdentifierTrainingErrorEntity::getProvidedKeyId)
|
||||
.containsExactly(
|
||||
org.assertj.core.groups.Tuple.tuple(1, "WRONG_PRIVATE_KEY"),
|
||||
org.assertj.core.groups.Tuple.tuple(2, "ANOTHER_WRONG_KEY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void prepareSameBankAgainCreatesNewTrainingRoundWithoutOverwritingHistory() throws Exception {
|
||||
long firstApplicationId = prepare("BKCHCNBJ20005");
|
||||
long secondApplicationId = prepare("BKCHCNBJ20005");
|
||||
|
||||
assertThat(secondApplicationId).isNotEqualTo(firstApplicationId);
|
||||
InstitutionIdentifierApplicationEntity first = applicationMapper.selectById(firstApplicationId);
|
||||
InstitutionIdentifierApplicationEntity second = applicationMapper.selectById(secondApplicationId);
|
||||
assertThat(first.getTrainingRound()).isEqualTo(1);
|
||||
assertThat(second.getTrainingRound()).isEqualTo(2);
|
||||
assertThat(first.getScoringCriteria()).isZero();
|
||||
assertThat(second.getScoringCriteria()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeApiWorkflowUsesApplicationIdAndPersistsIssuedResult() throws Exception {
|
||||
long applicationId = prepare("BKCHCNBJ20003");
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/digest")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepRequest(applicationId, null)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.applicationId").value(applicationId))
|
||||
.andExpect(jsonPath("$.data.status").value("DIGESTED"))
|
||||
.andExpect(jsonPath("$.data.algorithm").value("SM3"))
|
||||
.andExpect(jsonPath("$.data.digest").isNotEmpty());
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/sign")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepRequest(applicationId, InstitutionKeyService.BANK_SECOND_KEY)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("SIGNED"))
|
||||
.andExpect(jsonPath("$.data.signatureAlgorithm").value("SM2_WITH_SM3"))
|
||||
.andExpect(jsonPath("$.data.signatureHashAlgorithm").value("SM3"));
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/steps/package")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(stepRequest(applicationId, null)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("PACKAGED"));
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/applications/verification")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"applicationId\":" + applicationId + ",\"verificationKeyId\":\""
|
||||
+ InstitutionKeyService.BANK_SECOND_KEY + "\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("VERIFIED"))
|
||||
.andExpect(jsonPath("$.data.signatureValid").value(true))
|
||||
.andExpect(jsonPath("$.data.digestMatches").value(true));
|
||||
|
||||
mvc.perform(post(BASE_PATH + "/applications/confirmation")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"applicationId\":" + applicationId + ",\"signingKeyId\":\""
|
||||
+ InstitutionKeyService.CENTRAL_FIRST_KEY + "\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.status").value("ISSUED"))
|
||||
.andExpect(jsonPath("$.data.signatureAlgorithm").value("SM2_WITH_SM3"))
|
||||
.andExpect(jsonPath("$.data.institutionIdentifier").value(org.hamcrest.Matchers.startsWith("ORG_")));
|
||||
|
||||
InstitutionIdentifierApplicationEntity issued = applicationMapper.selectById(applicationId);
|
||||
assertThat(issued.getStatus()).isEqualTo("ISSUED");
|
||||
assertThat(issued.getDigest()).matches("[0-9A-F]{64}");
|
||||
assertThat(issued.getBankSignature()).isNotBlank();
|
||||
assertThat(issued.getCentralBankSignature()).isNotBlank();
|
||||
assertThat(issued.getInstitutionIdentifier()).startsWith("ORG_");
|
||||
|
||||
List<InstitutionIdentifierOperationLogEntity> logs = operationLogMapper.selectList(null);
|
||||
assertThat(logs).filteredOn(log -> log.getApplicationId().equals(applicationId))
|
||||
.extracting(InstitutionIdentifierOperationLogEntity::getOperation)
|
||||
.containsExactly("PREPARE", "DIGEST", "SIGN", "PACKAGE", "VERIFY", "CONFIRM");
|
||||
}
|
||||
|
||||
private long prepare(String bankCode) throws Exception {
|
||||
String response = mvc.perform(post(BASE_PATH + "/steps/prepare")
|
||||
.header("Authorization", authorization)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"bankCode\":\"" + bankCode + "\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.applicationId").isNumber())
|
||||
.andExpect(jsonPath("$.data.status").value("PREPARED"))
|
||||
.andExpect(jsonPath("$.data.bankCode").value(bankCode))
|
||||
.andExpect(jsonPath("$.data.trainingRound").isNumber())
|
||||
.andExpect(jsonPath("$.data.scoringCriteria").value(0))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
Number applicationId = JsonPath.read(response, "$.data.applicationId");
|
||||
return applicationId.longValue();
|
||||
}
|
||||
|
||||
private String stepRequest(long applicationId, String keyId) {
|
||||
if (keyId == null) {
|
||||
return "{\"applicationId\":" + applicationId + "}";
|
||||
}
|
||||
return "{\"applicationId\":" + applicationId + ",\"keyId\":\"" + keyId + "\"}";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
//package com.yau.digitalrmb.institutionidentity;
|
||||
//
|
||||
//import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationVerificationResult;
|
||||
//import com.yau.digitalrmb.institutionidentity.application.InstitutionApplicationVerificationService;
|
||||
//import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierConfirmationResult;
|
||||
//import com.yau.digitalrmb.institutionidentity.application.InstitutionIdentifierStepService;
|
||||
//import com.yau.digitalrmb.institutionidentity.application.InstitutionInformationPreparationResult;
|
||||
//import com.yau.digitalrmb.institutionidentity.application.InstitutionKeyService;
|
||||
//import com.yau.digitalrmb.institutionidentity.application.InstitutionKeySubject;
|
||||
//import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationEntity;
|
||||
//import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierApplicationMapper;
|
||||
//import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierOperationLogEntity;
|
||||
//import com.yau.digitalrmb.institutionidentity.infrastructure.InstitutionIdentifierOperationLogMapper;
|
||||
//import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.springframework.beans.factory.annotation.Autowired;
|
||||
//import org.springframework.boot.test.context.SpringBootTest;
|
||||
//import org.springframework.test.context.ActiveProfiles;
|
||||
//
|
||||
//import java.util.List;
|
||||
//
|
||||
//import static org.assertj.core.api.Assertions.assertThat;
|
||||
//import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
//
|
||||
//@SpringBootTest
|
||||
//@ActiveProfiles("test")
|
||||
//class InstitutionIdentifierWorkflowTest {
|
||||
// private static final InstitutionKeySubject SUBJECT = new InstitutionKeySubject(9101L, 1101L, 2101L);
|
||||
//
|
||||
// @Autowired
|
||||
// private InstitutionIdentifierStepService stepService;
|
||||
//
|
||||
// @Autowired
|
||||
// private InstitutionApplicationVerificationService verificationService;
|
||||
//
|
||||
// @Autowired
|
||||
// private InstitutionIdentifierApplicationMapper applicationMapper;
|
||||
//
|
||||
// @Autowired
|
||||
// private InstitutionIdentifierOperationLogMapper operationLogMapper;
|
||||
//
|
||||
// @Autowired
|
||||
// private InstitutionKeyService keyService;
|
||||
//
|
||||
// @Test
|
||||
// void everyStepIsPersistedAndCannotBeSkipped() {
|
||||
// keyService.commercialBankPublicKey(SUBJECT, "tester");
|
||||
// keyService.centralBankPublicKey(SUBJECT, "tester");
|
||||
// InstitutionInformationPreparationResult prepared = stepService.prepare("BKCHCNBJ10001", SUBJECT, "tester");
|
||||
// long applicationId = prepared.getApplicationId();
|
||||
// assertStatus(applicationId, "PREPARED");
|
||||
//
|
||||
// assertThatThrownBy(() -> stepService.sign(applicationId, InstitutionKeyService.BANK_SECOND_KEY,
|
||||
// SUBJECT, "tester"))
|
||||
// .isInstanceOf(BusinessException.class)
|
||||
// .hasMessageContaining("当前步骤不是商业银行签名");
|
||||
// assertStatus(applicationId, "PREPARED");
|
||||
//
|
||||
// stepService.digest(applicationId, SUBJECT, "tester");
|
||||
// assertStatus(applicationId, "DIGESTED");
|
||||
//
|
||||
// stepService.sign(applicationId, InstitutionKeyService.BANK_SECOND_KEY, SUBJECT, "tester");
|
||||
// assertStatus(applicationId, "SIGNED");
|
||||
//
|
||||
// stepService.packageApplication(applicationId, SUBJECT, "tester");
|
||||
// assertStatus(applicationId, "PACKAGED");
|
||||
//
|
||||
// InstitutionApplicationVerificationResult verification = verificationService.verify(applicationId,
|
||||
// InstitutionKeyService.BANK_SECOND_KEY, SUBJECT, "tester");
|
||||
// assertThat(verification.isVerified()).isTrue();
|
||||
// assertStatus(applicationId, "VERIFIED");
|
||||
//
|
||||
// InstitutionIdentifierConfirmationResult confirmation = verificationService.confirm(applicationId,
|
||||
// InstitutionKeyService.CENTRAL_FIRST_KEY, SUBJECT, "tester");
|
||||
// assertThat(confirmation.getStatus()).isEqualTo("ISSUED");
|
||||
// assertThat(confirmation.getInstitutionIdentifier()).startsWith("ORG_");
|
||||
//
|
||||
// InstitutionIdentifierApplicationEntity issued = applicationMapper.selectById(applicationId);
|
||||
// assertThat(issued.getStatus()).isEqualTo("ISSUED");
|
||||
// assertThat(issued.getDigest()).matches("[0-9A-F]{64}");
|
||||
// assertThat(issued.getBankSignature()).matches("[0-9A-F]+");
|
||||
// assertThat(issued.getSignatureValid()).isTrue();
|
||||
// assertThat(issued.getDigestMatches()).isTrue();
|
||||
// assertThat(issued.getCentralBankSignature()).matches("[0-9A-F]+");
|
||||
//
|
||||
// List<InstitutionIdentifierOperationLogEntity> logs = operationLogMapper.selectList(null);
|
||||
// assertThat(logs).filteredOn(log -> log.getApplicationId().equals(applicationId))
|
||||
// .extracting(InstitutionIdentifierOperationLogEntity::getOperation)
|
||||
// .containsExactly("PREPARE", "DIGEST", "SIGN", "PACKAGE", "VERIFY", "CONFIRM");
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void completedStepCannotBeRepeated() {
|
||||
// InstitutionInformationPreparationResult prepared = stepService.prepare("BKCHCNBJ10002", SUBJECT, "tester");
|
||||
// stepService.digest(prepared.getApplicationId(), SUBJECT, "tester");
|
||||
//
|
||||
// assertThatThrownBy(() -> stepService.digest(prepared.getApplicationId(), SUBJECT, "tester"))
|
||||
// .isInstanceOf(BusinessException.class)
|
||||
// .hasMessageContaining("当前步骤不是摘要计算");
|
||||
// assertStatus(prepared.getApplicationId(), "DIGESTED");
|
||||
// }
|
||||
//
|
||||
// private void assertStatus(long applicationId, String expectedStatus) {
|
||||
// assertThat(applicationMapper.selectById(applicationId).getStatus()).isEqualTo(expectedStatus);
|
||||
// }
|
||||
//}
|
||||
@ -0,0 +1,130 @@
|
||||
package com.yau.digitalrmb.security;
|
||||
|
||||
import com.yau.digitalrmb.security.context.AuthContextHolder;
|
||||
import com.yau.digitalrmb.security.context.JwtUser;
|
||||
import com.yau.digitalrmb.shared.api.ErrorCode;
|
||||
import com.yau.digitalrmb.shared.exception.BusinessException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class AuthContextHolderTest {
|
||||
private static final String USER_ID = "00000000-0000-0000-0000-000000000701";
|
||||
@AfterEach
|
||||
void clearSecurityContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsJwtUserFromCurrentSecurityContext() {
|
||||
authenticate(Jwt.withTokenValue("test-token")
|
||||
.header("alg", "none")
|
||||
.subject(USER_ID)
|
||||
.claim("userId", USER_ID)
|
||||
.claim("preferred_username", "sso701")
|
||||
.claim("studentId", "sso701")
|
||||
.claim("userName", "Test Student")
|
||||
.claim("className", "Class 1")
|
||||
.claim("phone", "13800000000")
|
||||
.claim("schoolName", "Yan'an University")
|
||||
.claim("schoolId", "610000")
|
||||
.claim("classId", "202401")
|
||||
.claim("authorizeTime", "2026-08-04")
|
||||
.claim("authorizeEndTime", "2027-08-04")
|
||||
.claim("roleId", 4)
|
||||
.claim("createTime", "2026-08-04T08:00:00")
|
||||
.claim("isDeleted", false)
|
||||
.claim("zyUserId", "701")
|
||||
.claim("roles", Arrays.asList("ROLE_STUDENT", "ROLE_USER"))
|
||||
.issuedAt(Instant.parse("2026-08-04T00:00:00Z"))
|
||||
.expiresAt(Instant.parse("2026-08-04T01:00:00Z"))
|
||||
.build());
|
||||
|
||||
JwtUser jwtUser = AuthContextHolder.get();
|
||||
|
||||
assertThat(jwtUser.getUserId()).isEqualTo(USER_ID);
|
||||
assertThat(jwtUser.getUsername()).isEqualTo("sso701");
|
||||
assertThat(jwtUser.getUserName()).isEqualTo("Test Student");
|
||||
assertThat(jwtUser.getSchoolId()).isEqualTo("610000");
|
||||
assertThat(jwtUser.getSchoolName()).isEqualTo("Yan'an University");
|
||||
assertThat(jwtUser.getClassId()).isEqualTo("202401");
|
||||
assertThat(jwtUser.getClassName()).isEqualTo("Class 1");
|
||||
assertThat(jwtUser.getPhone()).isEqualTo("13800000000");
|
||||
assertThat(jwtUser.getAuthorizeTime()).isEqualTo("2026-08-04");
|
||||
assertThat(jwtUser.getAuthorizeEndTime()).isEqualTo("2027-08-04");
|
||||
assertThat(jwtUser.getRoleId()).isEqualTo(4);
|
||||
assertThat(jwtUser.getCreateTime()).isEqualTo("2026-08-04T08:00:00");
|
||||
assertThat(jwtUser.getIsDeleted()).isFalse();
|
||||
assertThat(jwtUser.getZyUserId()).isEqualTo("701");
|
||||
assertThat(jwtUser.getRoles()).containsExactly("ROLE_STUDENT", "ROLE_USER");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToSubjectWhenUserIdClaimIsAbsent() {
|
||||
authenticate(Jwt.withTokenValue("test-token")
|
||||
.header("alg", "none")
|
||||
.subject("00000000-0000-0000-0000-000000000702")
|
||||
.issuedAt(Instant.parse("2026-08-04T00:00:00Z"))
|
||||
.expiresAt(Instant.parse("2026-08-04T01:00:00Z"))
|
||||
.build());
|
||||
|
||||
JwtUser jwtUser = AuthContextHolder.get();
|
||||
|
||||
assertThat(jwtUser.getUserId()).isEqualTo("00000000-0000-0000-0000-000000000702");
|
||||
assertThat(jwtUser.getUsername()).isEqualTo("00000000-0000-0000-0000-000000000702");
|
||||
assertThat(jwtUser.getSchoolId()).isNull();
|
||||
assertThat(jwtUser.getRoles()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToLocalUuidSubjectWhenUserIdClaimContainsPlatformId() {
|
||||
authenticate(Jwt.withTokenValue("test-token")
|
||||
.header("alg", "none")
|
||||
.subject("00000000-0000-0000-0000-000000000703")
|
||||
.claim("userId", "701")
|
||||
.claim("zyUserId", "701")
|
||||
.issuedAt(Instant.parse("2026-08-04T00:00:00Z"))
|
||||
.expiresAt(Instant.parse("2026-08-04T01:00:00Z"))
|
||||
.build());
|
||||
|
||||
JwtUser jwtUser = AuthContextHolder.get();
|
||||
|
||||
assertThat(jwtUser.getUserId()).isEqualTo("00000000-0000-0000-0000-000000000703");
|
||||
assertThat(jwtUser.getZyUserId()).isEqualTo("701");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnauthenticatedAccess() {
|
||||
assertThatThrownBy(AuthContextHolder::get)
|
||||
.isInstanceOfSatisfying(BusinessException.class,
|
||||
exception -> assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMalformedRoleIdClaim() {
|
||||
authenticate(Jwt.withTokenValue("test-token")
|
||||
.header("alg", "none")
|
||||
.subject(USER_ID)
|
||||
.claim("roleId", "not-a-number")
|
||||
.issuedAt(Instant.parse("2026-08-04T00:00:00Z"))
|
||||
.expiresAt(Instant.parse("2026-08-04T01:00:00Z"))
|
||||
.build());
|
||||
|
||||
assertThatThrownBy(AuthContextHolder::get)
|
||||
.isInstanceOfSatisfying(BusinessException.class,
|
||||
exception -> assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED))
|
||||
.hasMessage("Token中的roleId格式不正确");
|
||||
}
|
||||
|
||||
private void authenticate(Jwt jwt) {
|
||||
SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
package com.yau.digitalrmb.security;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
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 javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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 UserControllerTest {
|
||||
private static final String SECRET = "local-token-sso-test-secret-key-123456";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Test
|
||||
void tokenLoginSynchronizesCrossPlatformUserAndReturnsLocalToken() throws Exception {
|
||||
String response = mvc.perform(post("/api/user/login")
|
||||
.param("TOKEN", crossPlatformToken()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty())
|
||||
.andExpect(jsonPath("$.data.tokenType").value("Bearer"))
|
||||
.andExpect(jsonPath("$.data.username").value("cross001"))
|
||||
.andExpect(jsonPath("$.data.studentId").value("cross001"))
|
||||
.andExpect(jsonPath("$.data.name").value("Cross Student"))
|
||||
.andExpect(jsonPath("$.data.roleId").value(4))
|
||||
.andExpect(jsonPath("$.data.schoolId").value("610000"))
|
||||
.andExpect(jsonPath("$.data.schoolName").value("Yan'an University"))
|
||||
.andExpect(jsonPath("$.data.classId").value("202401"))
|
||||
.andExpect(jsonPath("$.data.className").value("Class 1"))
|
||||
.andExpect(jsonPath("$.data.collegeId").value("100"))
|
||||
.andExpect(jsonPath("$.data.majorId").value("101"))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
String localUserId = JsonPath.read(response, "$.data.userId");
|
||||
assertThat(UUID.fromString(localUserId).toString()).isEqualTo(localUserId);
|
||||
assertThat(jdbcTemplate.queryForObject(
|
||||
"SELECT zy_user_id FROM sys_user WHERE user_id = ?", String.class, localUserId))
|
||||
.isEqualTo("9001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parameterLoginMatchesCrossTrainingPlatformEndpointShape() throws Exception {
|
||||
mvc.perform(post("/api/user/login")
|
||||
.param("username", "tzs001")
|
||||
.param("passwordEncode", "123qwe"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.userId")
|
||||
.value("00000000-0000-0000-0000-000000000487"))
|
||||
.andExpect(jsonPath("$.data.username").value("tzs001"))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyPlaintextPasswordLoginUpgradesPasswordToBcrypt() throws Exception {
|
||||
String userId = "00000000-0000-0000-0000-000000000221";
|
||||
jdbcTemplate.update("INSERT INTO sys_user "
|
||||
+ "(user_id, student_id, password, user_name, role_id, is_deleted) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?)",
|
||||
userId, "legacy001", "123qwe", "Legacy Student", 4, 0);
|
||||
|
||||
mvc.perform(post("/api/user/login")
|
||||
.param("username", "legacy001")
|
||||
.param("passwordEncode", "123qwe"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.userId").value(userId))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty());
|
||||
|
||||
String upgradedPassword = jdbcTemplate.queryForObject(
|
||||
"SELECT password FROM sys_user WHERE user_id = ?", String.class, userId);
|
||||
assertThat(upgradedPassword).startsWith("$2").isNotEqualTo("123qwe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tokenLoginAcceptsLegacyTokenWithoutExpirationOrPassword() throws Exception {
|
||||
Map<String, Object> claims = baseClaims();
|
||||
claims.remove("password");
|
||||
claims.remove("exp");
|
||||
|
||||
mvc.perform(post("/api/user/login").param("TOKEN", signedToken(claims)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.username").value("cross001"))
|
||||
.andExpect(jsonPath("$.data.accessToken").isNotEmpty());
|
||||
}
|
||||
|
||||
private String crossPlatformToken() throws Exception {
|
||||
return signedToken(baseClaims());
|
||||
}
|
||||
|
||||
private Map<String, Object> baseClaims() {
|
||||
Map<String, Object> claims = new HashMap<String, Object>();
|
||||
claims.put("userId", 9001);
|
||||
claims.put("username", "cross001");
|
||||
claims.put("password", "token-password");
|
||||
claims.put("name", "Cross Student");
|
||||
claims.put("roleid", 4);
|
||||
claims.put("schoolId", 610000);
|
||||
claims.put("school", "Yan'an University");
|
||||
claims.put("collegeId", 100);
|
||||
claims.put("collegeName", "Computer College");
|
||||
claims.put("majorId", 101);
|
||||
claims.put("major", "Software Engineering");
|
||||
claims.put("classId", 202401);
|
||||
claims.put("class", "Class 1");
|
||||
claims.put("studentNo", "cross001");
|
||||
claims.put("exp", Instant.now().plusSeconds(300).getEpochSecond());
|
||||
return claims;
|
||||
}
|
||||
|
||||
private String signedToken(Map<String, Object> claims) throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, Object> header = new HashMap<String, Object>();
|
||||
header.put("alg", "HS256");
|
||||
header.put("typ", "JWT");
|
||||
String unsigned = encode(mapper.writeValueAsBytes(header))
|
||||
+ "." + encode(mapper.writeValueAsBytes(claims));
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
return unsigned + "." + encode(mac.doFinal(unsigned.getBytes(StandardCharsets.US_ASCII)));
|
||||
}
|
||||
|
||||
private String encode(byte[] value) {
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:digital_rmb_workflow;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE
|
||||
username: sa
|
||||
password: ""
|
||||
driver-class-name: org.h2.Driver
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
schema-locations: classpath:schema.sql,classpath:db/manual/institution_identifier_training.sql
|
||||
security:
|
||||
jwt:
|
||||
secret: 0123456789012345678901234567890123456789012345678901234567890123
|
||||
access-token-ttl: PT30M
|
||||
platform-integration:
|
||||
enabled: true
|
||||
datasource:
|
||||
url: jdbc:h2:mem:platform_workflow;MODE=MySQL;DB_CLOSE_DELAY=-1;DATABASE_TO_LOWER=TRUE
|
||||
username: sa
|
||||
password: test-password
|
||||
token:
|
||||
link-secret-key: local-token-sso-test-secret-key-123456
|
||||
max-age: PT2M
|
||||
teacher-claim-value: teacher
|
||||
student-claim-value: student
|
||||
cas:
|
||||
login-url: https://sso.example.edu/login
|
||||
validate-url: https://sso.example.edu/p3/serviceValidate
|
||||
callback-url: https://rmb.example.edu/api/v1/auth/cas/callback
|
||||
frontend:
|
||||
callback-url: https://rmb.example.edu/sso-callback
|
||||
@ -0,0 +1,168 @@
|
||||
CREATE TABLE IF NOT EXISTS sys_user (
|
||||
user_id VARCHAR(36) PRIMARY KEY,
|
||||
student_id VARCHAR(255) NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
user_name VARCHAR(100) NULL,
|
||||
class_id VARCHAR(36) NULL,
|
||||
class_name VARCHAR(100) NULL,
|
||||
phone VARCHAR(100) NULL,
|
||||
school_name VARCHAR(100) NULL,
|
||||
school_id VARCHAR(36) NULL,
|
||||
authorize_time DATE NULL,
|
||||
authorize_end_time DATE NULL,
|
||||
role_id TINYINT NULL,
|
||||
create_time DATETIME NULL,
|
||||
is_deleted TINYINT NOT NULL DEFAULT 0,
|
||||
zy_user_id VARCHAR(50) NULL,
|
||||
UNIQUE KEY uk_sys_user_student_id (student_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS platform_user_snapshot (
|
||||
platform_user_id VARCHAR(36) PRIMARY KEY,
|
||||
account VARCHAR(64) NOT NULL,
|
||||
display_name VARCHAR(64) NOT NULL,
|
||||
source_updated_at TIMESTAMP NOT NULL,
|
||||
synced_at TIMESTAMP NOT NULL,
|
||||
school_id VARCHAR(64) NULL,
|
||||
school_name VARCHAR(128) NULL,
|
||||
college_id VARCHAR(64) NULL,
|
||||
college_name VARCHAR(128) NULL,
|
||||
major_id VARCHAR(64) NULL,
|
||||
major_name VARCHAR(128) NULL,
|
||||
class_id VARCHAR(64) NULL,
|
||||
class_name VARCHAR(128) NULL,
|
||||
student_id VARCHAR(64) NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_login_exchange_code (
|
||||
code_hash CHAR(64) PRIMARY KEY,
|
||||
platform_user_id VARCHAR(36) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
consumed_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_refresh_token (
|
||||
token_hash CHAR(64) PRIMARY KEY,
|
||||
platform_user_id VARCHAR(36) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
revoked_at TIMESTAMP NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS institution_sm2_key (
|
||||
id BIGINT PRIMARY KEY,
|
||||
key_id VARCHAR(64) NOT NULL,
|
||||
key_name VARCHAR(64) NOT NULL,
|
||||
key_owner VARCHAR(32) NOT NULL,
|
||||
key_purpose VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
school_id BIGINT NOT NULL,
|
||||
class_id BIGINT NOT NULL,
|
||||
public_key VARCHAR(256) NOT NULL,
|
||||
encrypted_private_key VARCHAR(512) 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
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS institution_sm2_key_audit (
|
||||
id BIGINT PRIMARY KEY,
|
||||
key_record_id BIGINT NOT NULL,
|
||||
key_id VARCHAR(64) NOT NULL,
|
||||
operation VARCHAR(32) NOT NULL,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
school_id BIGINT NOT NULL,
|
||||
class_id BIGINT NOT NULL,
|
||||
operation_detail VARCHAR(512) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
CONSTRAINT fk_institution_key_audit_key
|
||||
FOREIGN KEY (key_record_id) REFERENCES institution_sm2_key(id)
|
||||
);
|
||||
|
||||
INSERT INTO sys_user (
|
||||
user_id, student_id, password, user_name, role_id, is_deleted, zy_user_id
|
||||
)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000487',
|
||||
'tzs001',
|
||||
'$2a$10$ufcw5KFHtOmLzxAsV4C.MuIDOErMlw0iw5J5hc8OMzaTx0u9QYxG6',
|
||||
'tzs001',
|
||||
4,
|
||||
0,
|
||||
'487'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
student_id = VALUES(student_id),
|
||||
password = VALUES(password),
|
||||
user_name = VALUES(user_name),
|
||||
role_id = VALUES(role_id),
|
||||
is_deleted = 0;
|
||||
|
||||
INSERT INTO platform_user_snapshot (
|
||||
platform_user_id, account, display_name, source_updated_at, synced_at
|
||||
)
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000487',
|
||||
'tzs001',
|
||||
'tzs001',
|
||||
CURRENT_TIMESTAMP,
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
account = VALUES(account),
|
||||
display_name = VALUES(display_name),
|
||||
source_updated_at = CURRENT_TIMESTAMP,
|
||||
synced_at = CURRENT_TIMESTAMP;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issuance_bank_inventory (
|
||||
bank_code VARCHAR(32) PRIMARY KEY,
|
||||
current_balance DECIMAL(20, 2) NOT NULL,
|
||||
warning_threshold DECIMAL(20, 2) NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issuance_request (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
request_no VARCHAR(64) NOT NULL UNIQUE,
|
||||
bank_code VARCHAR(32) NOT NULL,
|
||||
organization_id VARCHAR(64) NOT NULL,
|
||||
total_amount DECIMAL(20, 2) NOT NULL,
|
||||
currency VARCHAR(16) NOT NULL,
|
||||
request_timestamp VARCHAR(32),
|
||||
message_text TEXT,
|
||||
digest CHAR(64),
|
||||
signature TEXT,
|
||||
signing_key_ref VARCHAR(64),
|
||||
payload_json TEXT,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
central_receive_status VARCHAR(32) NOT NULL,
|
||||
central_received_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
updated_by VARCHAR(64) NOT NULL,
|
||||
created_by_user_id BIGINT NOT NULL,
|
||||
updated_by_user_id BIGINT NOT NULL,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS issuance_request_denomination (
|
||||
request_id CHAR(36) NOT NULL,
|
||||
denomination DECIMAL(10, 2) NOT NULL,
|
||||
quantity INT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
created_by VARCHAR(64) NOT NULL,
|
||||
updated_by VARCHAR(64) NOT NULL,
|
||||
created_by_user_id BIGINT NOT NULL,
|
||||
updated_by_user_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (request_id, denomination)
|
||||
);
|
||||
|
||||
INSERT INTO issuance_bank_inventory (bank_code, current_balance, warning_threshold, updated_at)
|
||||
VALUES ('BKCHCNBJ00001', 49950000.00, 50000000.00, CURRENT_TIMESTAMP)
|
||||
ON DUPLICATE KEY UPDATE current_balance = VALUES(current_balance),
|
||||
warning_threshold = VALUES(warning_threshold),
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
Loading…
Reference in New Issue