feat:crl管理、错误信息中文化

This commit is contained in:
waner
2026-05-06 09:28:51 +08:00
parent d41e0790bd
commit 61280718dc
119 changed files with 2055 additions and 685 deletions
+6 -1
View File
@@ -93,6 +93,7 @@ Open:
- Upgrade execute API: `POST /api/v1/upgrades/{taskId}/execute`
- Upgrade list API: `GET /api/v1/upgrades`
- CRL import API: `POST /api/v1/crls/import`
- CRL import task detail API: `POST /api/v1/crls/import-tasks/detail`
- CRL revoked list API: `POST /api/v1/crls/list`
- CRL delete API: `POST /api/v1/crls/delete`
- PCIe crypto count API: `GET /api/v1/device/crypto/device-count`
@@ -100,13 +101,17 @@ Open:
## Certificate CRL Management
前后端对接文档:
- [docs/openapi/certificate-frontend-integration.md](/Users/waner/Work/CISD/文档/tms-framework/docs/openapi/certificate-frontend-integration.md)
最小调用顺序:
- `POST /api/v1/crls/import`
- `POST /api/v1/crls/list`
- `POST /api/v1/crls/delete`
当前实现边界:
- CRL 文件支持 PEM / DER 格式,导入后保存 CRL metadata 和 CRL 内每条 revoked certificate 明细。
- CRL 文件支持 PEM / DER 格式,导入接口会先创建后台任务并返回 `taskId`,前端通过 `POST /api/v1/crls/import-tasks/detail` 轮询 `PENDING/RUNNING/SUCCESS/FAILED` 状态。
- 后台导入完成后保存 CRL metadata 和 CRL 内每条 revoked certificate 明细;原始 CRL 文件只作为临时文件参与解析,任务结束后删除,不作为业务数据长期保存。
- CRL issuer 通过可信 CA 的 issuer DN 候选和实际 CRL 签名验证解析。
- CRL issuer 必须是可信 CA,且 KeyUsage 必须允许 `cRLSign`。
- 重复 CRL 通过 SHA-256 fingerprint 拒绝。
@@ -9,7 +9,7 @@ import org.slf4j.MDC;
public class ApiResponse<T> {
private static final int SUCCESS_CODE = 200;
private static final String DEFAULT_SUCCESS_MSG = "success";
private static final String DEFAULT_SUCCESS_MSG = "成功";
private boolean success;
private int code;
@@ -1,16 +1,16 @@
package com.cisd.tms.common.enums;
public enum ErrorCode {
BAD_REQUEST(400, "bad request"),
UNAUTHORIZED(4010, "unauthorized"),
SESSION_INVALID(4011, "session invalid"),
FORBIDDEN(4030, "forbidden"),
VALIDATE_FAILED(4001, "parameter validation failed"),
CONFLICT(4090, "conflict"),
BIZ_ERROR(5001, "business exception"),
CRYPTO_CARD_ERROR(5201, "crypto card adapter error"),
SERVICE_UNAVAILABLE(5030, "service unavailable"),
INTERNAL_ERROR(5000, "internal server error");
BAD_REQUEST(400, "请求错误"),
UNAUTHORIZED(4010, "未认证或认证失败"),
SESSION_INVALID(4011, "会话无效"),
FORBIDDEN(4030, "无权限访问"),
VALIDATE_FAILED(4001, "参数校验失败"),
CONFLICT(4090, "数据冲突"),
BIZ_ERROR(5001, "业务处理异常"),
CRYPTO_CARD_ERROR(5201, "密码卡适配异常"),
SERVICE_UNAVAILABLE(5030, "服务暂不可用"),
INTERNAL_ERROR(5000, "服务器内部错误");
private final int code;
private final String message;
@@ -55,7 +55,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ApiResponse<Void>> handleMaxUploadSizeExceeded(MaxUploadSizeExceededException ex, HttpServletRequest request) {
return response(HttpStatus.BAD_REQUEST, ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), "upload file size exceeds limit")
return response(HttpStatus.BAD_REQUEST, ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), "上传文件大小超过限制")
.withPath(request.getRequestURI()));
}
@@ -73,7 +73,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(ReplayProtectionException.class)
public ResponseEntity<ApiResponse<Void>> handleReplayProtectionException(ReplayProtectionException ex, HttpServletRequest request) {
log.warn("Replay protection failed", ex);
log.warn("Replay protection 执行失败", ex);
return response(HttpStatus.SERVICE_UNAVAILABLE,
ApiResponse.fail(ErrorCode.SERVICE_UNAVAILABLE.getCode(), "replay protection unavailable")
.withPath(request.getRequestURI()));
@@ -16,7 +16,7 @@ public final class HmacUtil {
byte[] digest = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return toHex(digest);
} catch (Exception ex) {
throw new IllegalStateException("Cannot calculate hmac", ex);
throw new IllegalStateException("HMAC计算失败", ex);
}
}
@@ -26,13 +26,13 @@ public final class Sm2SignatureUtil {
public static boolean verifyBase64Signature(String publicKey, String data, String signatureBase64) {
if (publicKey == null || publicKey.isBlank()) {
throw new IllegalArgumentException("public key is required");
throw new IllegalArgumentException("public key不能为空");
}
if (data == null) {
throw new IllegalArgumentException("data is required");
throw new IllegalArgumentException("data不能为空");
}
if (signatureBase64 == null || signatureBase64.isBlank()) {
throw new IllegalArgumentException("signature is required");
throw new IllegalArgumentException("signature不能为空");
}
try {
PublicKey key = parsePublicKey(publicKey);
@@ -42,7 +42,7 @@ public final class Sm2SignatureUtil {
byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64.trim());
return signature.verify(signatureBytes);
} catch (Exception e) {
throw new IllegalArgumentException("sm2 verify failed", e);
throw new IllegalArgumentException("SM2验签失败", e);
}
}
@@ -11,9 +11,9 @@ public class PcieErrorMapper {
public PcieErrorMapper() {
// From PCIe custom document (SDR_BASE + 0x1F ~ 0x27)
lowByteErrorMap.put(0x1F, "ukey error");
lowByteErrorMap.put(0x20, "generate key error");
lowByteErrorMap.put(0x21, "state error");
lowByteErrorMap.put(0x1F, "UKey错误");
lowByteErrorMap.put(0x20, "生成密钥错误");
lowByteErrorMap.put(0x21, "状态错误");
lowByteErrorMap.put(0x22, "retry exceeded");
lowByteErrorMap.put(0x23, "device busy");
lowByteErrorMap.put(0x24, "error status");
@@ -25,7 +25,7 @@ public class PcieErrorMapper {
public String toMessage(int retCode) {
if (retCode == 0) {
return "success";
return "成功";
}
int lowByte = retCode & 0xFF;
@@ -34,6 +34,6 @@ public class PcieErrorMapper {
return known + " (retCode=0x" + Integer.toHexString(retCode) + ")";
}
return "native error (retCode=0x" + Integer.toHexString(retCode) + ")";
return "原生接口错误(retCode=0x" + Integer.toHexString(retCode) + ")";
}
}
@@ -70,7 +70,7 @@ public class PcieSessionTemplate {
} finally {
int closeSessionRc = lib.SDF_CloseSession(session);
if (closeSessionRc != 0) {
log.warn("{} close session failed: {}", apiName, errorMapper.toMessage(closeSessionRc));
log.warn("{} close session 执行失败:{}", apiName, errorMapper.toMessage(closeSessionRc));
}
}
}
@@ -136,7 +136,7 @@ public class PcieSessionTemplate {
log.info("Loaded PCIe native library by name: {}", libName);
}
} catch (UnsatisfiedLinkError ex) {
throw new PcieCryptoException("Native.load", "load PCIe native library failed", ex);
throw new PcieCryptoException("Native.load", "加载PCIe原生库失败", ex);
}
return nativeLibrary;
}
@@ -161,7 +161,7 @@ public class PcieSessionTemplate {
if (openSessionRc == 0) {
return sessionRef.getValue();
}
log.warn("{} open session failed, will reset shared device and retry once: {}", apiName, errorMapper.toMessage(openSessionRc));
log.warn("{} open session 执行失败, will reset shared device and retry once: {}", apiName, errorMapper.toMessage(openSessionRc));
return null;
}
@@ -178,7 +178,7 @@ public class PcieSessionTemplate {
}
int closeSessionRc = lib.SDF_CloseSession(sessionHandle);
if (closeSessionRc != 0) {
log.warn("{} close session failed: {}", apiName, errorMapper.toMessage(closeSessionRc));
log.warn("{} close session 执行失败:{}", apiName, errorMapper.toMessage(closeSessionRc));
}
}
@@ -195,7 +195,7 @@ public class PcieSessionTemplate {
sharedDeviceHandle = null;
int closeRc = lib.SDF_CloseDevice(device);
if (closeRc != 0) {
log.warn("close shared device failed: {}", errorMapper.toMessage(closeRc));
log.warn("close shared device 执行失败:{}", errorMapper.toMessage(closeRc));
}
}
@@ -148,7 +148,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
int retCode = lib.SDFE_CheckLMK(sessionHandle);
if (retCode != 0) {
if ((retCode & 0xFF) == SDFE_INIT_STATUS_LOW_BYTE) {
throw new IllegalStateException("master key initialization failed");
throw new IllegalStateException("主密钥初始化失败");
}
sessionTemplate.ensureSuccess("SDFE_CheckLMK", retCode);
}
@@ -260,7 +260,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
GenerateKeyPairRequest req = requireRequest("request", request);
int keyBits = req.getKeyBits();
if (keyBits != ECC_KEY_BITS) {
throw new IllegalArgumentException("keyBits must be 256 for ECC");
throw new IllegalArgumentException("ECC密钥位数必须为256");
}
int keyIndex = requireNonNegative("keyIndex", req.getKeyIndex());
@@ -331,7 +331,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
GenerateKekRequest req = requireRequest("request", request);
int keyBits = req.getKeyBits();
if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
throw new IllegalArgumentException("keyBits must be one of 128/192/256 for KEK");
throw new IllegalArgumentException("KEK密钥位数必须为128/192/256之一");
}
int keyIndex = requireNonNegative("keyIndex", req.getKeyIndex());
@@ -388,7 +388,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
SdfeKekComponent output = new SdfeKekComponent();
int structSize = output.size();
if (componentSize < structSize) {
throw new IllegalArgumentException("componentSize must be >= " + structSize);
throw new IllegalArgumentException("componentSize必须大于等于" + structSize);
}
sessionTemplate.ensureSuccess(
"SDFE_ExportKEKComponent",
@@ -424,7 +424,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
byte[] encryptedKey = requireNonEmptyBytes("encryptedKey", req.getEncryptedKey());
int storeFlag = req.getStoreFlag();
if (storeFlag != 0 && storeFlag != 1) {
throw new IllegalArgumentException("storeFlag must be 0 or 1");
throw new IllegalArgumentException("storeFlag必须为0或1");
}
try {
@@ -1140,7 +1140,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
Pointer agreementHandle = agreementHandleRef.getValue();
if (agreementHandle == null) {
throw new PcieCryptoException("SDF_GenerateAgreementDataWithECC", -1, "native returned null agreement handle");
throw new PcieCryptoException("SDF_GenerateAgreementDataWithECC", -1, "原生接口返回空协商句柄");
}
PointerByReference keyHandleRef = new PointerByReference();
@@ -1587,7 +1587,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
EccCipher cipher = toEccCipher("cipherBlob", cipherBlob);
int outBufferSize = cipher.l;
if (outBufferSize <= 0) {
throw new IllegalArgumentException("cipherBlob contains invalid plaintext length: " + outBufferSize);
throw new IllegalArgumentException("cipherBlob明文长度无效:" + outBufferSize);
}
EccInternalDecryptRequest decryptRequest = new EccInternalDecryptRequest();
@@ -1665,7 +1665,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
int length = outLen.getValue();
if (length <= 0 || length > out.length) {
throw new PcieCryptoException("SDFE_HmacFinal", -1, "native returned invalid HMAC length: " + length);
throw new PcieCryptoException("SDFE_HmacFinal", -1, "原生接口返回的HMAC长度无效:" + length);
}
HmacResult result = new HmacResult();
@@ -1948,13 +1948,13 @@ public class JnaPcieCryptoService implements PcieCryptoService {
}
int destroyCode = lib.SDF_DestroyKey(sessionHandle, keyHandle);
if (destroyCode != 0) {
throw new PcieCryptoException("SDF_DestroyKey", destroyCode, "destroy key failed");
throw new PcieCryptoException("SDF_DestroyKey", destroyCode, "销毁密钥失败");
}
}
private static byte[] fileNameBytes(String fileName) {
if (fileName == null || fileName.isBlank()) {
throw new IllegalArgumentException("fileName must not be blank");
throw new IllegalArgumentException("fileName不能为空");
}
return fileName.getBytes(StandardCharsets.UTF_8);
}
@@ -1978,7 +1978,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
private static BackupDataResult toBackupResult(String apiName, byte[] out, int outLen) {
if (outLen < 0 || outLen > out.length) {
throw new PcieCryptoException(apiName, -1, "native returned invalid output length: " + outLen);
throw new PcieCryptoException(apiName, -1, "原生接口返回的输出长度无效:" + outLen);
}
BackupDataResult result = new BackupDataResult();
result.setLength(outLen);
@@ -1996,7 +1996,7 @@ public class JnaPcieCryptoService implements PcieCryptoService {
private static int requireUserKeyType(int keyType) {
if (keyType != PcieUserKeyType.SIGN.getCode() && keyType != PcieUserKeyType.ENC.getCode()) {
throw new IllegalArgumentException("keyType must be 0(SIGN) or 1(ENC)");
throw new IllegalArgumentException("keyType必须为0(SIGN)或1(ENC)");
}
return keyType;
}
@@ -2099,11 +2099,11 @@ public class JnaPcieCryptoService implements PcieCryptoService {
cipher.read();
int cLength = cipher.l;
if (cLength < 0 || cLength > EccCipher.C_MAX_LEN) {
throw new PcieCryptoException(apiName, -1, "native returned invalid ECC cipher C length: " + cLength);
throw new PcieCryptoException(apiName, -1, "原生接口返回的ECC密文C长度无效:" + cLength);
}
int totalLength = ECC_CIPHER_PREFIX_LENGTH + cLength;
if (totalLength < 0 || totalLength > cipher.size()) {
throw new PcieCryptoException(apiName, -1, "native returned invalid ECC cipher blob length: " + totalLength);
throw new PcieCryptoException(apiName, -1, "原生接口返回的ECC密文结构长度无效:" + totalLength);
}
return cipher.getPointer().getByteArray(0, totalLength);
}
@@ -124,7 +124,7 @@ public class MockPcieCryptoService implements PcieCryptoService {
generateIk(1);
generateIk(2);
if (!checkLmk()) {
throw new IllegalStateException("master key initialization failed");
throw new IllegalStateException("主密钥初始化失败");
}
return exportLmkSeedMac();
}
@@ -172,7 +172,7 @@ public class MockPcieCryptoService implements PcieCryptoService {
public void generateKeyPairEcc(GenerateKeyPairRequest request) {
GenerateKeyPairRequest req = requireRequest("request", request);
if (req.getKeyBits() != ECC_KEY_BITS) {
throw new IllegalArgumentException("keyBits must be 256 for ECC");
throw new IllegalArgumentException("ECC密钥位数必须为256");
}
requireNonNegative("keyIndex", req.getKeyIndex());
requireUserKeyType(req.getKeyType());
@@ -201,7 +201,7 @@ public class MockPcieCryptoService implements PcieCryptoService {
GenerateKekRequest req = requireRequest("request", request);
int keyBits = req.getKeyBits();
if (keyBits != 128 && keyBits != 192 && keyBits != 256) {
throw new IllegalArgumentException("keyBits must be one of 128/192/256 for KEK");
throw new IllegalArgumentException("KEK密钥位数必须为128/192/256之一");
}
requireNonNegative("keyIndex", req.getKeyIndex());
}
@@ -246,7 +246,7 @@ public class MockPcieCryptoService implements PcieCryptoService {
requireNonEmptyBytes("encryptedKey", req.getEncryptedKey());
int storeFlag = req.getStoreFlag();
if (storeFlag != 0 && storeFlag != 1) {
throw new IllegalArgumentException("storeFlag must be 0 or 1");
throw new IllegalArgumentException("storeFlag必须为0或1");
}
}
@@ -784,7 +784,7 @@ public class MockPcieCryptoService implements PcieCryptoService {
result.setHmacLength(hmac.length);
return result;
} catch (Exception ex) {
throw new IllegalStateException("mock hmac failed", ex);
throw new IllegalStateException("模拟HMAC计算失败", ex);
}
}
@@ -940,7 +940,7 @@ public class MockPcieCryptoService implements PcieCryptoService {
private static int requireUserKeyType(int keyType) {
if (keyType != PcieUserKeyType.SIGN.getCode() && keyType != PcieUserKeyType.ENC.getCode()) {
throw new IllegalArgumentException("keyType must be 0(SIGN) or 1(ENC)");
throw new IllegalArgumentException("keyType必须为0(SIGN)或1(ENC)");
}
return keyType;
}
@@ -6,11 +6,11 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "管理员修改账号口令请求")
public class AdminChangePasswordRequest {
@NotBlank(message = "oldPassword is required")
@NotBlank(message = "oldPassword不能为空")
@Schema(description = "旧口令", example = "12345678")
private String oldPassword;
@NotBlank(message = "newPassword is required")
@NotBlank(message = "newPassword不能为空")
@Schema(description = "新口令", example = "Abc1234!")
private String newPassword;
@@ -7,12 +7,12 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "修改当前认证账户口令请求")
public class ChangePasswordRequest {
@NotBlank(message = "oldPassword is required")
@NotBlank(message = "oldPassword不能为空")
@JsonAlias("currentPassword")
@Schema(description = "旧口令", example = "12345678")
private String oldPassword;
@NotBlank(message = "newPassword is required")
@NotBlank(message = "newPassword不能为空")
@Schema(description = "新口令", example = "Abc1234!")
private String newPassword;
@@ -7,7 +7,7 @@ import java.util.List;
@Schema(description = "内部登录请求")
public class LoginRequest {
@NotBlank(message = "roleCode is required")
@NotBlank(message = "roleCode不能为空")
@Schema(description = "角色编码", example = "KEY_ADMIN")
private String roleCode;
@@ -18,7 +18,7 @@ public class LoginRequest {
private List<FullLoginAccountRequest> fullAccounts;
@Schema(description = "UKey 序列号列表,完整登录时需传入角色要求数量的序列号")
private List<@NotBlank(message = "ukey serial must not be blank") String> ukeySerials;
private List<@NotBlank(message = "ukey serial不能为空") String> ukeySerials;
public String getRoleCode() {
return roleCode;
@@ -7,11 +7,11 @@ import jakarta.validation.constraints.NotNull;
@Schema(description = "角色口令校验账号")
public class PasswordLoginAccountRequest {
@NotNull(message = "uid is required")
@NotNull(message = "uid不能为空")
@Schema(description = "角色固定席位编号", example = "1")
private Integer uid;
@NotBlank(message = "password is required")
@NotBlank(message = "password不能为空")
@Schema(description = "账号口令", example = "12345678")
private String password;
@@ -12,20 +12,20 @@ public class PasswordLoginRequest {
/**
* 统一使用主角色编码,不再暴露旧系统影子角色名。
*/
@NotBlank(message = "roleCode is required")
@NotBlank(message = "roleCode不能为空")
@Schema(description = "角色编码", example = "AUDIT_ADMIN")
private String roleCode;
@Valid
@NotEmpty(message = "accounts is required")
@NotEmpty(message = "accounts不能为空")
@Schema(description = "参与本次角色口令校验的账号列表")
private List<PasswordLoginAccountRequest> accounts;
// @NotBlank(message = "captchaCode is required")
// @NotBlank(message = "captchaCode不能为空")
@Schema(description = "图形验证码", example = "ABCD")
private String captchaCode;
// @NotBlank(message = "captchaId is required")
// @NotBlank(message = "captchaId不能为空")
@Schema(description = "验证码标识", example = "captcha-001")
private String captchaId;
@@ -7,25 +7,25 @@ import jakarta.validation.constraints.NotNull;
@Schema(description = "单个 UKey 登录因子")
public class UkeyLoginProof {
@NotBlank(message = "pubKey is required")
@NotBlank(message = "pubKey不能为空")
private String pubKey;
@NotNull(message = "uid is required")
@NotNull(message = "uid不能为空")
private Integer uid;
/**
* 对应旧系统下发的 rb,命名改为更明确的服务端随机数。
*/
@NotBlank(message = "serverRandom is required")
@NotBlank(message = "serverRandom不能为空")
private String serverRandom;
@NotBlank(message = "issueSignature is required")
@NotBlank(message = "issueSignature不能为空")
private String issueSignature;
@NotBlank(message = "loginPayload is required")
@NotBlank(message = "loginPayload不能为空")
private String loginPayload;
@NotBlank(message = "loginSignature is required")
@NotBlank(message = "loginSignature不能为空")
private String loginSignature;
@Schema(description = "当前 UKey 固定席位对应账号的口令,所有角色都必传", example = "12345678")
@@ -6,7 +6,7 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "申请 UKey 登录随机数")
public class UkeyLoginRandomRequest {
@NotBlank(message = "roleCode is required")
@NotBlank(message = "roleCode不能为空")
@Schema(description = "角色编码", example = "SUPER_ADMIN")
private String roleCode;
@@ -10,12 +10,12 @@ import java.util.List;
@Schema(description = "UKey 登录请求")
public class UkeyLoginRequest {
@NotBlank(message = "roleCode is required")
@NotBlank(message = "roleCode不能为空")
@Schema(description = "角色编码", example = "KEY_ADMIN")
private String roleCode;
@Valid
@NotEmpty(message = "loginFactors is required")
@NotEmpty(message = "loginFactors不能为空")
@JsonAlias("ukeyProofs")
@Schema(description = "UKey 登录因子列表")
private List<UkeyLoginProof> loginFactors;
@@ -37,14 +37,14 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
if (requireInternalAuth != null) {
String currentRole = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
if (!requireInternalAuth.role().getCode().equals(currentRole)) {
writeForbidden(response, "role not allowed");
operationAuditService.recordDeniedLog(request, handlerMethod, "role not allowed");
writeForbidden(response, "角色无权限访问");
operationAuditService.recordDeniedLog(request, handlerMethod, "角色无权限访问");
return false;
}
String currentAuthLevel = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL);
if (!authLevelAllowed(currentAuthLevel, requireInternalAuth.authLevel())) {
writeForbidden(response, "auth level not allowed");
operationAuditService.recordDeniedLog(request, handlerMethod, "auth level not allowed");
writeForbidden(response, "认证级别无权限访问");
operationAuditService.recordDeniedLog(request, handlerMethod, "认证级别无权限访问");
return false;
}
}
@@ -79,7 +79,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
List<AuthFullAccountEntity> fullAccounts = authFullAccountRepository.findByRoleCode(targetRoleCode);
if (fullAccounts.isEmpty()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full accounts not found");
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在");
}
for (AuthFullAccountEntity account : fullAccounts) {
account.setStatus(RoleAccountStatus.ACTIVE.name());
@@ -101,7 +101,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
List<AuthFullAccountEntity> fullAccounts = authFullAccountRepository.findByRoleCode(targetRoleCode);
if (fullAccounts.isEmpty()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full accounts not found");
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在");
}
for (AuthFullAccountEntity account : fullAccounts) {
String newSalt = passwordSaltGenerator.nextSalt();
@@ -166,7 +166,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
}
AuthFullAccountEntity account = authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full account not found"));
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"));
if (!passwordHasher.matches(oldPassword, account.getPasswordSalt(), account.getPasswordHash())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "old password is incorrect");
}
@@ -285,7 +285,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
RoleCode targetRole = resolveRoleCode(targetRoleCode);
validateRoleUid(targetRole, uid);
authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full account not found"));
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"));
RoleUkeyBindingEntity binding = findExistingActiveBinding(targetRoleCode, uid, ukeySerial, ukeyPubkey);
if (binding.getId() == null) {
@@ -333,7 +333,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair, targetRole.getCode()));
MasterKeyBackupPacket backupPacket = null;
if (request.getUkeySerial() == null || request.getUkeySerial().isBlank()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial is required");
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial不能为空");
}
if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(request.getUid());
@@ -382,7 +382,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
private RoleAccountEntity loadRole(String roleCode) {
resolveRoleCode(roleCode);
return roleAccountRepository.findByRoleCode(roleCode)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found"));
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色账号不存在"));
}
private void validateRoleUid(RoleCode targetRole, Integer uid) {
@@ -406,7 +406,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, roleCode));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
throw new IllegalStateException("serialize ukey issue payload 执行失败", ex);
}
}
}
@@ -82,7 +82,7 @@ public class AuthServiceImpl implements AuthService {
@Override
public LoginResponse login(LoginRequest request) {
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(request.getRoleCode())
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "角色账号不存在"));
validateRoleStatus(roleAccount);
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
@@ -180,7 +180,7 @@ public class AuthServiceImpl implements AuthService {
public CurrentUserResponse me(String username, String sessionToken) {
String normalized = username == null || username.isBlank() ? "admin" : username;
RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(normalized)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found"));
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "角色账号不存在"));
CurrentUserResponse response = new CurrentUserResponse();
response.setUsername(roleAccount.getRoleCode());
response.setDisplayName(roleAccount.getDisplayName());
@@ -211,7 +211,7 @@ public class AuthServiceImpl implements AuthService {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account is not authenticated in current session");
}
AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account not found"));
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(fullAccount);
if (!passwordHasher.matches(currentPassword, fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) {
onFullPasswordFailed(fullAccount);
@@ -262,7 +262,7 @@ public class AuthServiceImpl implements AuthService {
}
AuthFullAccountEntity roleAccountSeat = authFullAccountRepository
.findByRoleCodeAndUid(roleAccount.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account not found"));
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(roleAccountSeat);
if (!passwordHasher.matches(account.getPassword(), roleAccountSeat.getPasswordSalt(), roleAccountSeat.getPasswordHash())) {
onFullPasswordFailed(roleAccountSeat);
@@ -333,7 +333,7 @@ public class AuthServiceImpl implements AuthService {
}
AuthFullAccountEntity fullAccount = authFullAccountRepository
.findByRoleCodeAndUid(roleAccount.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "full account not found"));
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "完整账号不存在"));
validateFullAccountStatus(fullAccount);
if (!passwordHasher.matches(account.getPassword(), fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) {
onFullPasswordFailed(fullAccount);
@@ -357,7 +357,7 @@ public class AuthServiceImpl implements AuthService {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "uid does not match fixed role account");
}
if (proof.getPassword() == null || proof.getPassword().isBlank()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account password is required");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "account password不能为空");
}
FullLoginAccountRequest accountRequest = new FullLoginAccountRequest();
accountRequest.setUid(uid);
@@ -394,7 +394,7 @@ public class AuthServiceImpl implements AuthService {
.map(RoleUkeyBindingEntity::getUkeySerial)
.collect(Collectors.toSet());
if (!boundSerials.containsAll(uniqueSerials)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification 执行失败");
}
long matchedUidCount = bindings.stream()
.filter(binding -> uniqueSerials.contains(binding.getUkeySerial()))
@@ -437,7 +437,7 @@ public class AuthServiceImpl implements AuthService {
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, RoleCode.valueOf(roleCode).getCode()));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
throw new IllegalStateException("serialize ukey issue payload 执行失败", ex);
}
}
@@ -502,7 +502,7 @@ public class AuthServiceImpl implements AuthService {
try {
return objectMapper.writeValueAsString(authenticatedPrincipals);
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize authenticated principals failed", ex);
throw new IllegalStateException("serialize authenticated principals 执行失败", ex);
}
}
@@ -536,7 +536,7 @@ public class AuthServiceImpl implements AuthService {
new TypeReference<List<AuthSessionPrincipal>>() { }
);
} catch (JsonProcessingException ex) {
throw new IllegalStateException("deserialize authenticated principals failed", ex);
throw new IllegalStateException("deserialize authenticated principals 执行失败", ex);
}
}
@@ -579,9 +579,9 @@ public class AuthServiceImpl implements AuthService {
private AuthSessionEntity requireActiveSession(String sessionToken) {
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken)
.orElseThrow(() -> new BizException(ErrorCode.SESSION_INVALID.getCode(), "session not found"));
.orElseThrow(() -> new BizException(ErrorCode.SESSION_INVALID.getCode(), "会话不存在"));
if (session.getLogoutAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) {
throw new BizException(ErrorCode.SESSION_INVALID.getCode(), "session expired");
throw new BizException(ErrorCode.SESSION_INVALID.getCode(), "会话已过期");
}
return session;
}
@@ -63,10 +63,10 @@ public class InMemoryCaptchaService implements CaptchaService {
public void verify(String captchaId, String captchaCode) {
CaptchaEntry entry = store.remove(captchaId);
if (entry == null || entry.expiresAt().isBefore(clock.instant())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "captcha verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "captcha verification 执行失败");
}
if (captchaCode == null || !entry.code().equalsIgnoreCase(captchaCode.trim())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "captcha verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "captcha verification 执行失败");
}
}
@@ -116,7 +116,7 @@ public class InMemoryCaptchaService implements CaptchaService {
ImageIO.write(image, "png", outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
} catch (Exception ex) {
throw new IllegalStateException("generate captcha image failed", ex);
throw new IllegalStateException("generate captcha image 执行失败", ex);
}
}
@@ -39,7 +39,7 @@ public class InMemoryUkeyLoginRandomService implements UkeyLoginRandomService {
@Override
public List<String> issue(String roleCode, int count) {
if (count <= 0) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "count must be positive");
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "count必须为正数");
}
List<String> randoms = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
@@ -53,10 +53,10 @@ public class InMemoryUkeyLoginRandomService implements UkeyLoginRandomService {
public void assertIssued(String roleCode, List<String> randoms) {
IssuedRandoms issuedRandoms = store.remove(roleCode);
if (issuedRandoms == null || issuedRandoms.expiresAt().isBefore(clock.instant())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login randoms are missing or expired");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "登录随机数不存在或已过期");
}
if (randoms == null || randoms.size() != issuedRandoms.randoms().size() || !issuedRandoms.randoms().containsAll(randoms)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login random verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login random verification 执行失败");
}
}
@@ -23,7 +23,7 @@ public class Pbkdf2PasswordHasher implements PasswordHasher {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return Base64.getEncoder().encodeToString(keyFactory.generateSecret(spec).getEncoded());
} catch (GeneralSecurityException ex) {
throw new IllegalStateException("failed to hash password", ex);
throw new IllegalStateException("密码哈希计算失败", ex);
}
}
@@ -60,7 +60,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
} catch (BizException ex) {
throw ex;
} catch (RuntimeException ex) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "issue signature verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "issue signature verification 执行失败");
}
}
@@ -69,36 +69,36 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
try {
PublicKey publicKey = parsePublicKey(pubKey);
if (loginSignData == null || loginSignData.isBlank()) {
throw new IllegalArgumentException("login sign data is required");
throw new IllegalArgumentException("login sign data不能为空");
}
byte[] signatureBytes = decodeSignature(loginSign);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM, PROVIDER);
signature.initVerify(publicKey);
signature.update(loginSignData.getBytes(StandardCharsets.UTF_8));
if (!signature.verify(signatureBytes)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification 执行失败");
}
} catch (BizException ex) {
throw ex;
} catch (IllegalArgumentException ex) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), ex.getMessage());
} catch (RuntimeException ex) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification 执行失败");
} catch (Exception ex) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed");
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification 执行失败");
}
}
private PublicKey parsePublicKey(String value) throws Exception {
String trimmed = value == null ? "" : value.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("login public key is invalid");
throw new IllegalArgumentException("登录公钥无效");
}
if (trimmed.contains("BEGIN")) {
return parsePemPublicKey(trimmed);
}
String normalized = normalize(trimmed);
byte[] decoded = decodeHexOrBase64(normalized, "login public key is invalid");
byte[] decoded = decodeHexOrBase64(normalized, "登录公钥无效");
try {
return KeyFactory.getInstance("EC", PROVIDER).generatePublic(new X509EncodedKeySpec(decoded));
} catch (Exception ignored) {
@@ -110,7 +110,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
try (PEMParser parser = new PEMParser(new StringReader(pemContent))) {
Object parsed = parser.readObject();
if (!(parsed instanceof SubjectPublicKeyInfo publicKeyInfo)) {
throw new IllegalArgumentException("login public key is invalid");
throw new IllegalArgumentException("登录公钥无效");
}
return new JcaPEMKeyConverter().setProvider(PROVIDER).getPublicKey(publicKeyInfo);
}
@@ -133,7 +133,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
| ((keyBytes[2] & 0xff) << 8)
| (keyBytes[3] & 0xff);
if (bitsLittleEndian != 256 && bitsBigEndian != 256) {
throw new IllegalArgumentException("login public key is invalid");
throw new IllegalArgumentException("登录公钥无效");
}
byte[] pointBytes = new byte[65];
pointBytes[0] = 0x04;
@@ -152,7 +152,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
System.arraycopy(rawPointBytes, 0, pointBytes, 1, rawPointBytes.length);
}
if (pointBytes.length != 65 || pointBytes[0] != 0x04) {
throw new IllegalArgumentException("login public key is invalid");
throw new IllegalArgumentException("登录公钥无效");
}
ECParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec(SM2_CURVE);
ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(
@@ -164,9 +164,9 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
private byte[] decodeSignature(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("login signature is required");
throw new IllegalArgumentException("login signature不能为空");
}
byte[] decoded = decodeHexOrBase64(normalize(value), "login signature is invalid");
byte[] decoded = decodeHexOrBase64(normalize(value), "登录签名无效");
if (isDerSignature(decoded)) {
return decoded;
}
@@ -176,7 +176,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
if (decoded.length == 64) {
return rawRsToDer(decoded);
}
throw new IllegalArgumentException("login signature is invalid");
throw new IllegalArgumentException("登录签名无效");
}
private byte[] gm0018SignatureToRawRs(byte[] signatureBytes) {
@@ -209,7 +209,7 @@ public class PcieCompatUkeyVerifier implements CompatUkeyVerifier {
try {
return new DERSequence(vector).getEncoded(ASN1Encoding.DER);
} catch (Exception ex) {
throw new IllegalArgumentException("login signature is invalid", ex);
throw new IllegalArgumentException("登录签名无效", ex);
}
}
@@ -43,7 +43,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
@Override
public ResourceSealedPayload seal(byte[] payloadBytes) {
if (payloadBytes == null) {
throw new IllegalArgumentException("payloadBytes is required");
throw new IllegalArgumentException("payloadBytes不能为空");
}
// 资源包正文使用随机 SM4 DEK 加密,再由用户密钥把 DEK 封装成 envelope。
// 这样大 payload 仍走对称加密,ECC 只负责保护一次性数据密钥。
@@ -74,7 +74,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
sealedPayload.setAuthenticityKeyFingerprint(buildFingerprint("USER_KEY_SIGN", resourceBackupProperties.getAuthenticitySignKeyIndex()));
return sealedPayload;
} catch (Exception ex) {
throw new IllegalStateException("failed to seal resource payload", ex);
throw new IllegalStateException("资源备份载荷加封失败", ex);
} finally {
wipe(sm4Key);
}
@@ -83,7 +83,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
@Override
public byte[] sign(byte[] dataToSign) {
if (dataToSign == null) {
throw new IllegalArgumentException("dataToSign is required");
throw new IllegalArgumentException("dataToSign不能为空");
}
EccInternalSignRequest request = new EccInternalSignRequest();
request.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
@@ -95,10 +95,10 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
@Override
public void verify(byte[] dataToVerify, byte[] signature) {
if (dataToVerify == null) {
throw new IllegalArgumentException("dataToVerify is required");
throw new IllegalArgumentException("dataToVerify不能为空");
}
if (signature == null || signature.length == 0) {
throw new IllegalArgumentException("signature is required");
throw new IllegalArgumentException("signature不能为空");
}
EccInternalVerifyRequest verifyRequest = new EccInternalVerifyRequest();
verifyRequest.setKeyIndex(resourceBackupProperties.getAuthenticitySignKeyIndex());
@@ -110,16 +110,16 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
@Override
public byte[] unsealAndVerify(Map<String, Object> envelope, byte[] cipherBytes, byte[] dataToVerify, byte[] signature) {
if (envelope == null) {
throw new IllegalArgumentException("envelope is required");
throw new IllegalArgumentException("envelope不能为空");
}
if (cipherBytes == null) {
throw new IllegalArgumentException("cipherBytes is required");
throw new IllegalArgumentException("cipherBytes不能为空");
}
if (dataToVerify == null) {
throw new IllegalArgumentException("dataToVerify is required");
throw new IllegalArgumentException("dataToVerify不能为空");
}
if (signature == null || signature.length == 0) {
throw new IllegalArgumentException("signature is required");
throw new IllegalArgumentException("signature不能为空");
}
verify(dataToVerify, signature);
@@ -137,7 +137,7 @@ public class ResourceEnvelopeCryptoServiceImpl implements ResourceEnvelopeCrypto
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sm4Key, "SM4"), new IvParameterSpec(iv));
return cipher.doFinal(cipherBytes);
} catch (Exception ex) {
throw new IllegalStateException("failed to unseal resource payload", ex);
throw new IllegalStateException("资源备份载荷解封失败", ex);
} finally {
wipe(sm4Key);
}
@@ -19,12 +19,12 @@ public class ResourceDiscoveryServiceImpl implements ResourceDiscoveryService {
@Override
public List<DiscoveredResourceItem> discover(InitTaskEntity initTask) {
if (initTask == null) {
throw new IllegalArgumentException("initTask is required");
throw new IllegalArgumentException("initTask不能为空");
}
String productType = upper(initTask.getProductType());
String mqType = upper(initTask.getMqType());
if (productType.isEmpty() || mqType.isEmpty()) {
throw new IllegalArgumentException("productType and mqType are required");
throw new IllegalArgumentException("productType和mqType不能为空");
}
List<DiscoveredResourceItem> items = new ArrayList<>();
@@ -18,7 +18,7 @@ public class MqReplayContextBuilder {
public byte[] build(InitTaskEntity task) {
if (task == null || isBlank(task.getInitPlanJson())) {
throw new IllegalArgumentException("initPlanJson is empty");
throw new IllegalArgumentException("initPlanJson为空");
}
try {
JsonNode root = objectMapper.readTree(task.getInitPlanJson());
@@ -53,7 +53,7 @@ public class MqReplayContextBuilder {
return objectMapper.writeValueAsBytes(payload);
} catch (Exception ex) {
throw new IllegalArgumentException("failed to build mq replay context", ex);
throw new IllegalArgumentException("构建MQ重放上下文失败", ex);
}
}
@@ -64,11 +64,11 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
int exitCode = process.waitFor();
if (exitCode != 0) {
String error = new String(stderr, StandardCharsets.UTF_8).trim();
throw new IllegalStateException("mysqldump failed with exitCode=" + exitCode
throw new IllegalStateException("mysqldump 执行失败 with exitCode=" + exitCode
+ (error.isEmpty() ? "" : ", error=" + error));
}
if (stdout.length == 0) {
throw new IllegalStateException("mysqldump produced empty output");
throw new IllegalStateException("mysqldump输出为空");
}
CollectedDatabaseDump dump = new CollectedDatabaseDump();
dump.setDatabaseName(target.databaseName());
@@ -77,19 +77,19 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
return dump;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("mysqldump interrupted", ex);
throw new IllegalStateException("mysqldump 被中断", ex);
} catch (IOException ex) {
throw new IllegalStateException("failed to run mysqldump", ex);
throw new IllegalStateException("执行mysqldump失败", ex);
}
}
private MysqlConnectionTarget resolveTarget(String configuredDatabaseName, String fallbackDatabaseName) {
String url = trim(datasourceUrl);
if (url.isEmpty()) {
throw new IllegalStateException("spring.datasource.url is required for database backup");
throw new IllegalStateException("spring.datasource.url不能为空 for database backup");
}
if (!url.startsWith("jdbc:mysql://")) {
throw new IllegalStateException("unsupported datasource url for backup: " + url);
throw new IllegalStateException("不支持用于备份的数据源URL:" + url);
}
try {
String normalizedUrl = url.substring("jdbc:".length());
@@ -111,7 +111,7 @@ public class MysqlDatabaseBackupCollector implements DatabaseBackupCollector {
databaseName
);
} catch (IllegalArgumentException ex) {
throw new IllegalStateException("invalid datasource url for backup: " + url, ex);
throw new IllegalStateException("用于备份的数据源URL无效:" + url, ex);
}
}
@@ -17,7 +17,7 @@ final class ResourcePackageFormatSupport {
writePart(outputStream, cipherBytes);
return outputStream.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException("failed to build package signature source", ex);
throw new IllegalStateException("构建包签名原文失败", ex);
}
}
@@ -74,13 +74,13 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
CreateResourceBackupRequest request
) {
if (isBlank(taskId) || isBlank(backupId)) {
throw new IllegalArgumentException("taskId and backupId are required");
throw new IllegalArgumentException("taskId和backupId不能为空");
}
if (initTask == null) {
throw new IllegalArgumentException("initTask is required");
throw new IllegalArgumentException("initTask不能为空");
}
if (discoveredItems == null) {
throw new IllegalArgumentException("discoveredItems is required");
throw new IllegalArgumentException("discoveredItems不能为空");
}
try {
Path taskDir = Path.of(resourceBackupProperties.getOutputDir()).resolve(taskId).normalize();
@@ -147,14 +147,14 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
result.setManifestSummaryJson(objectMapper.writeValueAsString(manifestSummary));
return result;
} catch (IOException ex) {
throw new IllegalStateException("failed to create backup package", ex);
throw new IllegalStateException("创建备份包失败", ex);
}
}
@Override
public ResourcePackageInspectResult inspectPackageHeader(Path packagePath) {
if (packagePath == null) {
throw new IllegalArgumentException("packagePath is required");
throw new IllegalArgumentException("packagePath不能为空");
}
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
@@ -173,14 +173,14 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
result.setSignature(signatureBytes);
return result;
} catch (IOException ex) {
throw new IllegalStateException("failed to inspect backup package header", ex);
throw new IllegalStateException("解析备份包头失败", ex);
}
}
@Override
public ResourcePackageInspectResult inspectPackage(Path packagePath) {
if (packagePath == null) {
throw new IllegalArgumentException("packagePath is required");
throw new IllegalArgumentException("packagePath不能为空");
}
try (ZipFile zipFile = new ZipFile(packagePath.toFile())) {
byte[] manifestBytes = readRequiredEntry(zipFile, "manifest.json");
@@ -206,7 +206,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
result.setSignature(signatureBytes);
return result;
} catch (IOException ex) {
throw new IllegalStateException("failed to inspect backup package", ex);
throw new IllegalStateException("解析备份包失败", ex);
}
}
@@ -335,7 +335,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
try {
byte[] content = dump.getContent();
if (content == null || content.length == 0) {
throw new IllegalStateException("database backup collector returned empty dump");
throw new IllegalStateException("数据库备份采集器返回空dump");
}
String databaseName = trim(dump.getDatabaseName()).isEmpty() ? "TMS" : trim(dump.getDatabaseName());
String fileName = trim(dump.getFileName()).isEmpty() ? "TMS.sql" : trim(dump.getFileName());
@@ -434,7 +434,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
}
}
}
throw new IllegalStateException("payload archive entry missing: payload-manifest.json");
throw new IllegalStateException("payload归档条目缺失:payload-manifest.json");
}
private static String buildArchiveEntryPath(DiscoveredResourceItem item, String relativeName) {
@@ -458,7 +458,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
try {
return supplier.get();
} catch (IOException ex) {
throw new IllegalStateException("failed to write payload archive entry", ex);
throw new IllegalStateException("写入payload归档条目失败", ex);
}
}
@@ -471,7 +471,7 @@ public class ResourcePackageServiceImpl implements ResourcePackageService {
private static byte[] readRequiredEntry(ZipFile zipFile, String entryName) throws IOException {
ZipEntry entry = zipFile.getEntry(entryName);
if (entry == null) {
throw new IllegalStateException("backup package entry missing: " + entryName);
throw new IllegalStateException("备份包条目缺失:" + entryName);
}
try (InputStream inputStream = zipFile.getInputStream(entry)) {
return inputStream.readAllBytes();
@@ -40,12 +40,12 @@ public class ResourcePayloadExtractor {
Path payloadDir = normalizedWorkDir.resolve("payload").normalize();
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackage(packagePath);
byte[] payloadArchiveBytes = Optional.ofNullable(inspectResult.getPayloadArchiveBytes())
.orElseThrow(() -> new IllegalStateException("payload archive bytes missing"));
.orElseThrow(() -> new IllegalStateException("payload归档内容缺失"));
extractPayloadArchive(payloadArchiveBytes, payloadDir);
Path manifestPath = payloadDir.resolve("payload-manifest.json").normalize();
if (!Files.isRegularFile(manifestPath)) {
throw new IllegalStateException("payload manifest missing: " + manifestPath);
throw new IllegalStateException("payload清单缺失:" + manifestPath);
}
Map<String, Object> manifest = readManifest(manifestPath);
validateRestoreTargets(payloadDir, manifest);
@@ -77,14 +77,14 @@ public class ResourcePayloadExtractor {
}
}
} catch (IOException ex) {
throw new IllegalStateException("failed to extract payload archive", ex);
throw new IllegalStateException("解压payload归档失败", ex);
}
}
private Path resolveSafePayloadTarget(Path payloadDir, String entryName) {
Path target = payloadDir.resolve(entryName).normalize();
if (!target.startsWith(payloadDir)) {
throw new IllegalStateException("payload archive entry escapes payload dir: " + entryName);
throw new IllegalStateException("payload归档条目越过payload目录:" + entryName);
}
return target;
}
@@ -94,7 +94,7 @@ public class ResourcePayloadExtractor {
return objectMapper.readValue(manifestPath.toFile(), new TypeReference<Map<String, Object>>() {
});
} catch (IOException ex) {
throw new IllegalStateException("failed to read payload manifest: " + manifestPath, ex);
throw new IllegalStateException("读取payload清单失败:" + manifestPath, ex);
}
}
@@ -154,7 +154,7 @@ public class ResourcePayloadExtractor {
Files.writeString(workDir.resolve("restore-mq.tsv"), String.join(System.lineSeparator(), mqLines)
+ (mqLines.isEmpty() ? "" : System.lineSeparator()));
} catch (IOException ex) {
throw new IllegalStateException("failed to write shell restore plans", ex);
throw new IllegalStateException("写入shell恢复计划失败", ex);
}
}
@@ -213,7 +213,7 @@ public class ResourcePayloadExtractor {
}
Path sourceFile = payloadDir.resolve(entryPath).normalize();
if (!sourceFile.startsWith(payloadDir)) {
throw new IllegalStateException("payload manifest entry escapes payload dir: " + entryPath);
throw new IllegalStateException("payload清单条目越过payload目录:" + entryPath);
}
}
@@ -236,7 +236,7 @@ public class ResourcePayloadExtractor {
Path target = Path.of(restorePath).normalize().toAbsolutePath();
boolean allowed = allowedRoots.stream().anyMatch(target::startsWith);
if (!allowed) {
throw new IllegalStateException("restore path is not allowed: " + restorePath);
throw new IllegalStateException("恢复路径不允许:" + restorePath);
}
}
@@ -276,7 +276,7 @@ public class ResourcePayloadExtractor {
objectMapper.writerWithDefaultPrettyPrinter().writeValue(restoreContextPath.toFile(), context);
return restoreContextPath;
} catch (IOException ex) {
throw new IllegalStateException("failed to write restore context", ex);
throw new IllegalStateException("写入恢复上下文失败", ex);
}
}
@@ -46,7 +46,7 @@ public class LightweightRestoreApplierLauncher {
);
int exitCode = invoker.run(command, logFile, normalizedWorkDir);
if (exitCode != 0) {
throw new IllegalStateException("failed to launch lightweight restore apply script, exitCode=" + exitCode);
throw new IllegalStateException("启动轻量恢复应用脚本失败,exitCode=" + exitCode);
}
}
@@ -76,7 +76,7 @@ public class LightweightRestoreApplierLauncher {
}
Files.writeString(workDir.resolve("restore-env.sh"), builder.toString());
} catch (IOException ex) {
throw new IllegalStateException("failed to write lightweight restore env", ex);
throw new IllegalStateException("写入轻量恢复环境变量失败", ex);
}
}
@@ -27,10 +27,10 @@ public class ProcessBuilderRestoreCommandInvoker implements RestoreCommandInvoke
Process process = pb.start();
return process.waitFor();
} catch (IOException ex) {
throw new IllegalStateException("failed to start restore command: " + String.join(" ", command), ex);
throw new IllegalStateException("启动恢复命令失败:" + String.join(" ", command), ex);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("restore command interrupted: " + String.join(" ", command), ex);
throw new IllegalStateException("restore command 被中断: " + String.join(" ", command), ex);
}
}
}
@@ -44,7 +44,7 @@ public class RestoreStatusFileReader {
return objectMapper.readValue(statusFile.toFile(), new TypeReference<Map<String, Object>>() {
});
} catch (IOException ex) {
throw new IllegalStateException("failed to read restore status file: " + statusFile, ex);
throw new IllegalStateException("读取恢复状态文件失败:" + statusFile, ex);
}
}
@@ -59,7 +59,7 @@ public class RestoreStatusFileReader {
}
return new String(bytes, bytes.length - MAX_LOG_BYTES, MAX_LOG_BYTES, StandardCharsets.UTF_8);
} catch (IOException ex) {
throw new IllegalStateException("failed to read restore log file: " + logFile, ex);
throw new IllegalStateException("读取恢复日志文件失败:" + logFile, ex);
}
}
@@ -59,7 +59,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
public CreateResourceBackupResponse createBackupTask(CreateResourceBackupRequest request) {
// 资源备份必须基于“最近一次成功初始化”快照收集上下文,避免前端重新传一套可能失真的参数。
InitTaskEntity initTask = initTaskRepository.findLatestByTaskTypeAndStatus("INIT", "SUCCESS")
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing"));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "成功ful init task is missing"));
String taskId = resourceTaskIdGenerator.nextBackupTaskId();
String backupId = resourceTaskIdGenerator.nextBackupId();
@@ -119,7 +119,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
@Override
public ResourceBackupTaskDetailResponse getBackupTask(String taskId) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份任务不存在:" + trim(taskId)));
ResourceBackupTaskDetailResponse response = new ResourceBackupTaskDetailResponse();
response.setTaskId(entity.getTaskId());
response.setBackupId(entity.getBackupId());
@@ -147,10 +147,10 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
@Override
public ResourceBackupDownloadResult downloadBackupPackage(String taskId) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份任务不存在:" + trim(taskId)));
Path packagePath = Path.of(trim(entity.getPackagePath())).normalize().toAbsolutePath();
if (trim(entity.getPackagePath()).isEmpty() || !Files.exists(packagePath) || !Files.isRegularFile(packagePath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份包文件不存在");
}
try {
ResourceBackupDownloadResult result = new ResourceBackupDownloadResult();
@@ -166,7 +166,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
@Override
public List<ResourceTaskStepItemResponse> getBackupTaskSteps(String taskId) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份任务不存在:" + trim(taskId)));
ResourceTaskStepItemResponse item = new ResourceTaskStepItemResponse();
item.setStepNo(1);
item.setStepCode("PACKAGE_CREATE");
@@ -181,7 +181,7 @@ public class ResourceBackupServiceImpl implements ResourceBackupService {
@Override
public ResourceTaskStepLogResponse getBackupTaskStepLog(String taskId, int stepNo) {
ResourceBackupTaskEntity entity = resourceBackupTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份任务不存在:" + trim(taskId)));
ResourceTaskStepLogResponse response = new ResourceTaskStepLogResponse();
response.setTaskId(trim(taskId));
response.setStepNo(stepNo);
@@ -35,7 +35,7 @@ public class ResourcePrecheckServiceImpl implements ResourcePrecheckService {
@Override
public ResourceRestorePrecheckResponse precheck(ResourceRestorePrecheckRequest request) {
FileRecordEntity fileRecord = fileRecordRepository.findByFileId(trim(request.getFileId()))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "backup package file not found: " + trim(request.getFileId())));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "备份包文件不存在:" + trim(request.getFileId())));
// 预检阶段只解析包头、验签和比对指纹,不提前解密完整 payload。
ResourcePackageInspectResult inspectResult = resourcePackageService.inspectPackageHeader(Path.of(fileRecord.getStoragePath()));
String storageFingerprint = stringValue(inspectResult.getManifest().get("storageEncKeyFingerprint"));
@@ -41,21 +41,21 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
@Override
public CreateResourceRestoreResponse createRestoreTask(CreateResourceRestoreRequest request) {
if (!Boolean.TRUE.equals(request.getConfirmRiskAccepted())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "restore risk must be accepted");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "必须确认恢复风险");
}
// 恢复创建必须消费预检结果,确保“用户确认的 backupId”与预检看到的是同一个包。
ResourcePrecheckContext context = resourcePrecheckStateStore.find(request.getPrecheckId())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found"));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在"));
ResourceRestorePrecheckResponse precheck = context.getResponse();
validatePrecheck(precheck);
if (!trim(request.getConfirmBackupId()).equals(trim(precheck.getBackupId()))) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "confirmed backup id does not match precheck result");
}
FileRecordEntity packageFile = fileRecordRepository.findByFileId(context.getFileId())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package file not found: " + trim(context.getFileId())));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份包文件不存在:" + trim(context.getFileId())));
String packagePath = trim(packageFile.getStoragePath());
if (packagePath.isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource backup package path is empty");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源备份包路径为空");
}
String taskId = resourceTaskIdGenerator.nextRestoreTaskId();
@@ -97,7 +97,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
try {
Files.createDirectories(taskDir);
} catch (IOException ex) {
throw new IllegalStateException("failed to create restore task dir: " + taskDir, ex);
throw new IllegalStateException("创建恢复任务目录失败:" + taskDir, ex);
}
}
@@ -107,7 +107,7 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
private static void validatePrecheck(ResourceRestorePrecheckResponse precheck) {
if (precheck == null) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检不存在");
}
if (!Boolean.TRUE.equals(precheck.getCompatible())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck is not compatible");
@@ -116,10 +116,10 @@ public class ResourceRestoreServiceImpl implements ResourceRestoreService {
if (!expiresAt.isEmpty()) {
try {
if (LocalDateTime.parse(expiresAt).isBefore(LocalDateTime.now())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck expired");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检已过期");
}
} catch (DateTimeException ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore precheck expiresAt is invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复预检过期时间无效");
}
}
}
@@ -30,7 +30,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
@Override
public ResourceRestoreTaskDetailResponse getRestoreTask(String taskId) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
ResourceRestoreTaskDetailResponse response = new ResourceRestoreTaskDetailResponse();
response.setTaskId(entity.getTaskId());
response.setPrecheckId(entity.getPrecheckId());
@@ -59,7 +59,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
@Override
public List<ResourceTaskStepItemResponse> getRestoreTaskSteps(String taskId) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
Path taskDir = resolveTaskDir(entity);
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(taskDir);
@@ -79,7 +79,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
@Override
public ResourceTaskStepLogResponse getRestoreTaskStepLog(String taskId, int stepNo) {
ResourceRestoreTaskEntity entity = resourceRestoreTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "resource restore task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "资源恢复任务不存在:" + trim(taskId)));
List<String> restorePlan = readStringList(entity.getRestorePlanJson());
String stepCode = stepNo > 0 && stepNo <= restorePlan.size() ? restorePlan.get(stepNo - 1) : "";
RestoreStatusSnapshot snapshot = restoreStatusFileReader.read(resolveTaskDir(entity));
@@ -150,7 +150,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
return objectMapper.readValue(json, new TypeReference<List<String>>() {
});
} catch (Exception ex) {
throw new IllegalStateException("failed to parse restore plan json", ex);
throw new IllegalStateException("解析恢复计划JSON失败", ex);
}
}
@@ -161,7 +161,7 @@ public class ResourceTaskQueryServiceImpl implements ResourceTaskQueryService {
try {
return objectMapper.readValue(json, Object.class);
} catch (Exception ex) {
throw new IllegalStateException("failed to parse consistency report json", ex);
throw new IllegalStateException("解析一致性报告JSON失败", ex);
}
}
@@ -13,7 +13,7 @@ public class InMemoryResourcePrecheckStateStore implements ResourcePrecheckState
@Override
public void save(ResourcePrecheckContext context) {
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
throw new IllegalArgumentException("context with precheckId is required");
throw new IllegalArgumentException("context with precheckId不能为空");
}
contexts.put(context.getResponse().getPrecheckId(), context);
}
@@ -31,14 +31,14 @@ public class FileSystemResourcePrecheckStateStore implements ResourcePrecheckSta
@Override
public void save(ResourcePrecheckContext context) {
if (context == null || context.getResponse() == null || context.getResponse().getPrecheckId() == null) {
throw new IllegalArgumentException("context with precheckId is required");
throw new IllegalArgumentException("context with precheckId不能为空");
}
try {
Path file = resolvePath(context.getResponse().getPrecheckId());
Files.createDirectories(file.getParent());
objectMapper.writeValue(file.toFile(), new StoredResourcePrecheckContext(context.getFileId(), context.getResponse()));
} catch (IOException ex) {
throw new IllegalStateException("failed to persist resource precheck context", ex);
throw new IllegalStateException("保存资源恢复预检上下文失败", ex);
}
}
@@ -54,7 +54,7 @@ public class FileSystemResourcePrecheckStateStore implements ResourcePrecheckSta
ResourceRestorePrecheckResponse response = objectMapper.treeToValue(root.path("response"), ResourceRestorePrecheckResponse.class);
return Optional.of(new ResourcePrecheckContext(fileId, response));
} catch (IOException ex) {
throw new IllegalStateException("failed to load resource precheck context", ex);
throw new IllegalStateException("加载资源恢复预检上下文失败", ex);
}
}
@@ -31,7 +31,7 @@ public class FileSystemResourceTaskStepLogService implements ResourceTaskStepLog
response.setTruncated(false);
return response;
} catch (Exception ex) {
throw new IllegalStateException("failed to read resource task step log", ex);
throw new IllegalStateException("读取资源任务步骤日志失败", ex);
}
}
}
@@ -5,6 +5,8 @@ import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
import com.cisd.tms.modules.cert.dto.CrlImportTaskDetailRequest;
import com.cisd.tms.modules.cert.dto.CrlImportTaskResponse;
import com.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.dto.CrlRevokedItemResponse;
import com.cisd.tms.modules.cert.dto.IdRequest;
@@ -40,12 +42,18 @@ public class CrlController {
// @ReplayProtected
@AuditedOperation(module = ModuleCode.KEY, action = ActionType.IMPORT, summary = "导入证书吊销列表")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<Void> importCrl(
public ApiResponse<CrlImportTaskResponse> importCrl(
@Parameter(description = "CRL 文件,支持 PEM/DER X.509 CRL")
@RequestPart("file") MultipartFile file
) {
crlService.importCrl(file);
return ApiResponse.success();
return ApiResponse.success(crlService.submitImport(file));
}
@PostMapping("/import-tasks/detail")
@Operation(summary = "查询CRL导入任务", description = "根据任务号查询 CRL 后台导入任务状态、导入结果和失败原因。")
@RequireInternalAuth(role = RoleCode.KEY_ADMIN, authLevel = AuthLevel.LIMITED)
public ApiResponse<CrlImportTaskResponse> importTaskDetail(@Valid @RequestBody CrlImportTaskDetailRequest request) {
return ApiResponse.success(crlService.getImportTask(request.getTaskId()));
}
@PostMapping("/list")
@@ -0,0 +1,14 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "CRL导入任务详情请求")
public class CrlImportTaskDetailRequest {
@NotBlank(message = "任务号不能为空")
@Schema(description = "CRL导入任务号", example = "CRL202604301800001234")
private String taskId;
}
@@ -0,0 +1,41 @@
package com.cisd.tms.modules.cert.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Schema(description = "CRL导入任务响应")
public class CrlImportTaskResponse {
@Schema(description = "CRL导入任务号", example = "CRL202604301800001234")
private String taskId;
@Schema(description = "任务状态:PENDING、RUNNING、SUCCESS、FAILED", example = "RUNNING")
private String status;
@Schema(description = "上传文件名", example = "revoked.crl")
private String originalFilename;
@Schema(description = "上传文件大小,单位字节", example = "102400")
private Long fileSize;
@Schema(description = "导入成功后的CRL记录ID", example = "2049756829261557762")
private Long crlId;
@Schema(description = "CRL SHA-256指纹", example = "083C476C26DFD8876E0580601EA66726DF06BB20989DFD862BB34B9DD8B68094")
private String fingerprint;
@Schema(description = "吊销明细数量", example = "128")
private Integer revokedCount;
@Schema(description = "失败原因", example = "CRL已存在")
private String errorMessage;
@Schema(description = "任务开始时间", example = "2026-04-30T18:00:00")
private LocalDateTime startedAt;
@Schema(description = "任务结束时间", example = "2026-04-30T18:01:00")
private LocalDateTime finishedAt;
}
@@ -0,0 +1,27 @@
package com.cisd.tms.modules.cert.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cisd.tms.infrastructure.persistence.entity.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
@TableName("t_certificate_crl_import_task")
public class CertificateCrlImportTaskEntity extends BaseEntity {
private String taskId;
private String originalFilename;
private Long fileSize;
private String status;
private String fingerprint;
private Long crlId;
private Integer revokedCount;
private String errorMessage;
private LocalDateTime startedAt;
private LocalDateTime finishedAt;
}
@@ -0,0 +1,30 @@
package com.cisd.tms.modules.cert.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.cert.entity.CertificateCrlImportTaskEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
@Mapper
public interface CertificateCrlImportTaskMapper extends BaseMapperX<CertificateCrlImportTaskEntity> {
CertificateCrlImportTaskEntity selectByTaskId(@Param("taskId") String taskId);
int updateRunning(@Param("taskId") String taskId, @Param("startedAt") LocalDateTime startedAt);
int updateSuccess(
@Param("taskId") String taskId,
@Param("crlId") Long crlId,
@Param("fingerprint") String fingerprint,
@Param("revokedCount") Integer revokedCount,
@Param("finishedAt") LocalDateTime finishedAt
);
int updateFailed(
@Param("taskId") String taskId,
@Param("errorMessage") String errorMessage,
@Param("finishedAt") LocalDateTime finishedAt
);
}
@@ -3,7 +3,12 @@ package com.cisd.tms.modules.cert.mapper;
import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface CertificateCrlRevokedEntityMapper extends BaseMapperX<CertificateCrlRevokedEntity> {
int insertBatch(@Param("list") List<CertificateCrlRevokedEntity> list);
}
@@ -3,13 +3,22 @@ package com.cisd.tms.modules.cert.repository;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.entity.CertificateCrlEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlImportTaskEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
public interface CrlRepository {
CertificateCrlEntity saveCrl(CertificateCrlEntity entity);
CertificateCrlRevokedEntity saveRevoked(CertificateCrlRevokedEntity entity);
void saveRevokedBatch(List<CertificateCrlRevokedEntity> entities);
CertificateCrlImportTaskEntity saveImportTask(CertificateCrlImportTaskEntity entity);
Optional<CertificateCrlImportTaskEntity> findImportTaskByTaskId(String taskId);
void updateImportTaskRunning(String taskId, LocalDateTime startedAt);
void updateImportTaskSuccess(String taskId, Long crlId, String fingerprint, Integer revokedCount, LocalDateTime finishedAt);
void updateImportTaskFailed(String taskId, String errorMessage, LocalDateTime finishedAt);
Optional<CertificateCrlEntity> findCrlById(Long id);
Optional<CertificateCrlEntity> findCrlByFingerprint(String fingerprint);
void deleteCrlById(Long id);
@@ -2,16 +2,20 @@ package com.cisd.tms.modules.cert.repository.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.entity.CertificateCrlEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlImportTaskEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import com.cisd.tms.modules.cert.mapper.CertificateCrlEntityMapper;
import com.cisd.tms.modules.cert.mapper.CertificateCrlImportTaskMapper;
import com.cisd.tms.modules.cert.mapper.CertificateCrlRevokedEntityMapper;
import com.cisd.tms.modules.cert.repository.CrlRepository;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@@ -20,10 +24,16 @@ public class CrlRepositoryImpl implements CrlRepository {
private final CertificateCrlEntityMapper crlMapper;
private final CertificateCrlRevokedEntityMapper revokedMapper;
private final CertificateCrlImportTaskMapper importTaskMapper;
public CrlRepositoryImpl(CertificateCrlEntityMapper crlMapper, CertificateCrlRevokedEntityMapper revokedMapper) {
public CrlRepositoryImpl(
CertificateCrlEntityMapper crlMapper,
CertificateCrlRevokedEntityMapper revokedMapper,
CertificateCrlImportTaskMapper importTaskMapper
) {
this.crlMapper = crlMapper;
this.revokedMapper = revokedMapper;
this.importTaskMapper = importTaskMapper;
}
@Override
@@ -46,6 +56,56 @@ public class CrlRepositoryImpl implements CrlRepository {
return entity;
}
@Override
public void saveRevokedBatch(List<CertificateCrlRevokedEntity> entities) {
if (entities == null || entities.isEmpty()) {
return;
}
LocalDateTime now = LocalDateTime.now();
for (CertificateCrlRevokedEntity entity : entities) {
if (entity.getId() == null) {
entity.setId(IdWorker.getId());
}
if (entity.getCreateTime() == null) {
entity.setCreateTime(now);
}
if (entity.getUpdateTime() == null) {
entity.setUpdateTime(now);
}
}
revokedMapper.insertBatch(entities);
}
@Override
public CertificateCrlImportTaskEntity saveImportTask(CertificateCrlImportTaskEntity entity) {
if (entity.getId() == null) {
importTaskMapper.insert(entity);
} else {
importTaskMapper.updateById(entity);
}
return entity;
}
@Override
public Optional<CertificateCrlImportTaskEntity> findImportTaskByTaskId(String taskId) {
return Optional.ofNullable(importTaskMapper.selectByTaskId(taskId));
}
@Override
public void updateImportTaskRunning(String taskId, LocalDateTime startedAt) {
importTaskMapper.updateRunning(taskId, startedAt);
}
@Override
public void updateImportTaskSuccess(String taskId, Long crlId, String fingerprint, Integer revokedCount, LocalDateTime finishedAt) {
importTaskMapper.updateSuccess(taskId, crlId, fingerprint, revokedCount, finishedAt);
}
@Override
public void updateImportTaskFailed(String taskId, String errorMessage, LocalDateTime finishedAt) {
importTaskMapper.updateFailed(taskId, errorMessage, finishedAt);
}
@Override
public Optional<CertificateCrlEntity> findCrlById(Long id) {
return Optional.ofNullable(crlMapper.selectById(id));
@@ -112,6 +172,7 @@ public class CrlRepositoryImpl implements CrlRepository {
return new LambdaQueryWrapper<CertificateCrlRevokedEntity>()
.eq(CertificateCrlRevokedEntity::getIssuerDn, issuerDn)
.eq(CertificateCrlRevokedEntity::getSerialNumber, serialNumber)
.apply("EXISTS (SELECT 1 FROM t_certificate_crl c WHERE c.id = crl_id AND (c.next_update IS NULL OR c.next_update >= NOW()))");
// 吊销状态只来自仍在有效期内的 CRL;过期或缺失 nextUpdate 的 CRL 不能继续影响运行态。
.apply("EXISTS (SELECT 1 FROM t_certificate_crl c WHERE c.id = crl_id AND c.next_update >= NOW())");
}
}
@@ -11,14 +11,17 @@ import com.cisd.tms.modules.cert.entity.CertificateEntity;
import com.cisd.tms.modules.cert.repository.CertificateRepository;
import com.cisd.tms.modules.cert.support.CertPemSupport;
import com.cisd.tms.modules.cert.support.CertRuntimeStatusResolver;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
@Service
@@ -63,6 +66,7 @@ public class CertificateService {
//todo file空值判断
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
// 终端证书入库前先固定 PKI 边界:只接受当前有效、非 CA、用途满足业务认证的实体/用户证书。
assertValidTime(certificate);
assertEndEntityCertificate(certificate);
String algoType = CertPemSupport.detectAlgoType(certificate);
@@ -70,14 +74,17 @@ public class CertificateService {
if (certificateRepository.findByFingerprint(fingerprint).isPresent()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "证书已存在");
}
// 证书类型由公钥是否匹配本地实体自动推导,避免调用方伪造 ENTITY/USER 边界。
Optional<Long> matchedEntityId = entityService.findByCertificatePublicKey(certificate.getPublicKey().getEncoded());
Long finalEntityId = matchedEntityId.orElse(null);
String certType = matchedEntityId.isPresent() ? "ENTITY" : "USER";
// 先按签名解析真实 issuer,再递归校验当前可信 CA 链,不能只依赖 issuer DN 字符串相等。
var issuer = trustedCertService.resolveIssuer(certificate);
trustedCertService.validateParentChain(issuer);
String issuerDn = certificate.getIssuerX500Principal().getName();
String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase();
if (isRevoked(issuerDn, serialNumber)) {
// CRL 可能先于证书入库;拒绝导入前回填 subject,保证吊销明细检索仍可按主体查到。
fillRevokedSubjectIfMissing(issuerDn, serialNumber, certificate.getSubjectX500Principal().getName(), algoType);
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书已被CRL吊销");
}
@@ -154,10 +161,18 @@ public class CertificateService {
}
private String resolveRuntimeStatus(CertificateEntity entity) {
// 运行态以“当前有效信任”为准:吊销优先,其次时间窗口,最后再用当前可信 CA 集合重验证书链。
if (isRevoked(entity.getIssuerDn(), entity.getSerialNumber())) {
return "REVOKED";
}
return CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo());
String timeStatus = CertRuntimeStatusResolver.resolve(entity.getValidFrom(), entity.getValidTo());
if (!"VALID".equals(timeStatus)) {
return timeStatus;
}
if (StringUtils.hasText(entity.getCertData()) && !trustedCertService.validateCertificateChain(entity.getCertData())) {
return "INVALID_CHAIN";
}
return timeStatus;
}
private boolean isRevoked(String issuerDn, String serialNumber) {
@@ -195,6 +210,10 @@ public class CertificateService {
}
private void assertEndEntityCertificate(X509Certificate certificate) {
// RFC 5280 要求无法处理的 critical extension 必须拒绝,不能静默当作普通扩展。
if (CertPemSupport.hasUnsupportedCriticalExtension(certificate)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书包含不支持的关键扩展");
}
if (certificate.getBasicConstraints() >= 0) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体/用户证书不能是CA证书");
}
@@ -202,5 +221,24 @@ public class CertificateService {
if (keyUsage != null && (keyUsage.length <= 0 || !keyUsage[0])) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体/用户证书必须允许数字签名");
}
assertEndEntityExtendedKeyUsage(certificate);
}
private void assertEndEntityExtendedKeyUsage(X509Certificate certificate) {
List<String> extendedKeyUsage;
try {
extendedKeyUsage = certificate.getExtendedKeyUsage();
} catch (Exception ex) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书扩展用途无效");
}
if (extendedKeyUsage == null || extendedKeyUsage.isEmpty()) {
return;
}
if (extendedKeyUsage.contains(KeyPurposeId.anyExtendedKeyUsage.getId())
|| extendedKeyUsage.contains(KeyPurposeId.id_kp_clientAuth.getId())
|| extendedKeyUsage.contains(KeyPurposeId.id_kp_serverAuth.getId())) {
return;
}
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "实体/用户证书扩展用途不支持业务认证");
}
}
@@ -3,9 +3,11 @@ package com.cisd.tms.modules.cert.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.cert.dto.CrlImportTaskResponse;
import com.cisd.tms.modules.cert.dto.CrlListRequest;
import com.cisd.tms.modules.cert.dto.CrlRevokedItemResponse;
import com.cisd.tms.modules.cert.entity.CertificateCrlEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlImportTaskEntity;
import com.cisd.tms.modules.cert.entity.CertificateCrlRevokedEntity;
import com.cisd.tms.modules.cert.entity.CertificateEntity;
import com.cisd.tms.modules.cert.entity.TrustedCertEntity;
@@ -16,42 +18,238 @@ import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x509.CRLNumber;
import org.bouncycastle.asn1.x509.Extension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@Service
public class CrlService {
private static final Logger log = LoggerFactory.getLogger(CrlService.class);
private static final int REVOKED_BATCH_SIZE = 1000;
private static final DateTimeFormatter TASK_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private final CrlRepository crlRepository;
private final CertificateRepository certificateRepository;
private final TrustedCertService trustedCertService;
private final Path crlTempDir;
private final long maxCrlFileSizeBytes;
private final Executor backgroundExecutor;
private final TransactionTemplate transactionTemplate;
public CrlService(
CrlRepository crlRepository,
CertificateRepository certificateRepository,
TrustedCertService trustedCertService
) {
this(
crlRepository,
certificateRepository,
trustedCertService,
Path.of(System.getProperty("java.io.tmpdir"), "tms-crl-import").toString(),
500L * 1024 * 1024,
Executors.newSingleThreadExecutor(),
null
);
}
@Autowired
public CrlService(
CrlRepository crlRepository,
CertificateRepository certificateRepository,
TrustedCertService trustedCertService,
@Value("${tms.cert.crl.temp-dir:/home/tms/tmp/crl-import}") String crlTempDir,
@Value("${tms.cert.crl.max-file-size-bytes:524288000}") long maxCrlFileSizeBytes,
PlatformTransactionManager transactionManager
) {
this(
crlRepository,
certificateRepository,
trustedCertService,
crlTempDir,
maxCrlFileSizeBytes,
Executors.newSingleThreadExecutor(),
transactionManager
);
}
CrlService(
CrlRepository crlRepository,
CertificateRepository certificateRepository,
TrustedCertService trustedCertService,
String crlTempDir,
long maxCrlFileSizeBytes
) {
this(
crlRepository,
certificateRepository,
trustedCertService,
crlTempDir,
maxCrlFileSizeBytes,
Executors.newSingleThreadExecutor(),
null
);
}
CrlService(
CrlRepository crlRepository,
CertificateRepository certificateRepository,
TrustedCertService trustedCertService,
String crlTempDir,
long maxCrlFileSizeBytes,
Executor backgroundExecutor
) {
this(
crlRepository,
certificateRepository,
trustedCertService,
crlTempDir,
maxCrlFileSizeBytes,
backgroundExecutor,
null
);
}
private CrlService(
CrlRepository crlRepository,
CertificateRepository certificateRepository,
TrustedCertService trustedCertService,
String crlTempDir,
long maxCrlFileSizeBytes,
Executor backgroundExecutor,
PlatformTransactionManager transactionManager
) {
this.crlRepository = crlRepository;
this.certificateRepository = certificateRepository;
this.trustedCertService = trustedCertService;
this.crlTempDir = Path.of(crlTempDir);
this.maxCrlFileSizeBytes = maxCrlFileSizeBytes;
this.backgroundExecutor = backgroundExecutor;
this.transactionTemplate = transactionManager == null ? null : new TransactionTemplate(transactionManager);
}
@Transactional
public void importCrl(MultipartFile file) {
assertFileSizeAllowed(file);
Path uploadFile;
try {
X509CRL crl = CertPemSupport.parseCrl(file.getBytes());
uploadFile = copyUploadToTempFile(file);
} catch (Exception ex) {
log.warn("CRL upload materialization 执行失败:{}", ex.toString());
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL文件保存失败");
}
try {
importCrlFile(uploadFile);
} finally {
deleteQuietly(uploadFile);
}
}
public CrlImportTaskResponse submitImport(MultipartFile file) {
assertFileSizeAllowed(file);
Path uploadFile;
try {
// 大 CRL 不能在请求线程里转 byte[];先落临时文件,再把解析导入交给后台任务。
uploadFile = copyUploadToTempFile(file);
} catch (Exception ex) {
log.warn("CRL upload materialization 执行失败:{}", ex.toString());
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL文件保存失败");
}
CertificateCrlImportTaskEntity task = new CertificateCrlImportTaskEntity()
.setTaskId(buildTaskId())
.setOriginalFilename(file.getOriginalFilename())
.setFileSize(file.getSize())
.setStatus("PENDING")
.setRevokedCount(0);
task.setCreateTime(LocalDateTime.now());
task.setUpdateTime(LocalDateTime.now());
try {
crlRepository.saveImportTask(task);
CompletableFuture.runAsync(() -> runSubmittedImport(task.getTaskId(), uploadFile), backgroundExecutor);
} catch (RuntimeException ex) {
deleteQuietly(uploadFile);
throw ex;
}
return toTaskResponse(task);
}
public CrlImportTaskResponse getImportTask(String taskId) {
CertificateCrlImportTaskEntity task = crlRepository.findImportTaskByTaskId(taskId)
.orElseThrow(() -> new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL导入任务不存在"));
return toTaskResponse(task);
}
private void runSubmittedImport(String taskId, Path uploadFile) {
crlRepository.updateImportTaskRunning(taskId, LocalDateTime.now());
try {
// 元数据、CRL 指纹和 revoked 明细必须在同一事务里落库,避免任务成功但明细半导入。
ImportResult result = runImportInTransaction(uploadFile);
crlRepository.updateImportTaskSuccess(
taskId,
result.crlId(),
result.fingerprint(),
result.revokedCount(),
LocalDateTime.now()
);
} catch (BizException ex) {
crlRepository.updateImportTaskFailed(taskId, ex.getMessage(), LocalDateTime.now());
} catch (Exception ex) {
log.warn("CRL async import 执行失败:{}", ex.toString());
crlRepository.updateImportTaskFailed(taskId, "CRL导入失败", LocalDateTime.now());
} finally {
deleteQuietly(uploadFile);
}
}
private ImportResult runImportInTransaction(Path uploadFile) {
if (transactionTemplate == null) {
return importCrlFile(uploadFile);
}
return transactionTemplate.execute(status -> importCrlFile(uploadFile));
}
private ImportResult importCrlFile(Path uploadFile) {
MaterializedCrl materialized = null;
try {
// PEM 会流式解码成 DER 临时文件;DER 直接复用上传临时文件,避免 500MB CRL 进入堆内存。
materialized = materializeCrlToDer(uploadFile);
X509CRL crl;
try (InputStream inputStream = Files.newInputStream(materialized.derFile())) {
crl = CertPemSupport.parseCrl(inputStream);
}
assertEffective(crl);
String fingerprint = CertPemSupport.sha256Fingerprint(crl);
String fingerprint = materialized.fingerprint();
if (crlRepository.findCrlByFingerprint(fingerprint).isPresent()) {
throw new BizException(ErrorCode.CONFLICT.getCode(), "CRL已存在");
}
// CRL 只有被当前可信 CA 签名且签发者允许 cRLSign 时才参与吊销判断。
TrustedCertEntity issuer = trustedCertService.resolveCrlIssuer(crl);
trustedCertService.validateParentChain(issuer);
String issuerDn = crl.getIssuerX500Principal().getName();
@@ -60,29 +258,36 @@ public class CrlService {
algoType = "SM2";
}
String crlNumber = crlNumber(crl);
Set<? extends X509CRLEntry> revokedCertificates = crl.getRevokedCertificates();
CertificateCrlEntity entity = new CertificateCrlEntity()
.setIssuerDn(issuerDn)
.setCrlNumber(crlNumber)
.setAlgoType(algoType)
.setFingerprint(fingerprint)
.setCrlData(CertPemSupport.toPem(crl))
// 原始 CRL 不长期保存;证据和查询依赖 fingerprint、元数据和 revoked 明细。
.setCrlData("")
.setThisUpdate(toLocalDateTime(crl.getThisUpdate()))
.setNextUpdate(crl.getNextUpdate() == null ? null : toLocalDateTime(crl.getNextUpdate()))
.setRevokedCount(crl.getRevokedCertificates() == null ? 0 : crl.getRevokedCertificates().size())
.setRevokedCount(revokedCertificates == null ? 0 : revokedCertificates.size())
.setImportTime(LocalDateTime.now())
.setCreateBy("system");
entity.setCreateTime(LocalDateTime.now());
entity.setUpdateTime(LocalDateTime.now());
crlRepository.saveCrl(entity);
if (crl.getRevokedCertificates() != null) {
for (X509CRLEntry entry : crl.getRevokedCertificates()) {
saveRevoked(entity.getId(), issuerDn, crlNumber, algoType, entry);
}
int revokedCount = 0;
if (revokedCertificates != null) {
revokedCount = saveRevokedRows(entity.getId(), issuerDn, crlNumber, algoType, revokedCertificates);
}
return new ImportResult(entity.getId(), fingerprint, revokedCount);
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
log.warn("CRL import 执行失败:{}", ex.toString());
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL导入失败");
} finally {
if (materialized != null) {
deleteQuietly(materialized.derFile());
}
}
}
@@ -101,7 +306,31 @@ public class CrlService {
crlRepository.deleteCrlById(id);
}
private void saveRevoked(Long crlId, String issuerDn, String crlNumber, String crlAlgoType, X509CRLEntry entry) {
private int saveRevokedRows(
Long crlId,
String issuerDn,
String crlNumber,
String crlAlgoType,
Set<? extends X509CRLEntry> revokedCertificates
) {
// 大 CRL 可能包含几十万条吊销明细,批量插入避免每条记录一次数据库往返。
List<CertificateCrlRevokedEntity> batch = new ArrayList<>(Math.min(REVOKED_BATCH_SIZE, revokedCertificates.size()));
int count = 0;
for (X509CRLEntry entry : revokedCertificates) {
batch.add(buildRevoked(crlId, issuerDn, crlNumber, crlAlgoType, entry));
count++;
if (batch.size() >= REVOKED_BATCH_SIZE) {
crlRepository.saveRevokedBatch(batch);
batch.clear();
}
}
if (!batch.isEmpty()) {
crlRepository.saveRevokedBatch(batch);
}
return count;
}
private CertificateCrlRevokedEntity buildRevoked(Long crlId, String issuerDn, String crlNumber, String crlAlgoType, X509CRLEntry entry) {
String serialNumber = entry.getSerialNumber().toString(16).toUpperCase();
Optional<CertificateEntity> certificate = certificateRepository.findByIssuerDnAndSerialNumber(issuerDn, serialNumber);
CertificateCrlRevokedEntity revoked = new CertificateCrlRevokedEntity()
@@ -115,7 +344,7 @@ public class CrlService {
.setRevocationReason(reasonName(entry));
revoked.setCreateTime(LocalDateTime.now());
revoked.setUpdateTime(LocalDateTime.now());
crlRepository.saveRevoked(revoked);
return revoked;
}
private CrlRevokedItemResponse toItem(CertificateCrlRevokedEntity entity) {
@@ -138,7 +367,11 @@ public class CrlService {
if (thisUpdate.isAfter(now)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL生效时间晚于当前时间");
}
if (crl.getNextUpdate() != null && toLocalDateTime(crl.getNextUpdate()).isBefore(now)) {
if (crl.getNextUpdate() == null) {
// RFC 5280 要求 CRL issuer 提供 nextUpdate;缺失时不能让吊销状态永久生效。
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL缺少下次更新时间");
}
if (toLocalDateTime(crl.getNextUpdate()).isBefore(now)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL已过期");
}
}
@@ -179,4 +412,151 @@ public class CrlService {
private LocalDateTime toLocalDateTime(java.util.Date date) {
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}
private void assertFileSizeAllowed(MultipartFile file) {
long fileSize = file.getSize();
if (fileSize > maxCrlFileSizeBytes) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL文件过大,最大支持" + readableSize(maxCrlFileSizeBytes));
}
}
private Path copyUploadToTempFile(MultipartFile file) throws java.io.IOException {
Files.createDirectories(crlTempDir);
Path uploadFile = Files.createTempFile(crlTempDir, "crl-upload-", ".tmp");
if (file.getSize() > 0) {
try {
file.transferTo(uploadFile);
if (Files.size(uploadFile) == file.getSize()) {
return uploadFile;
}
} catch (Exception ex) {
Files.deleteIfExists(uploadFile);
uploadFile = Files.createTempFile(crlTempDir, "crl-upload-", ".tmp");
}
}
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, uploadFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
return uploadFile;
}
private MaterializedCrl materializeCrlToDer(Path uploadFile) throws java.io.IOException {
if (!looksLikePem(uploadFile)) {
return new MaterializedCrl(uploadFile, sha256File(uploadFile));
}
Path derFile = Files.createTempFile(crlTempDir, "crl-", ".der");
try {
String fingerprint = decodePemToDer(uploadFile, derFile);
return new MaterializedCrl(derFile, fingerprint);
} finally {
deleteQuietly(uploadFile);
}
}
private String buildTaskId() {
return "CRL" + TASK_ID_TS.format(LocalDateTime.now()) + String.format("%04d", java.util.concurrent.ThreadLocalRandom.current().nextInt(10_000));
}
private CrlImportTaskResponse toTaskResponse(CertificateCrlImportTaskEntity task) {
CrlImportTaskResponse response = new CrlImportTaskResponse();
response.setTaskId(task.getTaskId());
response.setStatus(task.getStatus());
response.setOriginalFilename(task.getOriginalFilename());
response.setFileSize(task.getFileSize());
response.setCrlId(task.getCrlId());
response.setFingerprint(task.getFingerprint());
response.setRevokedCount(task.getRevokedCount());
response.setErrorMessage(task.getErrorMessage());
response.setStartedAt(task.getStartedAt());
response.setFinishedAt(task.getFinishedAt());
return response;
}
private boolean looksLikePem(Path file) throws java.io.IOException {
try (InputStream inputStream = Files.newInputStream(file)) {
byte[] prefix = inputStream.readNBytes(64);
return new String(prefix, java.nio.charset.StandardCharsets.US_ASCII).contains("-----BEGIN");
}
}
private String decodePemToDer(Path pemFile, Path derFile) throws java.io.IOException {
MessageDigest digest = sha256Digest();
Base64.Decoder decoder = Base64.getDecoder();
boolean inCrl = false;
try (BufferedReader reader = Files.newBufferedReader(pemFile, java.nio.charset.StandardCharsets.US_ASCII);
OutputStream outputStream = Files.newOutputStream(derFile)) {
String line;
while ((line = reader.readLine()) != null) {
String trim = line.trim();
if (trim.startsWith("-----BEGIN ") && trim.contains("CRL")) {
inCrl = true;
continue;
}
if (trim.startsWith("-----END ") && trim.contains("CRL")) {
break;
}
if (!inCrl || trim.isEmpty()) {
continue;
}
byte[] decoded = decoder.decode(trim);
digest.update(decoded);
outputStream.write(decoded);
}
}
return hex(digest.digest());
}
private String sha256File(Path file) throws java.io.IOException {
MessageDigest digest = sha256Digest();
try (InputStream inputStream = Files.newInputStream(file)) {
byte[] buffer = new byte[1024 * 1024];
int read;
while ((read = inputStream.read(buffer)) >= 0) {
digest.update(buffer, 0, read);
}
}
return hex(digest.digest());
}
private MessageDigest sha256Digest() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("SHA-256算法不可用", ex);
}
}
private String hex(byte[] bytes) {
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
builder.append(String.format("%02X", b));
}
return builder.toString();
}
private String readableSize(long bytes) {
if (bytes % (1024L * 1024L) == 0) {
return (bytes / 1024L / 1024L) + "MB";
}
if (bytes % 1024L == 0) {
return (bytes / 1024L) + "KB";
}
return bytes + "B";
}
private void deleteQuietly(Path path) {
if (path == null) {
return;
}
try {
Files.deleteIfExists(path);
} catch (Exception ignored) {
}
}
private record MaterializedCrl(Path derFile, String fingerprint) {
}
private record ImportResult(Long crlId, String fingerprint, int revokedCount) {
}
}
@@ -7,6 +7,7 @@ import com.cisd.tms.integration.crypto.pcie.PcieUserKeyType;
import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult;
import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyPairRequest;
import com.cisd.tms.integration.crypto.pcie.model.UserKeySm2SignRequest;
import com.cisd.tms.integration.crypto.pcie.jna.RsaRefPublicKey;
import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService;
import com.cisd.tms.modules.cert.dto.EntityCreateRequest;
import com.cisd.tms.modules.cert.dto.EntityCreateResponse;
@@ -35,8 +36,12 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
@@ -79,6 +84,7 @@ public class EntityService {
@Transactional
public synchronized EntityCreateResponse create(EntityCreateRequest request) {
String algoType = normalizeAlgoType(request.getAlgoType());
// 密钥索引同时存在卡内状态和数据库记录;单 JVM 内串行分配,数据库唯一索引仍是最终保护。
int keyIdx = resolveNextKeyIdx();
int keyLen = SM2.equals(algoType) ? 256 : 2048;
GenerateKeyPairRequest generateRequest = new GenerateKeyPairRequest();
@@ -92,7 +98,9 @@ public class EntityService {
pcieCryptoService.generateKeyPairRsa(generateRequest);
}
try {
// 备份块是密码卡返回的二进制结构,Base64 后可能远超短 VARCHAR,库表字段需保持 TEXT。
BackupDataResult backup = pcieCryptoService.backupUserKey(keyIdx, PcieUserKeyType.SIGN.getCode(), 4096);
// publicKey 保存卡原始公钥结构;证书导入时再按算法转换为 X.509 公钥进行匹配。
BackupDataResult publicKey = SM2.equals(algoType)
? pcieCryptoService.exportSignPublicKeyEcc(keyIdx)
: pcieCryptoService.exportSignPublicKeyRsa(keyIdx);
@@ -113,6 +121,7 @@ public class EntityService {
return new EntityCreateResponse(entity.getId());
} catch (RuntimeException ex) {
try {
// 数据库落库失败时清理刚生成的卡内用户密钥,避免卡状态与实体表长期不一致。
pcieCryptoService.deleteUserKey(keyIdx);
} catch (Exception ignored) {
}
@@ -156,6 +165,7 @@ public class EntityService {
X500Name subjectName = subject.toX500Name();
String subjectDn = subjectName.toString();
PublicKey publicKey = Sm2KeySupport.toSm2PublicKey(Base64.getDecoder().decode(entity.getPublicKey()));
// P10 使用卡内签名密钥完成 SM2-with-SM3 签名,私钥始终不离开密码卡。
PKCS10CertificationRequestBuilder builder =
new JcaPKCS10CertificationRequestBuilder(subjectName, publicKey);
PKCS10CertificationRequest csr = builder.build(new CardContentSigner(entity.getKeyIdx(), pcieCryptoService));
@@ -225,18 +235,107 @@ public class EntityService {
}
private boolean certificatePublicKeyMatches(KeyEntity entity, byte[] certificatePublicKey) {
if (entity == null || !SM2.equalsIgnoreCase(entity.getAlgoType()) || !StringUtils.hasText(entity.getPublicKey())) {
// ENTITY 证书绑定只看证书公钥是否等于本地实体公钥,不能信任调用方传入的证书类型。
if (entity == null || !StringUtils.hasText(entity.getAlgoType()) || !StringUtils.hasText(entity.getPublicKey())) {
return false;
}
try {
byte[] entityPublicKey = Base64.getDecoder().decode(entity.getPublicKey());
byte[] entityX509Key = Sm2KeySupport.toSm2PublicKey(entityPublicKey).getEncoded();
return Arrays.equals(certificatePublicKey, entityX509Key);
if (SM2.equalsIgnoreCase(entity.getAlgoType())) {
byte[] entityX509Key = Sm2KeySupport.toSm2PublicKey(entityPublicKey).getEncoded();
return Arrays.equals(certificatePublicKey, entityX509Key);
}
if (RSA.equalsIgnoreCase(entity.getAlgoType())) {
return rsaPublicKeyMatches(entityPublicKey, certificatePublicKey);
}
return false;
} catch (Exception ex) {
return false;
}
}
private boolean rsaPublicKeyMatches(byte[] entityPublicKey, byte[] certificatePublicKey) {
if (Arrays.equals(entityPublicKey, certificatePublicKey)) {
return true;
}
if (matchesRsaX509PublicKey(entityPublicKey, certificatePublicKey)) {
return true;
}
return matchesRsaRefPublicKey(entityPublicKey, certificatePublicKey);
}
private boolean matchesRsaX509PublicKey(byte[] entityPublicKey, byte[] certificatePublicKey) {
try {
PublicKey publicKey = KeyFactory.getInstance(RSA)
.generatePublic(new X509EncodedKeySpec(entityPublicKey));
return Arrays.equals(certificatePublicKey, publicKey.getEncoded());
} catch (Exception ex) {
return false;
}
}
private boolean matchesRsaRefPublicKey(byte[] entityPublicKey, byte[] certificatePublicKey) {
RsaRefPublicKey refPublicKey = new RsaRefPublicKey();
int size = refPublicKey.size();
if (entityPublicKey.length != size) {
return false;
}
try {
refPublicKey.getPointer().clear(size);
refPublicKey.getPointer().write(0, entityPublicKey, 0, size);
refPublicKey.read();
int modulusLength = (refPublicKey.bits + 7) / 8;
if (modulusLength <= 0 || modulusLength > RsaRefPublicKey.MAX_LEN) {
return false;
}
// 不同 SDF 实现可能左右对齐 m/e 字段;同时尝试常见编码,避免 RSA 实体无法绑定证书。
byte[][] modulusCandidates = {
trimLeadingZeros(Arrays.copyOf(refPublicKey.m, modulusLength)),
trimLeadingZeros(Arrays.copyOfRange(refPublicKey.m, RsaRefPublicKey.MAX_LEN - modulusLength, RsaRefPublicKey.MAX_LEN))
};
byte[][] exponentCandidates = {
trimLeadingZeros(refPublicKey.e),
trimTrailingZeros(refPublicKey.e)
};
KeyFactory keyFactory = KeyFactory.getInstance(RSA);
for (byte[] modulus : modulusCandidates) {
if (modulus.length == 0) {
continue;
}
for (byte[] exponent : exponentCandidates) {
if (exponent.length == 0) {
continue;
}
PublicKey publicKey = keyFactory.generatePublic(
new RSAPublicKeySpec(new BigInteger(1, modulus), new BigInteger(1, exponent))
);
if (Arrays.equals(certificatePublicKey, publicKey.getEncoded())) {
return true;
}
}
}
return false;
} catch (Exception ex) {
return false;
}
}
private byte[] trimLeadingZeros(byte[] value) {
int start = 0;
while (start < value.length && value[start] == 0) {
start++;
}
return Arrays.copyOfRange(value, start, value.length);
}
private byte[] trimTrailingZeros(byte[] value) {
int end = value.length;
while (end > 0 && value[end - 1] == 0) {
end--;
}
return Arrays.copyOf(value, end);
}
private int resolveNextKeyIdx() {
List<Integer> used = keyEntityRepository.findUsedKeyIdx();
for (int idx = 2; idx <= 8; idx++) {
@@ -18,6 +18,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
@@ -59,6 +60,7 @@ public class TrustedCertService {
public ImportCertificateResponse importTrusted(MultipartFile file, String alias) {
try {
X509Certificate certificate = CertPemSupport.parseCertificate(file.getBytes());
// 可信库只保存 CA 证书;终端证书必须走 CertificateService,避免信任锚和业务证书混用。
assertValidTime(certificate);
assertTrustedCaCertificate(certificate);
String fingerprint = CertPemSupport.sha256Fingerprint(certificate);
@@ -67,8 +69,10 @@ public class TrustedCertService {
}
int caLevel = isSelfSigned(certificate) ? 0 : 1;
if (caLevel == 0) {
// 根 CA 必须能自签自验,防止把 subject=issuer 的伪造证书放入信任锚。
certificate.verify(certificate.getPublicKey(), "BC");
} else {
// 中间 CA 必须由当前可信链签发;仅导入叶子中间证书不足以建立信任。
TrustedCertEntity issuer = resolveIssuer(certificate);
validateParentChain(issuer);
}
@@ -143,6 +147,7 @@ public class TrustedCertService {
}
for (TrustedCertEntity candidate : matched) {
try {
// 同名 CA 可能存在轮换或交叉签发,必须逐个用公钥验签找到真实签发者。
X509Certificate issuerCert = CertPemSupport.parseCertificate(candidate.getCertData().getBytes());
assertTrustedCaCertificate(issuerCert);
certificate.verify(issuerCert.getPublicKey(), "BC");
@@ -162,6 +167,7 @@ public class TrustedCertService {
}
for (TrustedCertEntity candidate : matched) {
try {
// CRL issuer 也按签名验证解析,避免仅凭 DN 命中错误 CA 后错误采信吊销列表。
X509Certificate issuerCert = CertPemSupport.parseCertificate(candidate.getCertData().getBytes());
assertCrlIssuerCertificate(issuerCert);
crl.verify(issuerCert.getPublicKey(), "BC");
@@ -178,6 +184,7 @@ public class TrustedCertService {
assertValidTime(certificate);
assertTrustedCaCertificate(certificate);
if (entity.getSubjectDn().equals(entity.getIssuerDn())) {
// 递归到自签根时仍要验自签,保证整条路径最终落在有效信任锚上。
certificate.verify(certificate.getPublicKey(), "BC");
return;
}
@@ -190,6 +197,19 @@ public class TrustedCertService {
}
}
public boolean validateCertificateChain(String certData) {
try {
// 用于列表/详情运行态:每次按当前可信库重新建链,反映 CA 删除、过期或强制变更。
X509Certificate certificate = CertPemSupport.parseCertificate(certData.getBytes(StandardCharsets.UTF_8));
assertValidTime(certificate);
TrustedCertEntity issuer = resolveIssuer(certificate);
validateParentChain(issuer);
return true;
} catch (Exception ex) {
return false;
}
}
private TrustedCertItemResponse toItem(TrustedCertEntity entity) {
TrustedCertItemResponse response = new TrustedCertItemResponse();
response.setId(entity.getId());
@@ -221,6 +241,9 @@ public class TrustedCertService {
}
private void assertTrustedCaCertificate(X509Certificate certificate) {
if (CertPemSupport.hasUnsupportedCriticalExtension(certificate)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书包含不支持的关键扩展");
}
if (certificate.getBasicConstraints() < 0) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "可信证书必须是CA证书");
}
@@ -231,6 +254,9 @@ public class TrustedCertService {
}
private void assertCrlIssuerCertificate(X509Certificate certificate) {
if (CertPemSupport.hasUnsupportedCriticalExtension(certificate)) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "证书包含不支持的关键扩展");
}
if (certificate.getBasicConstraints() < 0) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "CRL签发者必须是CA证书");
}
@@ -1,8 +1,11 @@
package com.cisd.tms.modules.cert.support;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.PublicKey;
@@ -12,10 +15,30 @@ import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Locale;
import java.util.Set;
public final class CertPemSupport {
private static final String PROVIDER = "BC";
// 只列入当前模块能理解并正确执行语义的 critical extension,其他关键扩展必须拒绝。
private static final Set<String> SUPPORTED_CRITICAL_EXTENSION_OIDS = Set.of(
Extension.basicConstraints.getId(),
Extension.keyUsage.getId(),
Extension.extendedKeyUsage.getId(),
Extension.subjectKeyIdentifier.getId(),
Extension.authorityKeyIdentifier.getId(),
Extension.subjectAlternativeName.getId(),
Extension.issuerAlternativeName.getId(),
Extension.cRLDistributionPoints.getId(),
Extension.freshestCRL.getId(),
Extension.authorityInfoAccess.getId(),
Extension.subjectInfoAccess.getId(),
Extension.certificatePolicies.getId(),
Extension.policyMappings.getId(),
Extension.policyConstraints.getId(),
Extension.inhibitAnyPolicy.getId(),
Extension.nameConstraints.getId()
);
static {
if (Security.getProvider(PROVIDER) == null) {
@@ -46,6 +69,15 @@ public final class CertPemSupport {
}
}
public static X509CRL parseCrl(InputStream inputStream) {
try {
CertificateFactory factory = CertificateFactory.getInstance("X.509", PROVIDER);
return (X509CRL) factory.generateCRL(new BufferedInputStream(inputStream));
} catch (Exception e) {
throw new IllegalArgumentException("CRL文件无效", e);
}
}
public static String toPem(X509Certificate certificate) {
try {
String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
@@ -89,6 +121,7 @@ public final class CertPemSupport {
if ("RSA".equalsIgnoreCase(publicKey.getAlgorithm())) {
return "RSA";
}
// SM2 证书在不同 provider 下可能暴露为 EC 公钥,因此同时参考签名算法名称和国密 OID。
String signatureName = certificate.getSigAlgName();
String signatureOid = certificate.getSigAlgOID();
if ((signatureName != null && signatureName.toUpperCase(Locale.ROOT).contains("SM2"))
@@ -101,6 +134,15 @@ public final class CertPemSupport {
throw new IllegalArgumentException("不支持的证书算法:" + publicKey.getAlgorithm());
}
public static boolean hasUnsupportedCriticalExtension(X509Certificate certificate) {
// X509Certificate#getCriticalExtensionOIDs() 已剥离 non-critical 扩展,只需关注必须理解的集合。
Set<String> criticalOids = certificate.getCriticalExtensionOIDs();
if (criticalOids == null || criticalOids.isEmpty()) {
return false;
}
return criticalOids.stream().anyMatch(oid -> !SUPPORTED_CRITICAL_EXTENSION_OIDS.contains(oid));
}
private static byte[] toDer(byte[] bytes, String... labels) {
String text = new String(bytes, StandardCharsets.UTF_8);
for (String label : labels) {
@@ -2916,7 +2916,7 @@ public class NetworkConfigService {
int exitCode = process.waitFor();
if (exitCode != 0) {
log.error("Command execution failed. Exit code: {}, Command: [{}], Output: {}", exitCode, commandArgs, output);
log.error("Command execution 执行失败. Exit code: {}, Command: [{}], Output: {}", exitCode, commandArgs, output);
throw new RuntimeException("系统底层操作执行失败");}
return output;
} catch (IOException e){
@@ -2927,7 +2927,7 @@ public class NetworkConfigService {
process.destroy();
}
Thread.currentThread().interrupt();
log.error("Process was interrupted while executing command: [{}]", commandArgs, e);
log.error("Process was 被中断 while executing command: [{}]", commandArgs, e);
throw new RuntimeException("服务器内部处理中断");}
}
@@ -38,7 +38,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
String timezone = request.getTimezone();
if (timezone == null || !timezone.matches("^[A-Za-z0-9/_-]+$")) {
throw new IllegalArgumentException("Invalid or empty timezone format");
throw new IllegalArgumentException("时区格式为空或无效");
}
if (!ZoneId.getAvailableZoneIds().contains(timezone)) {
@@ -47,12 +47,12 @@ public class TimeConfigServiceImpl implements TimeConfigService {
String datetime = request.getDatetime();
if (datetime == null) {
throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM");
throw new IllegalArgumentException("日期时间格式为空或无效,期望格式:YYYY-MM-DD HH:MM");
}
try {
LocalDateTime.parse(datetime, FORMATTER);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM", e);
throw new IllegalArgumentException("日期时间格式为空或无效,期望格式:YYYY-MM-DD HH:MM", e);
}
try{
@@ -80,20 +80,20 @@ public class TimeConfigServiceImpl implements TimeConfigService {
List<String> servers = request.getNtpServers();
if (servers == null || servers.isEmpty()) {
throw new IllegalArgumentException("At least one NTP server IP must be provided in NTP mode");
throw new IllegalArgumentException("NTP模式下至少需要提供一个NTP服务器IP");
}
int interval = request.getSyncInterval() != null ? request.getSyncInterval() : 512;
if (interval <= 0) {
throw new IllegalArgumentException("SyncInterval must be a positive integer.");
throw new IllegalArgumentException("同步间隔必须为正整数。");
}
if ((interval & (interval - 1)) != 0) {
throw new IllegalArgumentException("SyncInterval must be a power of 2.");
throw new IllegalArgumentException("同步间隔必须为2的幂。");
}
int MIN_INTERVAL = 8;
int MAX_INTERVAL = 131072;
if (interval < MIN_INTERVAL || interval > MAX_INTERVAL) {
throw new IllegalArgumentException(String.format("SyncInterval must be between %d and %d.", MIN_INTERVAL, MAX_INTERVAL));
throw new IllegalArgumentException(String.format("同步间隔必须在%d到%d之间。", MIN_INTERVAL, MAX_INTERVAL));
}
int pollExp = Integer.numberOfTrailingZeros(interval);
@@ -101,12 +101,12 @@ public class TimeConfigServiceImpl implements TimeConfigService {
StringBuilder serverConfLines = new StringBuilder();
for (String server : servers) {
if (!server.matches("^[A-Za-z0-9.-]+$")) {
throw new IllegalArgumentException("Invalid NTP server address format: " + server);
throw new IllegalArgumentException("NTP服务器地址格式无效:" + server);
}
String pingResult = executeCommand("ping", "-c", "1", "-W", "1", server);
if (!pingResult.contains("1 received")) {
throw new RuntimeException("NTP server is unreachable via ping: " + server);
throw new RuntimeException("NTP服务器ping不通:" + server);
}
serverConfLines.append("server ").append(server)
.append(" iburst minpoll ").append(pollExp)
@@ -131,7 +131,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
String status = executeCommand("chronyc", "tracking");
if (!status.contains("Reference ID")) {
throw new RuntimeException("NTP sync status check failed");
throw new RuntimeException("NTP sync status check 执行失败");
}
} catch (Exception e) {
@@ -141,13 +141,13 @@ public class TimeConfigServiceImpl implements TimeConfigService {
executeCommand("systemctl", "restart", "chronyd");
}
} catch (Exception rollbackEx) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "Failed to apply new Chrony configuration. CRITICAL: Rollback also failed!");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "Failed to apply new Chrony configuration. CRITICAL: Rollback also 执行失败!");
}
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "Failed to apply new Chrony configuration. Rolled back to original state.");
}
} else {
throw new IllegalArgumentException("Unknown mode: " + mode);
throw new IllegalArgumentException("未知模式:" + mode);
}
}
@@ -172,7 +172,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
String outputStr = outputBuilder.toString().trim();
if (exitCode != 0) {
log.error("Command execution failed. Exit code: {}, Command: {}, Output: {}",
log.error("Command execution 执行失败. Exit code: {}, Command: {}, Output: {}",
exitCode, Arrays.toString(commandArgs), outputStr);
throw new RuntimeException("系统底层操作执行失败");
}
@@ -186,7 +186,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
process.destroy();
}
Thread.currentThread().interrupt();
log.error("Process was interrupted while executing command: {}", Arrays.toString(commandArgs), e);
log.error("Process was 被中断 while executing command: {}", Arrays.toString(commandArgs), e);
throw new RuntimeException("服务器内部处理中断", e);
}
}
@@ -16,7 +16,7 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
Thread.sleep(120L);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("cpu sampling interrupted", ex);
throw new IllegalStateException("cpu sampling 被中断", ex);
}
CpuTimes after = readCpuTimes();
long totalDiff = after.total - before.total;
@@ -40,10 +40,10 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
}
}
} catch (IOException ex) {
throw new IllegalStateException("memory probe failed", ex);
throw new IllegalStateException("memory probe 执行失败", ex);
}
if (totalKb <= 0 || availableKb < 0) {
throw new IllegalStateException("memory probe returned invalid value");
throw new IllegalStateException("内存探测返回无效值");
}
double totalGb = totalKb / 1024D / 1024D;
double usedGb = (totalKb - availableKb) / 1024D / 1024D;
@@ -55,17 +55,17 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
try {
List<String> lines = runCommand("df -k /");
if (lines.size() < 2) {
throw new IllegalStateException("disk probe returned no data");
throw new IllegalStateException("磁盘探测未返回数据");
}
String[] parts = lines.get(1).trim().split("\\s+");
if (parts.length < 5) {
throw new IllegalStateException("disk probe returned invalid data");
throw new IllegalStateException("磁盘探测返回无效数据");
}
double totalGb = Long.parseLong(parts[1]) / 1024D / 1024D;
double usedGb = Long.parseLong(parts[2]) / 1024D / 1024D;
return new UsageSnapshot(round(usedGb), round(totalGb));
} catch (IOException ex) {
throw new IllegalStateException("disk probe failed", ex);
throw new IllegalStateException("disk probe 执行失败", ex);
}
}
@@ -80,7 +80,7 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
}
return new CpuTimes(idle, total);
} catch (IOException ex) {
throw new IllegalStateException("cpu probe failed", ex);
throw new IllegalStateException("cpu probe 执行失败", ex);
}
}
@@ -98,12 +98,12 @@ public class DefaultHardwareStatusProbe implements HardwareStatusProbe {
int exitCode = process.waitFor();
List<String> lines = new String(process.getInputStream().readAllBytes()).lines().toList();
if (exitCode != 0) {
throw new IllegalStateException("command failed: " + command);
throw new IllegalStateException("命令执行失败: " + command);
}
return lines;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("command interrupted: " + command, ex);
throw new IllegalStateException("command 被中断: " + command, ex);
}
}
@@ -109,9 +109,9 @@ public class DefaultSoftwareRuntimeProbe implements SoftwareRuntimeProbe {
List<Pattern> patterns = compilePatterns(tokens);
return processSnapshot().stream().anyMatch(commandLine -> matchesAny(commandLine, patterns));
} catch (PatternSyntaxException ex) {
throw new IllegalStateException("invalid process probe tokens: " + String.join(",", tokens), ex);
throw new IllegalStateException("进程探测命令参数无效:" + String.join(",", tokens), ex);
} catch (RuntimeException ex) {
throw new IllegalStateException("process probe failed: " + String.join(",", tokens), ex);
throw new IllegalStateException("process probe 执行失败:" + String.join(",", tokens), ex);
}
}
@@ -180,14 +180,14 @@ public class DefaultSoftwareRuntimeProbe implements SoftwareRuntimeProbe {
.filter(line -> !line.isBlank())
.toList();
if (exitCode != 0) {
throw new IllegalStateException("ps command failed");
throw new IllegalStateException("ps 命令执行失败");
}
return commandLines;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("process snapshot interrupted", ex);
throw new IllegalStateException("process snapshot 被中断", ex);
} catch (IOException ex) {
throw new IllegalStateException("process snapshot failed", ex);
throw new IllegalStateException("process snapshot 执行失败", ex);
}
}
@@ -197,9 +197,9 @@ public class DefaultSoftwareRuntimeProbe implements SoftwareRuntimeProbe {
return process.waitFor();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("command interrupted", ex);
throw new IllegalStateException("command 被中断", ex);
} catch (IOException ex) {
throw new IllegalStateException("command failed", ex);
throw new IllegalStateException("命令执行失败", ex);
}
}
@@ -36,7 +36,7 @@ public final class CryptoPayloadCodec {
public static Map<String, Object> lmkComponentPayload(BackupDataResult result) {
byte[] normalized = normalizeBinary(result.getData(), result.getLength());
if (normalized.length < 56) {
throw new IllegalStateException("Invalid LMK component length: " + normalized.length);
throw new IllegalStateException("LMK分量长度无效:" + normalized.length);
}
HexFormat hexFormat = HexFormat.of();
@@ -61,7 +61,7 @@ public final class CryptoPayloadCodec {
String lmkMacHex = requireText("lmkMacHex", request.getLmkMacHex());
Map<String, String> components = request.getComponents();
if (components == null || components.isEmpty()) {
throw new IllegalArgumentException("components must not be empty");
throw new IllegalArgumentException("components不能为空");
}
byte[] result = new byte[56];
@@ -35,10 +35,10 @@ public class FileServiceImpl implements FileService {
@Override
public FileUploadResponse upload(MultipartFile file) {
if (file == null || file.isEmpty() || file.getSize() <= 0) {
throw new IllegalArgumentException("upload file is empty");
throw new IllegalArgumentException("上传文件为空");
}
if (file.getSize() > fileStorageProperties.getMaxFileSizeBytes()) {
throw new IllegalArgumentException("upload file size exceeds limit");
throw new IllegalArgumentException("上传文件大小超过限制");
}
String originalFilename = sanitizeFilename(file.getOriginalFilename());
@@ -72,7 +72,7 @@ public class FileServiceImpl implements FileService {
@Override
public FileDetailResponse getDetail(String fileId) {
FileRecordEntity entity = fileRecordRepository.findByFileId(normalize(fileId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "file not found: " + normalize(fileId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "文件不存在:" + normalize(fileId)));
Path path = Paths.get(entity.getStoragePath()).normalize().toAbsolutePath();
FileDetailResponse response = new FileDetailResponse();
@@ -98,18 +98,18 @@ public class FileServiceImpl implements FileService {
private String sanitizeFilename(String originalFilename) {
String normalized = normalize(originalFilename);
if (normalized.isEmpty()) {
throw new IllegalArgumentException("original filename is blank");
throw new IllegalArgumentException("原始文件名为空");
}
try {
// 先剥离客户端传入文件名里的路径段,避免把目录信息带入服务端存储路径。
String fileName = Paths.get(normalized).getFileName().toString();
String sanitized = fileName.replaceAll("[^A-Za-z0-9._-]", "_");
if (sanitized.isBlank()) {
throw new IllegalArgumentException("original filename is invalid");
throw new IllegalArgumentException("原始文件名无效");
}
return sanitized;
} catch (InvalidPathException ex) {
throw new IllegalArgumentException("original filename is invalid");
throw new IllegalArgumentException("原始文件名无效");
}
}
@@ -46,7 +46,7 @@ public class InitCommandProfileService {
private static Map<String, Map<String, Map<String, String>>> load(ObjectMapper objectMapper) {
try (InputStream input = InitCommandProfileService.class.getClassLoader().getResourceAsStream(PROFILE_RESOURCE)) {
if (input == null) {
throw new IllegalStateException("missing command profile resource: " + PROFILE_RESOURCE);
throw new IllegalStateException("命令配置资源不存在:" + PROFILE_RESOURCE);
}
// JSON 结构:{productType:{mqType:{stepCode:command}}}
TypeReference<Map<String, Map<String, Map<String, String>>>> typeRef =
@@ -55,7 +55,7 @@ public class InitCommandProfileService {
Map<String, Map<String, Map<String, String>>> map = objectMapper.readValue(input, typeRef);
return map == null ? Collections.emptyMap() : map;
} catch (IOException ex) {
throw new IllegalStateException("failed to load command profile resource: " + PROFILE_RESOURCE, ex);
throw new IllegalStateException("加载命令配置资源失败:" + PROFILE_RESOURCE, ex);
}
}
@@ -14,37 +14,37 @@ public class InitPreviewRequest {
"^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$";
private static final String FILE_ID_REGEX = "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$";
@NotBlank(message = "orgCodeType is required")
@Pattern(regexp = "BIC|CIPSID|LEI", message = "orgCodeType must be BIC/CIPSID/LEI")
@NotBlank(message = "orgCodeType不能为空")
@Pattern(regexp = "BIC|CIPSID|LEI", message = "orgCodeType必须为BIC/CIPSID/LEI")
@Schema(description = "机构代码类型", example = "BIC")
private String orgCodeType;
@NotBlank(message = "orgCode is required")
@Size(max = 32, message = "orgCode length must be <= 32")
@NotBlank(message = "orgCode不能为空")
@Size(max = 32, message = "orgCode长度必须小于等于32")
@Schema(description = "机构代码", example = "BKCHCNBJXXX")
private String orgCode;
@NotBlank(message = "orgNameCn is required")
@Size(max = 128, message = "orgNameCn length must be <= 128")
@NotBlank(message = "orgNameCn不能为空")
@Size(max = 128, message = "orgNameCn长度必须小于等于128")
@Schema(description = "机构中文名称", example = "中国银行")
private String orgNameCn;
@Size(max = 256, message = "orgNameEn length must be <= 256")
@Size(max = 256, message = "orgNameEn长度必须小于等于256")
@Schema(description = "机构英文名称", example = "Bank of China")
private String orgNameEn;
@NotBlank(message = "deployMode is required")
@Pattern(regexp = "SINGLE|DUAL|QUAD", message = "deployMode must be SINGLE/DUAL/QUAD")
@NotBlank(message = "deployMode不能为空")
@Pattern(regexp = "SINGLE|DUAL|QUAD", message = "deployMode必须为SINGLE/DUAL/QUAD")
@Schema(description = "部署模式", example = "DUAL")
private String deployMode;
@Valid
@NotNull(message = "nodes is required")
@NotNull(message = "nodes不能为空")
@Schema(description = "节点 IP 配置")
private Nodes nodes;
@Valid
@NotNull(message = "mq is required")
@NotNull(message = "mq不能为空")
@Schema(description = "消息中间件配置")
private Mq mq;
@@ -119,20 +119,20 @@ public class InitPreviewRequest {
@Schema(description = "节点 IP 配置")
public static class Nodes {
@NotBlank(message = "nodes.node01Ip is required")
@Pattern(regexp = IPV4_REGEX, message = "nodes.node01Ip must be a valid IPv4")
@NotBlank(message = "nodes.node01Ip不能为空")
@Pattern(regexp = IPV4_REGEX, message = "nodes.node01Ip必须是有效IPv4地址")
@Schema(description = "节点 1 IP", example = "192.168.1.11")
private String node01Ip;
@Pattern(regexp = IPV4_REGEX, message = "nodes.node02Ip must be a valid IPv4")
@Pattern(regexp = IPV4_REGEX, message = "nodes.node02Ip必须是有效IPv4地址")
@Schema(description = "节点 2 IP", example = "192.168.1.12")
private String node02Ip;
@Pattern(regexp = IPV4_REGEX, message = "nodes.node03Ip must be a valid IPv4")
@Pattern(regexp = IPV4_REGEX, message = "nodes.node03Ip必须是有效IPv4地址")
@Schema(description = "节点 3 IP", example = "192.168.1.13")
private String node03Ip;
@Pattern(regexp = IPV4_REGEX, message = "nodes.node04Ip must be a valid IPv4")
@Pattern(regexp = IPV4_REGEX, message = "nodes.node04Ip必须是有效IPv4地址")
@Schema(description = "节点 4 IP", example = "192.168.1.14")
private String node04Ip;
@@ -172,37 +172,37 @@ public class InitPreviewRequest {
@Schema(description = "消息中间件配置")
public static class Mq {
@NotBlank(message = "mq.mqType is required")
@NotBlank(message = "mq.mqType不能为空")
@Schema(description = "消息中间件类型", example = "RABBITMQ")
private String mqType;
@NotBlank(message = "mq.channelUsername is required")
@Size(max = 32, message = "mq.channelUsername length must be <= 32")
@NotBlank(message = "mq.channelUsername不能为空")
@Size(max = 32, message = "mq.channelUsername长度必须小于等于32")
@Schema(description = "RABBITMQ通道用户名", example = "tms")
private String channelUsername;
@NotBlank(message = "mq.channelPassword is required")
@Size(max = 128, message = "mq.channelPassword length must be <= 128")
@NotBlank(message = "mq.channelPassword不能为空")
@Size(max = 128, message = "mq.channelPassword长度必须小于等于128")
@Schema(description = "RABBITMQ通道密码")
private String channelPassword;
@Size(max = 128, message = "mq.tlqLicenseFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.tlqLicenseFileId must be a fileId")
@Size(max = 128, message = "mq.tlqLicenseFileId长度必须小于等于128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.tlqLicenseFileId必须是有效fileId")
@Schema(description = "TLQ 许可证文件 fileId。作用:用于 TLQ 相关模式的许可证下发。使用条件:`TLQ` 或 `RABBITMQ_TLQ` 场景必填", example = "FILE-20260310-000001")
private String tlqLicenseFileId;
@Size(max = 64, message = "mq.cipsGatewayHost length must be <= 64")
@Size(max = 64, message = "mq.cipsGatewayHost长度必须小于等于64")
@Schema(description = "CIPS 中心网关地址。作用:回写到 TLQ 网关脚本中的中心网关主机地址。使用条件:仅 `DIRECT + RABBITMQ_TLQ` 必填", example = "192.168.10.20")
private String cipsGatewayHost;
@Size(max = 128, message = "mq.cfgZipFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.cfgZipFileId must be a fileId")
@Size(max = 128, message = "mq.cfgZipFileId长度必须小于等于128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.cfgZipFileId必须是有效fileId")
@Schema(description = "直参 PMTS-CAE 客户化配置包 fileId。使用条件:仅 `DIRECT` 使用;`RABBITMQ_TLQ`必填", example = "FILE-20260310-000201")
private String cfgZipFileId;
@Size(max = 128, message = "mq.cfmqConfigFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.cfmqConfigFileId must be a fileId")
@Size(max = 128, message = "mq.cfmqConfigFileId长度必须小于等于128")
@Pattern(regexp = FILE_ID_REGEX, message = "mq.cfmqConfigFileId必须是有效fileId")
@Schema(description = "直参 CFMQ 客户化脚本/配置 fileId。使用条件:仅 `DIRECT + RABBITMQ_CFMQ` 必填", example = "FILE-20260310-000002")
private String cfmqConfigFileId;
@@ -267,14 +267,14 @@ public class InitPreviewRequest {
@Schema(description = "许可证和配置包引用")
public static class Licenses {
@NotBlank(message = "licenses.receiverLicenseFileId is required")
@Size(max = 128, message = "licenses.receiverLicenseFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "licenses.receiverLicenseFileId must be a fileId")
@NotBlank(message = "licenses.receiverLicenseFileId不能为空")
@Size(max = 128, message = "licenses.receiverLicenseFileId长度必须小于等于128")
@Pattern(regexp = FILE_ID_REGEX, message = "licenses.receiverLicenseFileId必须是有效fileId")
@Schema(description = "接收端许可证文件 fileId", example = "FILE-20260310-000003")
private String receiverLicenseFileId;
@Size(max = 128, message = "licenses.cfgZipFileId length must be <= 128")
@Pattern(regexp = FILE_ID_REGEX, message = "licenses.cfgZipFileId must be a fileId")
@Size(max = 128, message = "licenses.cfgZipFileId长度必须小于等于128")
@Pattern(regexp = FILE_ID_REGEX, message = "licenses.cfgZipFileId必须是有效fileId")
@Schema(description = "标准版配置包 zip 文件 fileId。作用:用于标准版/间参版初始化时引用配置包。使用条件:`ENTERPRISE`、`INDIRECT` 场景按对应流程使用,`DIRECT` 不使用此字段", example = "FILE-20260310-000004")
private String cfgZipFileId;
@@ -67,7 +67,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
public InitStepExecutionResult execute(InitTaskEntity task, InitTaskStepEntity step) {
String command = step.getCommandLine();
if (isBlank(command)) {
return InitStepExecutionResult.failure("empty command", -1, null);
return InitStepExecutionResult.failure("命令不能为空", -1, null);
}
if (command.startsWith(INTERNAL_PREFIX)) {
@@ -76,19 +76,19 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
// SIMULATE 模式不触发外部脚本,便于联调和单测
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
return InitStepExecutionResult.success("simulated success", 0, null);
return InitStepExecutionResult.success("模拟执行成功", 0, null);
}
// LOCAL 模式下只允许执行白名单前缀命令,避免注入或误执行
if (!isWhitelisted(command, properties.getWhitelistPrefixes())) {
return InitStepExecutionResult.failure("command rejected by whitelist", -1, null);
return InitStepExecutionResult.failure("命令未通过白名单校验", -1, null);
}
Path logFile;
try {
logFile = prepareLogPath(task, step);
} catch (IOException ex) {
return InitStepExecutionResult.failure("prepare log path failed: " + ex.getMessage(), -1, null);
return InitStepExecutionResult.failure("准备日志路径失败:" + ex.getMessage(), -1, null);
}
Process process = null;
@@ -102,19 +102,19 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
boolean finished = process.waitFor(properties.getTimeoutSec(), TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
return InitStepExecutionResult.failure("command timeout", -1, logFile.toString());
return InitStepExecutionResult.failure("命令执行超时", -1, logFile.toString());
}
int exitCode = process.exitValue();
if (exitCode == 0) {
return InitStepExecutionResult.success("command success", exitCode, logFile.toString());
return InitStepExecutionResult.success("命令执行成功", exitCode, logFile.toString());
}
return InitStepExecutionResult.failure("command failed", exitCode, logFile.toString());
return InitStepExecutionResult.failure("命令执行失败", exitCode, logFile.toString());
} catch (IOException | InterruptedException ex) {
if (ex instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return InitStepExecutionResult.failure("executor exception: " + ex.getMessage(), -1, logFile.toString());
return InitStepExecutionResult.failure("执行器异常:" + ex.getMessage(), -1, logFile.toString());
} finally {
if (process != null && process.isAlive()) {
process.destroyForcibly();
@@ -157,13 +157,13 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
case "cfmq_route_apply" -> executeCfmqRouteApply(task, step);
case "verify" -> executeVerify(task, step);
default -> {
String logPath = writeInternalLog(task, step, "skip unsupported internal step: " + internalCode);
yield InitStepExecutionResult.success("skip unsupported internal step: " + internalCode, 0, logPath);
String logPath = writeInternalLog(task, step, "跳过不支持的内部步骤:" + internalCode);
yield InitStepExecutionResult.success("跳过不支持的内部步骤:" + internalCode, 0, logPath);
}
};
} catch (Exception ex) {
String logPath = writeInternalLog(task, step, "internal step exception: " + ex.getMessage());
return InitStepExecutionResult.failure("internal step failed: " + ex.getMessage(), -1, logPath);
return InitStepExecutionResult.failure("内部步骤执行失败:" + ex.getMessage(), -1, logPath);
}
}
@@ -172,7 +172,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
ResolvedCpconfig resolvedCpconfig = resolveCpconfigPath(task, context);
Path cpconfigPath = resolvedCpconfig.path();
if (!Files.exists(cpconfigPath) || !Files.isRegularFile(cpconfigPath)) {
String detail = "cpconfig not found: " + cpconfigPath + ", candidates=" + formatPathList(resolvedCpconfig.candidates());
String detail = "cpconfig不存在:" + cpconfigPath + ",候选路径=" + formatPathList(resolvedCpconfig.candidates());
String logPath = writeInternalLog(task, step, detail);
return InitStepExecutionResult.failure(detail, -1, logPath);
}
@@ -235,7 +235,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
appendLog(logFile, "tlq license copied: " + tlqSource + " -> " + tlqTarget);
}
}
return InitStepExecutionResult.success("file prepare success", 0, logFile.toString());
return InitStepExecutionResult.success("文件准备成功", 0, logFile.toString());
}
private InitStepExecutionResult executeMqUserApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
@@ -245,21 +245,21 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
return executeDirectMqUserApply(task, step, context);
}
if (!"ENTERPRISE".equals(context.productType()) && !"INDIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for non-standard product type");
return InitStepExecutionResult.success("mq_user_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 已跳过 for non-standard product type");
return InitStepExecutionResult.success("mq_user_apply 已跳过", 0, logPath);
}
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "mq_user_apply skipped for current standard mode");
return InitStepExecutionResult.success("mq_user_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 已跳过 for current standard mode");
return InitStepExecutionResult.success("mq_user_apply 已跳过", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "mq_user_apply simulated");
return InitStepExecutionResult.success("mq_user_apply simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 模拟执行成功");
return InitStepExecutionResult.success("mq_user_apply 模拟执行成功", 0, logPath);
}
if (isBlank(context.channelUsername()) || isBlank(context.channelPassword())) {
String logPath = writeInternalLog(task, step, "mq_user_apply failed: channel account is blank");
return InitStepExecutionResult.failure("mq_user_apply failed: channel account is blank", -1, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 执行失败:通道账号为空");
return InitStepExecutionResult.failure("mq_user_apply 执行失败:通道账号为空", -1, logPath);
}
try {
@@ -274,29 +274,29 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
scriptContext.stagingScriptDir()
);
if (code != 0) {
return InitStepExecutionResult.failure("mq_user_apply failed: setuprabq.sh failed", code, logFile.toString());
return InitStepExecutionResult.failure("mq_user_apply 执行失败:setuprabq.sh执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("mq user applied by setuprabq.sh", 0, logFile.toString());
return InitStepExecutionResult.success("MQ用户已通过setuprabq.sh应用", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
String logPath = writeInternalLog(task, step, "mq_user_apply interrupted");
return InitStepExecutionResult.failure("mq_user_apply interrupted", -1, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 被中断");
return InitStepExecutionResult.failure("mq_user_apply 被中断", -1, logPath);
}
}
private InitStepExecutionResult executeMqQueueApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"ENTERPRISE".equals(context.productType()) && !"INDIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for non-standard product type");
return InitStepExecutionResult.success("mq_queue_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_queue_apply 已跳过 for non-standard product type");
return InitStepExecutionResult.success("mq_queue_apply 已跳过", 0, logPath);
}
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "mq_queue_apply skipped for current standard mode");
return InitStepExecutionResult.success("mq_queue_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_queue_apply 已跳过 for current standard mode");
return InitStepExecutionResult.success("mq_queue_apply 已跳过", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "mq_queue_apply simulated");
return InitStepExecutionResult.success("mq_queue_apply simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_queue_apply 模拟执行成功");
return InitStepExecutionResult.success("mq_queue_apply 模拟执行成功", 0, logPath);
}
try {
@@ -313,13 +313,13 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
scriptContext.stagingScriptDir()
);
if (code != 0) {
return InitStepExecutionResult.failure("mq_queue_apply failed: " + selectedScriptName + " failed", code, logFile.toString());
return InitStepExecutionResult.failure("mq_queue_apply 执行失败:" + selectedScriptName + " 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("mq queues applied by " + selectedScriptName, 0, logFile.toString());
return InitStepExecutionResult.success("MQ队列已通过" + selectedScriptName, 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
String logPath = writeInternalLog(task, step, "mq_queue_apply interrupted");
return InitStepExecutionResult.failure("mq_queue_apply interrupted", -1, logPath);
String logPath = writeInternalLog(task, step, "mq_queue_apply 被中断");
return InitStepExecutionResult.failure("mq_queue_apply 被中断", -1, logPath);
}
}
@@ -329,12 +329,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
PlanContext context
) throws IOException {
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "mq_user_apply simulated");
return InitStepExecutionResult.success("mq_user_apply simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 模拟执行成功");
return InitStepExecutionResult.success("mq_user_apply 模拟执行成功", 0, logPath);
}
if (isBlank(context.channelUsername()) || isBlank(context.channelPassword())) {
String logPath = writeInternalLog(task, step, "mq_user_apply failed: channel account is blank");
return InitStepExecutionResult.failure("mq_user_apply failed: channel account is blank", -1, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 执行失败:通道账号为空");
return InitStepExecutionResult.failure("mq_user_apply 执行失败:通道账号为空", -1, logPath);
}
try {
@@ -345,7 +345,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String pass = context.channelPassword();
if (!waitRabbitmqReady(logFile, ctl)) {
appendLog(logFile, "rabbitmqctl status check failed, continue with apply flow");
appendLog(logFile, "rabbitmqctl状态检查失败,继续执行应用流程");
}
runCommandAllowAlreadyExists(logFile, Arrays.asList(ctl, "add_vhost", vhost), "vhost");
@@ -365,21 +365,21 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
if (permissionCode != 0) {
String lastLine = readLastLogLine(logFile);
return InitStepExecutionResult.failure(
"mq_user_apply failed: set permissions failed"
"mq_user_apply 执行失败:set permissions执行失败"
+ ", exit=" + permissionCode
+ (isBlank(lastLine) ? "" : ", detail=" + lastLine),
permissionCode,
logFile.toString()
);
}
return InitStepExecutionResult.success("mq user applied", 0, logFile.toString());
return InitStepExecutionResult.success("MQ用户已应用", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
String logPath = writeInternalLog(task, step, "mq_user_apply interrupted");
return InitStepExecutionResult.failure("mq_user_apply interrupted", -1, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 被中断");
return InitStepExecutionResult.failure("mq_user_apply 被中断", -1, logPath);
} catch (IllegalArgumentException ex) {
String logPath = writeInternalLog(task, step, "mq_user_apply failed: " + ex.getMessage());
return InitStepExecutionResult.failure("mq_user_apply failed: " + ex.getMessage(), -1, logPath);
String logPath = writeInternalLog(task, step, "mq_user_apply 执行失败:" + ex.getMessage());
return InitStepExecutionResult.failure("mq_user_apply 执行失败:" + ex.getMessage(), -1, logPath);
}
}
@@ -613,7 +613,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String rabbitAdminCmd = firstNonBlank(properties.getRabbitmqAdminCommand(), "/usr/sbin/rabbitmqadmin");
String adminUser = firstNonBlank(properties.getRabbitAdminUsername(), "admin");
String adminPass = firstNonBlank(properties.getRabbitAdminPassword(), "admin");
appendLog(logFile, "rabbitmqctl add/change failed, fallback to rabbitmqadmin");
appendLog(logFile, "rabbitmqctl新增或修改失败,切换到rabbitmqadmin兜底");
runCommand(
logFile,
@@ -641,7 +641,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
if (userCode != 0) {
String lastLine = readLastLogLine(logFile);
return InitStepExecutionResult.failure(
"mq_user_apply failed: add/change user failed"
"mq_user_apply 执行失败:add/change user 执行失败"
+ ", addExit=" + addUserCode
+ ", changeExit=" + changeCode
+ ", fallbackExit=" + userCode
@@ -668,7 +668,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
if (permissionCode != 0) {
String lastLine = readLastLogLine(logFile);
return InitStepExecutionResult.failure(
"mq_user_apply failed: fallback set permissions failed"
"mq_user_apply 执行失败:fallback set permissions执行失败"
+ ", exit=" + permissionCode
+ (isBlank(lastLine) ? "" : ", detail=" + lastLine),
permissionCode,
@@ -676,7 +676,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
);
}
return InitStepExecutionResult.success(
"mq user applied by rabbitmqadmin fallback (rabbitmqctl addExit=" + addUserCode + ", changeExit=" + changeCode + ")",
"MQ用户已应用 通过rabbitmqadmin兜底应用 (rabbitmqctl addExit=" + addUserCode + ", changeExit=" + changeCode + ")",
0,
logFile.toString()
);
@@ -731,7 +731,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
}
throw new IllegalArgumentException(
"rabbitmqctl not found, please set tms.init.executor.rabbitmq-ctl-command to absolute path"
"rabbitmqctl不存在,请将tms.init.executor.rabbitmq-ctl-command配置为绝对路径"
);
}
@@ -761,27 +761,27 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private InitStepExecutionResult executeAppMqCredentialsApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"ENTERPRISE".equals(context.productType()) && !"INDIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply skipped for non-standard product type");
return InitStepExecutionResult.success("app_mq_credentials_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply 已跳过 for non-standard product type");
return InitStepExecutionResult.success("app_mq_credentials_apply 已跳过", 0, logPath);
}
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply skipped for current standard mode");
return InitStepExecutionResult.success("app_mq_credentials_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply 已跳过 for current standard mode");
return InitStepExecutionResult.success("app_mq_credentials_apply 已跳过", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply simulated");
return InitStepExecutionResult.success("app_mq_credentials_apply simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply 模拟执行成功");
return InitStepExecutionResult.success("app_mq_credentials_apply 模拟执行成功", 0, logPath);
}
if (isBlank(context.channelUsername()) || isBlank(context.channelPassword())) {
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply failed: channel account is blank");
return InitStepExecutionResult.failure("app_mq_credentials_apply failed: channel account is blank", -1, logPath);
String logPath = writeInternalLog(task, step, "app_mq_credentials_apply 执行失败:通道账号为空");
return InitStepExecutionResult.failure("app_mq_credentials_apply 执行失败:通道账号为空", -1, logPath);
}
Path logFile = prepareLogPath(task, step);
List<Path> configFiles = resolveGlobFiles(properties.getStandardRabbitAppConfigPatterns());
if (configFiles.isEmpty()) {
appendLog(logFile, "config patterns=" + properties.getStandardRabbitAppConfigPatterns());
return InitStepExecutionResult.failure("app_mq_credentials_apply failed: no app config file matched", -1, logFile.toString());
return InitStepExecutionResult.failure("app_mq_credentials_apply 执行失败:未匹配到应用配置文件", -1, logFile.toString());
}
String configuredUsername = context.channelUsername();
@@ -791,8 +791,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
configuredUsername = encryptWith3DesHex(context.channelUsername(), properties.getStandardRabbitCredentialSecretKey());
configuredPassword = encryptWith3DesHex(context.channelPassword(), properties.getStandardRabbitCredentialSecretKey());
} catch (Exception ex) {
appendLog(logFile, "encrypt rabbitmq credential failed: " + ex.getMessage());
return InitStepExecutionResult.failure("app_mq_credentials_apply failed: encrypt credential failed", -1, logFile.toString());
appendLog(logFile, "encrypt rabbitmq credential 执行失败:" + ex.getMessage());
return InitStepExecutionResult.failure("app_mq_credentials_apply 执行失败:加密凭据失败", -1, logFile.toString());
}
}
@@ -802,24 +802,24 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
changed += replaceProperty(configFile, "spring.rabbitmq.password", configuredPassword);
appendLog(logFile, "updated app rabbitmq credentials: " + configFile);
}
return InitStepExecutionResult.success("app mq credentials applied, changed=" + changed, 0, logFile.toString());
return InitStepExecutionResult.success("应用MQ凭据已应用,变更数量=" + changed, 0, logFile.toString());
}
private InitStepExecutionResult executeDirectDbInit(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "direct_db_init skipped for non-direct product type");
return InitStepExecutionResult.success("direct_db_init skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "direct_db_init 已跳过 for non-direct product type");
return InitStepExecutionResult.success("direct_db_init 已跳过", 0, logPath);
}
String logPath = writeInternalLog(task, step, "direct_db_init skipped: out of direct customer-config scope");
return InitStepExecutionResult.success("direct_db_init skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "direct_db_init 已跳过: out of direct customer-config scope");
return InitStepExecutionResult.success("direct_db_init 已跳过", 0, logPath);
}
private InitStepExecutionResult executeStandardDbRender(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "db_render skipped for direct product type");
return InitStepExecutionResult.success("db_render skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "db_render 已跳过 for direct product type");
return InitStepExecutionResult.success("db_render 已跳过", 0, logPath);
}
Path scriptBaseDir = Paths.get(firstNonBlank(
@@ -837,8 +837,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
List<Path> requiredFiles = List.of(cmtpTemplate, cmepDmlTemplate, cmepEntryTemplate, schemaTemplate, upOrgTemplate);
for (Path requiredFile : requiredFiles) {
if (!Files.exists(requiredFile) || !Files.isRegularFile(requiredFile)) {
String logPath = writeInternalLog(task, step, "db_render failed: required sql template not found: " + requiredFile);
return InitStepExecutionResult.failure("db_render failed: sql template not found", -1, logPath);
String logPath = writeInternalLog(task, step, "db_render 执行失败:必需SQL模板不存在: " + requiredFile);
return InitStepExecutionResult.failure("db_render 执行失败:SQL模板不存在", -1, logPath);
}
}
@@ -858,19 +858,19 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private InitStepExecutionResult executeStandardDbApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "db_apply skipped for direct product type");
return InitStepExecutionResult.success("db_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "db_apply 已跳过 for direct product type");
return InitStepExecutionResult.success("db_apply 已跳过", 0, logPath);
}
Path stageSqlDir = resolveStandardDbStageSqlDir(task);
if (!Files.isDirectory(stageSqlDir)) {
String logPath = writeInternalLog(task, step, "db_apply failed: staged sql dir not found: " + stageSqlDir);
return InitStepExecutionResult.failure("db_apply failed: staged sql dir not found", -1, logPath);
String logPath = writeInternalLog(task, step, "db_apply 执行失败:暂存SQL目录不存在: " + stageSqlDir);
return InitStepExecutionResult.failure("db_apply 执行失败:暂存SQL目录不存在", -1, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "db_apply simulated, staged dir=" + stageSqlDir);
return InitStepExecutionResult.success("db_apply simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "db_apply 模拟执行成功, staged dir=" + stageSqlDir);
return InitStepExecutionResult.success("db_apply 模拟执行成功", 0, logPath);
}
Path loadDir = Paths.get(firstNonBlank(
@@ -890,8 +890,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
appendLog(logFile, "staged sql copied: " + stageSqlDir + " -> " + loadDir);
String applyCommand = normalize(properties.getStandardDbApplyCommand());
if (isBlank(applyCommand)) {
appendLog(logFile, "db_apply failed: standardDbApplyCommand is blank");
return InitStepExecutionResult.failure("db_apply failed: apply command is blank", -1, logFile.toString());
appendLog(logFile, "db_apply 执行失败:standardDbApplyCommand is blank");
return InitStepExecutionResult.failure("db_apply 执行失败:应用命令为空", -1, logFile.toString());
}
try {
@@ -903,20 +903,20 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
buildStandardDbApplyEnvironment(loadDir, context)
);
if (code != 0) {
return InitStepExecutionResult.failure("db_apply failed", code, logFile.toString());
return InitStepExecutionResult.failure("db_apply 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("db sql applied to load dir=" + loadDir + ", command executed", 0, logFile.toString());
return InitStepExecutionResult.success("数据库SQL已应用到加载目录=" + loadDir + ",命令已执行", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("db_apply interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure("db_apply 被中断", -1, logFile.toString());
}
}
private InitStepExecutionResult executeStandardNginxConfigApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "nginx_config_apply skipped for direct product type");
return InitStepExecutionResult.success("nginx_config_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "nginx_config_apply 已跳过 for direct product type");
return InitStepExecutionResult.success("nginx_config_apply 已跳过", 0, logPath);
}
Path templatePath = Paths.get(firstNonBlank(
@@ -924,8 +924,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
properties.getStandardHomeDir() + "/cpackage/software/nginx.conf"
)).normalize().toAbsolutePath();
if (!Files.exists(templatePath) || !Files.isRegularFile(templatePath)) {
String logPath = writeInternalLog(task, step, "nginx_config_apply failed: template not found: " + templatePath);
return InitStepExecutionResult.failure("nginx_config_apply failed: template not found", -1, logPath);
String logPath = writeInternalLog(task, step, "nginx_config_apply 执行失败:模板不存在: " + templatePath);
return InitStepExecutionResult.failure("nginx_config_apply 执行失败:模板不存在", -1, logPath);
}
Path stageFile = resolveStandardNginxStageFile(task);
@@ -937,25 +937,25 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
Files.writeString(stageFile, rendered, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
Path logFile = prepareLogPath(task, step);
appendLog(logFile, "nginx config staged: " + templatePath + " -> " + stageFile);
appendLog(logFile, "nginx配置已暂存: " + templatePath + " -> " + stageFile);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
return InitStepExecutionResult.success("nginx config staged", 0, logFile.toString());
return InitStepExecutionResult.success("nginx配置已暂存", 0, logFile.toString());
}
Path targetPath = Paths.get(firstNonBlank(properties.getStandardNginxTargetPath(), "/usr/local/nginx/conf/nginx.conf"))
.normalize()
.toAbsolutePath();
copyToTarget(stageFile, targetPath);
appendLog(logFile, "nginx config applied: " + stageFile + " -> " + targetPath);
return InitStepExecutionResult.success("nginx config applied", 0, logFile.toString());
appendLog(logFile, "nginx配置已应用: " + stageFile + " -> " + targetPath);
return InitStepExecutionResult.success("nginx配置已应用", 0, logFile.toString());
}
private InitStepExecutionResult executeStandardWebConfigApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "web_config_apply skipped for direct product type");
return InitStepExecutionResult.success("web_config_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "web_config_apply 已跳过 for direct product type");
return InitStepExecutionResult.success("web_config_apply 已跳过", 0, logPath);
}
Path frontZip = Paths.get(firstNonBlank(
@@ -963,8 +963,8 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
properties.getStandardHomeDir() + "/cpackage/front/front.zip"
)).normalize().toAbsolutePath();
if (!Files.exists(frontZip) || !Files.isRegularFile(frontZip)) {
String logPath = writeInternalLog(task, step, "web_config_apply failed: front zip not found: " + frontZip);
return InitStepExecutionResult.failure("web_config_apply failed: front zip not found", -1, logPath);
String logPath = writeInternalLog(task, step, "web_config_apply 执行失败:前端zip文件不存在: " + frontZip);
return InitStepExecutionResult.failure("web_config_apply 执行失败:前端zip文件不存在", -1, logPath);
}
Path stagedWebDir = resolveStandardWebStageDir(task);
@@ -981,10 +981,10 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
);
Path logFile = prepareLogPath(task, step);
appendLog(logFile, "web assets staged: " + frontZip + " -> " + stagedWebDir);
appendLog(logFile, "前端资源已暂存: " + frontZip + " -> " + stagedWebDir);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
return InitStepExecutionResult.success("web assets staged", 0, logFile.toString());
return InitStepExecutionResult.success("前端资源已暂存", 0, logFile.toString());
}
Path targetDir = Paths.get(firstNonBlank(properties.getStandardWebTargetDir(), "/usr/local/nginx/html"))
@@ -998,9 +998,9 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
"/usr/local/nginx/html/organization.json"
)).normalize().toAbsolutePath();
copyToTarget(stagedOrganizationJson, organizationJsonTarget);
appendLog(logFile, "web assets applied: " + stagedWebDir + " -> " + targetDir);
appendLog(logFile, "organization.json applied: " + stagedOrganizationJson + " -> " + organizationJsonTarget);
return InitStepExecutionResult.success("web assets applied", 0, logFile.toString());
appendLog(logFile, "前端资源已应用: " + stagedWebDir + " -> " + targetDir);
appendLog(logFile, "organization.json 已应用: " + stagedOrganizationJson + " -> " + organizationJsonTarget);
return InitStepExecutionResult.success("前端资源已应用", 0, logFile.toString());
}
private InitStepExecutionResult executeStandardStartCommand(
@@ -1011,22 +1011,22 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, stepName + " skipped for direct product type");
return InitStepExecutionResult.success(stepName + " skipped", 0, logPath);
String logPath = writeInternalLog(task, step, stepName + " 已跳过 for direct product type");
return InitStepExecutionResult.success(stepName + " 已跳过", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, stepName + " simulated");
return InitStepExecutionResult.success(stepName + " simulated", 0, logPath);
String logPath = writeInternalLog(task, step, stepName + " 模拟执行成功");
return InitStepExecutionResult.success(stepName + " 模拟执行成功", 0, logPath);
}
if (isBlank(command)) {
String logPath = writeInternalLog(task, step, stepName + " failed: command is blank");
return InitStepExecutionResult.failure(stepName + " failed: command is blank", -1, logPath);
String logPath = writeInternalLog(task, step, stepName + " 执行失败:command is blank");
return InitStepExecutionResult.failure(stepName + " 执行失败:command is blank", -1, logPath);
}
Path logFile = prepareLogPath(task, step);
Path workingDirectory = resolveExistingStandardWorkingDirectory();
if (workingDirectory == null) {
appendLog(logFile, "cwd skipped: standardHomeDir not found");
appendLog(logFile, "cwd 已跳过: standardHomeDir不存在");
}
try {
int code = runCommand(
@@ -1037,12 +1037,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
buildStandardStartEnvironment(stepName)
);
if (code != 0) {
return InitStepExecutionResult.failure(stepName + " failed", code, logFile.toString());
return InitStepExecutionResult.failure(stepName + " 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success(stepName + " success", 0, logFile.toString());
return InitStepExecutionResult.success(stepName + " 成功", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure(stepName + " interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure(stepName + " 被中断", -1, logFile.toString());
}
}
@@ -1053,14 +1053,14 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
) throws IOException {
PlanContext context = loadPlanContext(task);
if ("DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "reset db drop skipped for direct product type");
return InitStepExecutionResult.success("reset db drop skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "reset db drop 已跳过 for direct product type");
return InitStepExecutionResult.success("reset db drop 已跳过", 0, logPath);
}
String databaseName = isBlank(fixedDatabaseName) ? normalize(context.orgCode()) : fixedDatabaseName;
String identifier = requireMysqlIdentifier(databaseName);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "reset db drop simulated, database=" + identifier);
return InitStepExecutionResult.success("reset db drop simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "reset db drop 模拟执行成功, database=" + identifier);
return InitStepExecutionResult.success("reset db drop 模拟执行成功", 0, logPath);
}
Path logFile = prepareLogPath(task, step);
@@ -1076,28 +1076,28 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
buildStandardDbApplyEnvironment(null, context)
);
if (code != 0) {
return InitStepExecutionResult.failure("reset db drop failed", code, logFile.toString());
return InitStepExecutionResult.failure("reset db drop 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("reset db dropped: " + identifier, 0, logFile.toString());
return InitStepExecutionResult.success("重置数据库已删除:" + identifier, 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("reset db drop interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure("reset db drop 被中断", -1, logFile.toString());
}
}
private InitStepExecutionResult executeResetMqCleanup(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "mq_cleanup skipped for current mode");
return InitStepExecutionResult.success("mq_cleanup skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_cleanup 已跳过 for current mode");
return InitStepExecutionResult.success("mq_cleanup 已跳过", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "mq_cleanup simulated");
return InitStepExecutionResult.success("mq_cleanup simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "mq_cleanup 模拟执行成功");
return InitStepExecutionResult.success("mq_cleanup 模拟执行成功", 0, logPath);
}
if (isBlank(context.channelUsername())) {
String logPath = writeInternalLog(task, step, "mq_cleanup failed: channel username is blank");
return InitStepExecutionResult.failure("mq_cleanup failed: channel username is blank", -1, logPath);
String logPath = writeInternalLog(task, step, "mq_cleanup 执行失败:通道用户名为空");
return InitStepExecutionResult.failure("mq_cleanup 执行失败:通道用户名为空", -1, logPath);
}
Path logFile = prepareLogPath(task, step);
@@ -1106,7 +1106,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String user = normalize(context.channelUsername());
try {
if (!waitRabbitmqReady(logFile, ctl)) {
appendLog(logFile, "rabbitmqctl status check failed, continue with cleanup flow");
appendLog(logFile, "rabbitmqctl status check 执行失败, continue with cleanup flow");
}
for (String queue : buildStandardCustomerDefaultVhostQueues(normalize(context.orgCode()))) {
@@ -1117,18 +1117,18 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
runCommand(logFile, Arrays.asList(ctl, "clear_permissions", "-p", vhost, user), true);
runCommand(logFile, Arrays.asList(ctl, "delete_user", user), true);
return InitStepExecutionResult.success("mq cleanup applied", 0, logFile.toString());
return InitStepExecutionResult.success("mq cleanup 已应用", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("mq cleanup interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure("mq cleanup 被中断", -1, logFile.toString());
}
}
private InitStepExecutionResult executeResetStopRabbitmq(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!shouldApplyStandardRabbitmq(context)) {
String logPath = writeInternalLog(task, step, "stop_rabbitmq skipped for current mode");
return InitStepExecutionResult.success("stop_rabbitmq skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "stop_rabbitmq 已跳过 for current mode");
return InitStepExecutionResult.success("stop_rabbitmq 已跳过", 0, logPath);
}
return executeStandardStartCommand(task, step, "stop_rabbitmq", properties.getStandardRabbitStopCommand());
}
@@ -1136,37 +1136,37 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private InitStepExecutionResult executeResetTlqLicenseCleanup(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"TLQ".equals(context.mqType()) && !"RABBITMQ_TLQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "tlq_license_cleanup skipped for non-TLQ mode");
return InitStepExecutionResult.success("tlq_license_cleanup skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "tlq_license_cleanup 已跳过 for non-TLQ mode");
return InitStepExecutionResult.success("tlq_license_cleanup 已跳过", 0, logPath);
}
Path target = Paths.get(resolveTlqLicenseTargetPath(context.productType())).normalize().toAbsolutePath();
Path logFile = prepareLogPath(task, step);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
appendLog(logFile, "tlq license cleanup simulated: " + target);
return InitStepExecutionResult.success("tlq license cleanup simulated", 0, logFile.toString());
appendLog(logFile, "tlq license cleanup 模拟执行成功: " + target);
return InitStepExecutionResult.success("tlq license cleanup 模拟执行成功", 0, logFile.toString());
}
Files.deleteIfExists(target);
appendLog(logFile, "tlq license deleted if exists: " + target);
return InitStepExecutionResult.success("tlq license cleanup applied", 0, logFile.toString());
return InitStepExecutionResult.success("tlq license cleanup 已应用", 0, logFile.toString());
}
private InitStepExecutionResult executeResetCleanStaging(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
Path stageDir = Paths.get(properties.getStagingRootDir()).resolve(task.getTaskId()).normalize().toAbsolutePath();
Path logFile = prepareLogPath(task, step);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
appendLog(logFile, "clean_staging simulated: " + stageDir);
return InitStepExecutionResult.success("clean_staging simulated", 0, logFile.toString());
appendLog(logFile, "clean_staging 模拟执行成功: " + stageDir);
return InitStepExecutionResult.success("clean_staging 模拟执行成功", 0, logFile.toString());
}
deleteRecursively(stageDir);
appendLog(logFile, "clean_staging removed: " + stageDir);
return InitStepExecutionResult.success("clean_staging applied", 0, logFile.toString());
return InitStepExecutionResult.success("clean_staging 已应用", 0, logFile.toString());
}
private InitStepExecutionResult executeVerifyReset(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "verify_reset simulated");
return InitStepExecutionResult.success("verify_reset simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "verify_reset 模拟执行成功");
return InitStepExecutionResult.success("verify_reset 模拟执行成功", 0, logPath);
}
Path logFile = prepareLogPath(task, step);
@@ -1183,29 +1183,29 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
if (!failures.isEmpty()) {
String detail = String.join("; ", failures);
appendLog(logFile, "verify_reset failed: " + detail);
return InitStepExecutionResult.failure("verify_reset failed: " + detail, -1, logFile.toString());
appendLog(logFile, "verify_reset 执行失败:" + detail);
return InitStepExecutionResult.failure("verify_reset 执行失败:" + detail, -1, logFile.toString());
}
appendLog(logFile, "verify_reset success");
return InitStepExecutionResult.success("verify_reset success", 0, logFile.toString());
appendLog(logFile, "verify_reset 成功");
return InitStepExecutionResult.success("verify_reset 成功", 0, logFile.toString());
}
private InitStepExecutionResult executeTlqInitCfg(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "tlq_init_cfg skipped for non-direct TLQ mode");
return InitStepExecutionResult.success("tlq_init_cfg skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "tlq_init_cfg 已跳过 for non-direct TLQ mode");
return InitStepExecutionResult.success("tlq_init_cfg 已跳过", 0, logPath);
}
String logPath = writeInternalLog(task, step, "tlq_init_cfg skipped: no longer part of direct customer-config scope");
return InitStepExecutionResult.success("tlq_init_cfg skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "tlq_init_cfg 已跳过: no longer part of direct customer-config scope");
return InitStepExecutionResult.success("tlq_init_cfg 已跳过", 0, logPath);
}
private InitStepExecutionResult executeCaePackageApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "cae_package_apply skipped for non-direct product type");
return InitStepExecutionResult.success("cae_package_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "cae_package_apply 已跳过 for non-direct product type");
return InitStepExecutionResult.success("cae_package_apply 已跳过", 0, logPath);
}
Path zipFile = resolveAndStageInputFile(task, context.cfgZipFileId(), "cae_package", null);
@@ -1219,7 +1219,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
Files.createDirectories(targetDir);
copyDirectoryContents(unpackDir, targetDir);
String message = "cae package applied: " + zipFile + " -> " + targetDir;
String message = "cae package 已应用: " + zipFile + " -> " + targetDir;
String logPath = writeInternalLog(task, step, message);
return InitStepExecutionResult.success(message, 0, logPath);
}
@@ -1227,18 +1227,18 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private InitStepExecutionResult executeCaeUpdate(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType())) {
String logPath = writeInternalLog(task, step, "cae_update skipped for non-direct product type");
return InitStepExecutionResult.success("cae_update skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "cae_update 已跳过 for non-direct product type");
return InitStepExecutionResult.success("cae_update 已跳过", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "cae_update simulated");
return InitStepExecutionResult.success("cae_update simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "cae_update 模拟执行成功");
return InitStepExecutionResult.success("cae_update 模拟执行成功", 0, logPath);
}
String command = normalize(properties.getDirectCaeUpdateCommand());
if (isBlank(command)) {
String logPath = writeInternalLog(task, step, "cae_update validated only: command is blank");
return InitStepExecutionResult.success("cae_update validated", 0, logPath);
String logPath = writeInternalLog(task, step, "cae_update 已校验 only: command is blank");
return InitStepExecutionResult.success("cae_update 已校验", 0, logPath);
}
Path scriptDir = Paths.get(firstNonBlank(properties.getDirectCaeUpdateScriptDir(), "/opt/CAE_Install_App_Resource/CAE"))
@@ -1248,26 +1248,26 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
try {
int code = runCommand(logFile, Arrays.asList("/bin/zsh", "-lc", command), false, scriptDir);
if (code != 0) {
return InitStepExecutionResult.failure("cae_update failed", code, logFile.toString());
return InitStepExecutionResult.failure("cae_update 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("cae_update applied", 0, logFile.toString());
return InitStepExecutionResult.success("cae_update 已应用", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("cae_update interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure("cae_update 被中断", -1, logFile.toString());
}
}
private InitStepExecutionResult executeTlqGatewayApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "tlq_gateway_apply skipped for non-direct TLQ mode");
return InitStepExecutionResult.success("tlq_gateway_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "tlq_gateway_apply 已跳过 for non-direct TLQ mode");
return InitStepExecutionResult.success("tlq_gateway_apply 已跳过", 0, logPath);
}
Path sourceScript = resolveTlqGatewaySourceScript(task);
if (!Files.exists(sourceScript) || !Files.isRegularFile(sourceScript)) {
String logPath = writeInternalLog(task, step, "tlq_gateway_apply failed: source script not found: " + sourceScript);
return InitStepExecutionResult.failure("tlq_gateway_apply failed: source script not found", -1, logPath);
String logPath = writeInternalLog(task, step, "tlq_gateway_apply 执行失败:源脚本不存在: " + sourceScript);
return InitStepExecutionResult.failure("tlq_gateway_apply 执行失败:源脚本不存在", -1, logPath);
}
String rewritten = rewriteGatewayHost(Files.readString(sourceScript), context.cipsGatewayHost());
@@ -1279,58 +1279,58 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
Files.writeString(targetScript, rewritten, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "tlq_gateway_apply simulated: " + targetScript);
return InitStepExecutionResult.success("tlq_gateway_apply simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "tlq_gateway_apply 模拟执行成功: " + targetScript);
return InitStepExecutionResult.success("tlq_gateway_apply 模拟执行成功", 0, logPath);
}
Path logFile = prepareLogPath(task, step);
try {
int code = runCommand(logFile, Arrays.asList("bash", targetScript.toString()), false);
if (code != 0) {
return InitStepExecutionResult.failure("tlq_gateway_apply failed", code, logFile.toString());
return InitStepExecutionResult.failure("tlq_gateway_apply 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("tlq gateway applied: " + targetScript, 0, logFile.toString());
return InitStepExecutionResult.success("tlq gateway 已应用: " + targetScript, 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("tlq_gateway_apply interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure("tlq_gateway_apply 被中断", -1, logFile.toString());
}
}
private InitStepExecutionResult executeRestartTlq(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_TLQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "restart_tlq skipped for non-direct TLQ mode");
return InitStepExecutionResult.success("restart_tlq skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "restart_tlq 已跳过 for non-direct TLQ mode");
return InitStepExecutionResult.success("restart_tlq 已跳过", 0, logPath);
}
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "restart_tlq simulated");
return InitStepExecutionResult.success("restart_tlq simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "restart_tlq 模拟执行成功");
return InitStepExecutionResult.success("restart_tlq 模拟执行成功", 0, logPath);
}
String command = normalize(properties.getDirectTlqRestartCommand());
if (isBlank(command)) {
String logPath = writeInternalLog(task, step, "restart_tlq validated only: command is blank");
return InitStepExecutionResult.success("restart_tlq validated", 0, logPath);
String logPath = writeInternalLog(task, step, "restart_tlq 已校验 only: command is blank");
return InitStepExecutionResult.success("restart_tlq 已校验", 0, logPath);
}
Path logFile = prepareLogPath(task, step);
try {
int code = runCommand(logFile, Arrays.asList("/bin/zsh", "-lc", command), false);
if (code != 0) {
return InitStepExecutionResult.failure("restart_tlq failed", code, logFile.toString());
return InitStepExecutionResult.failure("restart_tlq 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("restart_tlq applied", 0, logFile.toString());
return InitStepExecutionResult.success("restart_tlq 已应用", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("restart_tlq interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure("restart_tlq 被中断", -1, logFile.toString());
}
}
private InitStepExecutionResult executeCfmqLinkApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_CFMQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "cfmq_link_apply skipped for non-direct CFMQ mode");
return InitStepExecutionResult.success("cfmq_link_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "cfmq_link_apply 已跳过 for non-direct CFMQ mode");
return InitStepExecutionResult.success("cfmq_link_apply 已跳过", 0, logPath);
}
Path sourceScript = resolveAndStageInputFile(task, context.cfmqConfigFileId(), "cfmq_link_script", null);
@@ -1341,28 +1341,28 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
Files.writeString(stagedScript, rewritten, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
if (properties.getMode() == InitExecutorProperties.Mode.SIMULATE) {
String logPath = writeInternalLog(task, step, "cfmq_link_apply simulated: " + stagedScript);
return InitStepExecutionResult.success("cfmq_link_apply simulated", 0, logPath);
String logPath = writeInternalLog(task, step, "cfmq_link_apply 模拟执行成功: " + stagedScript);
return InitStepExecutionResult.success("cfmq_link_apply 模拟执行成功", 0, logPath);
}
Path logFile = prepareLogPath(task, step);
try {
int code = runCommand(logFile, Arrays.asList("bash", stagedScript.toString()), false);
if (code != 0) {
return InitStepExecutionResult.failure("cfmq_link_apply failed", code, logFile.toString());
return InitStepExecutionResult.failure("cfmq_link_apply 执行失败", code, logFile.toString());
}
return InitStepExecutionResult.success("cfmq_link_apply applied", 0, logFile.toString());
return InitStepExecutionResult.success("cfmq_link_apply 已应用", 0, logFile.toString());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return InitStepExecutionResult.failure("cfmq_link_apply interrupted", -1, logFile.toString());
return InitStepExecutionResult.failure("cfmq_link_apply 被中断", -1, logFile.toString());
}
}
private InitStepExecutionResult executeCfmqRouteApply(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
PlanContext context = loadPlanContext(task);
if (!"DIRECT".equals(context.productType()) || !"RABBITMQ_CFMQ".equals(context.mqType())) {
String logPath = writeInternalLog(task, step, "cfmq_route_apply skipped for non-direct CFMQ mode");
return InitStepExecutionResult.success("cfmq_route_apply skipped", 0, logPath);
String logPath = writeInternalLog(task, step, "cfmq_route_apply 已跳过 for non-direct CFMQ mode");
return InitStepExecutionResult.success("cfmq_route_apply 已跳过", 0, logPath);
}
Path targetDir = Paths.get(firstNonBlank(properties.getDirectCaeTargetDir(), properties.getCfmqTargetDir()))
@@ -1387,7 +1387,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
Path pmtsMsgHdl = targetDir.resolve("pmtsmsghdl.cfg");
Files.deleteIfExists(pmtsMsgHdl);
appendLog(logFile, "cfmq route files changed=" + routeChanged + ", pmtsmsghdl deleted if exists=" + pmtsMsgHdl);
return InitStepExecutionResult.success("cfmq_route_apply applied", 0, logFile.toString());
return InitStepExecutionResult.success("cfmq_route_apply 已应用", 0, logFile.toString());
}
private InitStepExecutionResult executeVerify(InitTaskEntity task, InitTaskStepEntity step) throws IOException {
@@ -1398,10 +1398,10 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String logPath = writeInternalLog(
task,
step,
"verify failed: cpconfig not found: " + cpconfigPath
+ ", candidates=" + formatPathList(resolvedCpconfig.candidates())
"verify 执行失败:cpconfig不存在:" + cpconfigPath
+ ",候选路径=" + formatPathList(resolvedCpconfig.candidates())
);
return InitStepExecutionResult.failure("verify failed: cpconfig not found", -1, logPath);
return InitStepExecutionResult.failure("verify执行失败:cpconfig不存在", -1, logPath);
}
Map<String, String> configMap = readConfigMap(cpconfigPath);
@@ -1415,11 +1415,11 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
if (!failures.isEmpty()) {
String detail = String.join("; ", failures);
String logPath = writeInternalLog(task, step, "verify failed: " + detail);
return InitStepExecutionResult.failure("verify failed: " + detail, -1, logPath);
String logPath = writeInternalLog(task, step, "verify 执行失败:" + detail);
return InitStepExecutionResult.failure("verify 执行失败:" + detail, -1, logPath);
}
String message = "verify success, cpconfig=" + cpconfigPath;
String message = "verify 成功, cpconfig=" + cpconfigPath;
String logPath = writeInternalLog(task, step, message);
return InitStepExecutionResult.success(message, 0, logPath);
}
@@ -1707,7 +1707,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private void ensureFileId(String rawValue) {
if (!isLikelyFileId(rawValue)) {
throw new IllegalArgumentException("fileId is invalid: " + normalize(rawValue));
throw new IllegalArgumentException("fileId无效:" + normalize(rawValue));
}
}
@@ -1730,7 +1730,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
) throws IOException {
ResolvedInputFile resolved = resolveInputFile(fileId, preferredFileName);
if (resolved.path() == null) {
String detail = fieldName + " file not found, candidates=" + formatPathList(resolved.candidates());
String detail = fieldName + " 文件不存在,候选路径=" + formatPathList(resolved.candidates());
throw new IllegalArgumentException(detail);
}
String stageFileName = firstNonBlank(preferredFileName, resolved.path().getFileName() == null ? fieldName : resolved.path().getFileName().toString());
@@ -1927,7 +1927,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private void runCommandAllowAlreadyExists(Path logFile, List<String> command, String scene) throws IOException, InterruptedException {
int code = runCommand(logFile, command, true);
if (code != 0) {
appendLog(logFile, scene + " already exists or command failed with exit code=" + code + ", ignored");
appendLog(logFile, scene + " already exists or 命令执行失败 with exit code=" + code + ", ignored");
}
}
@@ -1961,12 +1961,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
boolean finished = process.waitFor(properties.getTimeoutSec(), TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
appendLog(logFile, "command timeout: " + String.join(" ", command));
appendLog(logFile, "命令执行超时: " + String.join(" ", command));
return -1;
}
int exitCode = process.exitValue();
if (!ignoreFailure && exitCode != 0) {
appendLog(logFile, "command failed: " + String.join(" ", command) + ", exitCode=" + exitCode);
appendLog(logFile, "命令执行失败: " + String.join(" ", command) + ", exitCode=" + exitCode);
}
return exitCode;
}
@@ -2077,7 +2077,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
String plain = normalize(input);
String key = normalize(secretKey);
if (isBlank(key)) {
throw new IllegalArgumentException("secret key is blank");
throw new IllegalArgumentException("密钥为空");
}
SecretKeySpec desKey = new SecretKeySpec(key.getBytes(), "DESede");
Cipher cipher = Cipher.getInstance("DESede");
@@ -2105,12 +2105,12 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private PlanContext loadPlanContext(InitTaskEntity task) throws IOException {
if (isBlank(task.getInitPlanJson())) {
throw new IllegalArgumentException("initPlanJson is empty");
throw new IllegalArgumentException("initPlanJson为空");
}
JsonNode root = objectMapper.readTree(task.getInitPlanJson());
JsonNode request = root.path("request");
if (request.isMissingNode() || request.isNull()) {
throw new IllegalArgumentException("initPlanJson.request missing, recreate task with latest schema");
throw new IllegalArgumentException("initPlanJson.request缺失,请使用最新结构重新创建任务");
}
JsonNode nodes = request.path("nodes");
@@ -2254,7 +2254,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
failures.add(label + " process verification interrupted");
failures.add(label + " process verification 被中断");
}
}
@@ -2280,7 +2280,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
failures.add("database verification interrupted: " + identifier);
failures.add("database verification 被中断: " + identifier);
}
}
@@ -2307,7 +2307,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
failures.add(message + " (verification interrupted)");
failures.add(message + " (verification 被中断)");
}
}
@@ -2324,7 +2324,7 @@ public class ConfigurableInitStepExecutor implements InitStepExecutor {
private String requireMysqlIdentifier(String databaseName) {
String normalized = normalize(databaseName);
if (!normalized.matches("[A-Za-z0-9_]+")) {
throw new IllegalArgumentException("invalid database identifier: " + databaseName);
throw new IllegalArgumentException("数据库标识无效:" + databaseName);
}
return normalized;
}
@@ -88,7 +88,7 @@ public class InitServiceImpl implements InitService {
@Override
public InitPreviewResponse preview(InitPreviewRequest request) {
if (request == null) {
throw new IllegalArgumentException("request must not be null");
throw new IllegalArgumentException("请求不能为空");
}
String productType = resolvePresetProductType();
@@ -122,7 +122,7 @@ public class InitServiceImpl implements InitService {
@Override
public InitCreateTaskResponse createTask(InitPreviewRequest request) {
if (request == null) {
throw new IllegalArgumentException("request must not be null");
throw new IllegalArgumentException("请求不能为空");
}
String productType = resolvePresetProductType();
@@ -300,7 +300,7 @@ public class InitServiceImpl implements InitService {
@Override
public InitTaskStepLogResponse getTaskStepLog(String taskId, int stepNo) {
if (stepNo <= 0) {
throw new IllegalArgumentException("stepNo must be > 0");
throw new IllegalArgumentException("stepNo必须大于0");
}
requireTask(taskId);
InitTaskStepEntity step = requireStep(taskId, stepNo);
@@ -343,7 +343,7 @@ public class InitServiceImpl implements InitService {
InitTaskEntity task = requireTask(taskId);
String taskType = normalize(task.getTaskType());
if (!TASK_TYPE_RESET.equals(taskType)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "reset task not found: " + taskId);
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "重置任务不存在:" + taskId);
}
return executeTaskInternal(taskId);
}
@@ -450,7 +450,7 @@ public class InitServiceImpl implements InitService {
private InitTaskEntity requireTask(String taskId) {
return initTaskRepository.findByTaskId(taskId)
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "init task not found: " + taskId));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "初始化任务不存在:" + taskId));
}
private InitTaskStepEntity requireStep(String taskId, int stepNo) {
@@ -459,7 +459,7 @@ public class InitServiceImpl implements InitService {
.findFirst()
.orElseThrow(() -> new BizException(
ErrorCode.BIZ_ERROR.getCode(),
"init task step not found: taskId=" + taskId + ", stepNo=" + stepNo
"初始化任务步骤不存在:taskId=" + taskId + ", stepNo=" + stepNo
));
}
@@ -611,14 +611,14 @@ public class InitServiceImpl implements InitService {
private String resolvePresetProductType() {
String raw = normalize(cisdPresetProperties.getProductType());
if (isBlank(raw)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device preset product type is missing, initialization is forbidden");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "设备预置产品类型缺失,禁止初始化");
}
String productType = raw.toUpperCase(Locale.ROOT);
if (!PRESET_PRODUCT_TYPES.contains(productType)) {
throw new BizException(
ErrorCode.BIZ_ERROR.getCode(),
"device preset product type is invalid: " + raw + ", initialization is forbidden"
"设备预置产品类型无效:" + raw + ",禁止初始化"
);
}
return productType;
@@ -634,10 +634,10 @@ public class InitServiceImpl implements InitService {
private void validateCommon(InitPreviewRequest request) {
if (request.getNodes() == null) {
throw new IllegalArgumentException("nodes is required");
throw new IllegalArgumentException("nodes不能为空");
}
if (request.getMq() == null) {
throw new IllegalArgumentException("mq is required");
throw new IllegalArgumentException("mq不能为空");
}
requireNotBlank("orgCode", request.getOrgCode());
@@ -681,21 +681,21 @@ public class InitServiceImpl implements InitService {
ResolvedResetContext context = resolveResetContextFromInitTask(latestInit.get());
if (!productType.equals(context.productType())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "device preset product type changed, reset context is invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "设备预置产品类型已变更,重置上下文无效");
}
requireAllowed("mqType", context.mqType(), resolveStandardMqAllowed(productType));
if (("RABBITMQ".equals(context.mqType()) || "RABBITMQ_TLQ".equals(context.mqType())) && isBlank(context.channelUsername())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "current init context missing rabbitmq channel username");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "当前初始化上下文缺少RabbitMQ通道用户名");
}
return context;
}
private ResolvedResetContext resolveResetContextFromInitTask(InitTaskEntity task) {
if (task == null) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "成功ful init task is missing");
}
if (isBlank(task.getInitPlanJson())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "成功ful init task snapshot is missing");
}
try {
JsonNode root = objectMapper.readTree(task.getInitPlanJson());
@@ -707,20 +707,20 @@ public class InitServiceImpl implements InitService {
String channelUsername = firstNonBlank(text(mq, "channelUsername"), text(request, "channelUsername"));
if (isBlank(productType) || isBlank(orgCode) || isBlank(mqType)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is incomplete");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "成功ful init task snapshot is incomplete");
}
return new ResolvedResetContext(productType, normalize(orgCode), normalizeUpper(mqType), normalize(channelUsername), task.getTaskId());
} catch (JsonProcessingException ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse 成功ful init task snapshot");
}
}
private CurrentInitConfigResponse resolveCurrentInitConfigFromTask(InitTaskEntity task) {
if (task == null) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task is missing");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "成功ful init task is missing");
}
if (isBlank(task.getInitPlanJson())) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "successful init task snapshot is missing");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "成功ful init task snapshot is missing");
}
try {
JsonNode root = objectMapper.readTree(task.getInitPlanJson());
@@ -747,7 +747,7 @@ public class InitServiceImpl implements InitService {
response.setCfmqFileNames(buildCfmqFileNames(mq, mqType));
return response;
} catch (JsonProcessingException ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse successful init task snapshot");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to parse 成功ful init task snapshot");
}
}
@@ -812,7 +812,7 @@ public class InitServiceImpl implements InitService {
requireAllowed("deployMode", deployMode, DEPLOY_STANDARD);
requireAllowed("mq.mqType", mqType, resolveStandardMqAllowed(productType));
if (request.getLicenses() == null) {
throw new IllegalArgumentException("licenses is required");
throw new IllegalArgumentException("licenses不能为空");
}
requireNotBlank("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId());
requireFileId("licenses.receiverLicenseFileId", request.getLicenses().getReceiverLicenseFileId());
@@ -834,7 +834,7 @@ public class InitServiceImpl implements InitService {
if ("INDIRECT".equals(productType)) {
return MQ_INDIRECT;
}
throw new IllegalArgumentException("unsupported standard productType: " + productType);
throw new IllegalArgumentException("不支持的标准产品类型:" + productType);
}
private void validateDirect(InitPreviewRequest request, String deployMode, String mqType) {
@@ -174,7 +174,7 @@ public class AuditBackupService {
log.info("审计日志备份完成,共备份 {} 条", logList.size());
BackupConfigEntity config = backupConfigRepository.find().orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "backupConfig not found"));
BackupConfigEntity config = backupConfigRepository.find().orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "备份配置不存在"));
config.setLastBackupTime(LocalDateTime.now());
backupConfigRepository.saveOrUpdate(config);
@@ -308,7 +308,7 @@ public class AuditBackupService {
log.info("审计日志备份完成,共备份 {} 条", logList.size());
BackupConfigEntity config = backupConfigRepository.find().orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "backupConfig not found"));
BackupConfigEntity config = backupConfigRepository.find().orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "备份配置不存在"));
config.setLastBackupTime(LocalDateTime.now());
backupConfigRepository.saveOrUpdate(config);
@@ -362,7 +362,7 @@ public class AuditBackupService {
writePart(outputStream, cipherBytes);
return outputStream.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException("failed to build package signature source", ex);
throw new IllegalStateException("构建包签名原文失败", ex);
}
}
@@ -374,7 +374,7 @@ public class AuditBackupService {
private byte[] sign(byte[] dataToSign){
if (dataToSign == null) {
throw new IllegalArgumentException("dataToSign is required");
throw new IllegalArgumentException("dataToSign不能为空");
}
UserKeySignRequest request = new UserKeySignRequest();
request.setKeyIndex(1);
@@ -384,7 +384,7 @@ public class AuditBackupService {
private BackupDataResult signWithDigest(byte[] dataToSign){
if (dataToSign == null) {
throw new IllegalArgumentException("dataToSign is required");
throw new IllegalArgumentException("dataToSign不能为空");
}
UserKeySignRequest request = new UserKeySignRequest();
request.setKeyIndex(1);
@@ -63,8 +63,8 @@ public class AuditLogRestoreService {
Path extractDir = Paths.get(TEMP_RESTORE_DIR, batchId);
int totalCount = 0;
int skippedCount = 0;
int successCount = 0;
int 已跳过Count = 0;
int 成功Count = 0;
boolean restoreSuccess = false;
try {
log.info("开始审计日志恢复任务, 批次号: {}", batchId);
@@ -105,7 +105,7 @@ public class AuditLogRestoreService {
// } catch (BizException ex) {
// throw ex;
// } catch (RuntimeException ex) {
// throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification failed");
// throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification 执行失败");
// }
try {
@@ -157,7 +157,7 @@ public class AuditLogRestoreService {
for (OperationAuditLogEntity logEntity : parsedLogs) {
if (existSignSet.contains(logEntity.getSignValue())) {
skippedCount ++; // 若已存在:跳过
已跳过Count ++; // 若已存在:跳过
} else {
pendingInsertList.add(logEntity); // 若不存在:加入待插入队列
}
@@ -165,16 +165,16 @@ public class AuditLogRestoreService {
// 执行批量插入
if (!pendingInsertList.isEmpty()) {
successCount = auditLogRepository.addBatch(pendingInsertList);
成功Count = auditLogRepository.addBatch(pendingInsertList);
}
// RestoreResultResponse result = new RestoreResultResponse();
// result.setTotalCount(totalCount);
// result.setSkippedCount(skippedCount);
// result.setSuccessCount(successCount);
// result.setSuccessCount(成功Count);
restoreSuccess = true;
return String.format("恢复完成。共解析 %d 条,跳过已存在 %d 条,成功恢复 %d 条",
totalCount, skippedCount, successCount);
totalCount, 已跳过Count, 成功Count);
} catch (Exception e) {
log.error("恢复审计日志失败", e);
@@ -187,7 +187,7 @@ public class AuditLogRestoreService {
log.error("清理临时目录失败: {}", extractDir, e);
}
recordRestoreAuditLog(file.getOriginalFilename(), totalCount, skippedCount, successCount, restoreSuccess);
recordRestoreAuditLog(file.getOriginalFilename(), totalCount, 已跳过Count, 成功Count, restoreSuccess);
}
}
@@ -225,7 +225,7 @@ public class AuditLogRestoreService {
writePart(outputStream, cipherBytes);
return outputStream.toByteArray();
} catch (IOException ex) {
throw new IllegalStateException("failed to build package signature source", ex);
throw new IllegalStateException("构建包签名原文失败", ex);
}
}
@@ -311,7 +311,7 @@ public class AuditLogRestoreService {
}
private void recordRestoreAuditLog(String fileName, int total, int skipped, int success, boolean restoreSuccess) {
private void recordRestoreAuditLog(String fileName, int total, int 已跳过, int 成功, boolean restoreSuccess) {
OperationAuditCommand command = new OperationAuditCommand();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
@@ -199,7 +199,7 @@ public class OperationAuditService {
// } catch (BizException ex) {
// throw ex;
// } catch (RuntimeException ex) {
// throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification failed");
// throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "signature verification 执行失败");
// }
@@ -74,12 +74,12 @@ public class LmkController {
private void validatePacketIndexes(List<MasterKeyBackupPacket> packets) {
if (packets == null || packets.size() != 2) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "packets size must be 2");
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "packets数量必须为2");
}
boolean hasPacketOne = packets.stream().anyMatch(packet -> packet != null && packet.getPacketIndex() == 1);
boolean hasPacketTwo = packets.stream().anyMatch(packet -> packet != null && packet.getPacketIndex() == 2);
if (!hasPacketOne || !hasPacketTwo) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "packetIndex must be 1 or 2");
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "packetIndex必须为1或2");
}
}
}
@@ -11,8 +11,8 @@ import java.util.List;
public class MasterKeyBackupPacket {
@Min(value = 1, message = "packetIndex must be 1 or 2")
@Max(value = 2, message = "packetIndex must be 1 or 2")
@Min(value = 1, message = "packetIndex必须为1或2")
@Max(value = 2, message = "packetIndex必须为1或2")
@JsonProperty("packetIndex")
private int packetIndex;
@@ -41,7 +41,7 @@ public class MasterKeyBackupPacket {
}
@JsonIgnore
@AssertTrue(message = "packetIndex must be 1 or 2")
@AssertTrue(message = "packetIndex必须为1或2")
public boolean isPacketIndexValid() {
return packetIndex == 1 || packetIndex == 2;
}
@@ -8,8 +8,8 @@ import java.util.List;
public class MasterKeyRecoverPacketsRequest {
@NotEmpty(message = "packets must not be empty")
@Size(min = 2, max = 2, message = "packets size must be 2")
@NotEmpty(message = "packets不能为空")
@Size(min = 2, max = 2, message = "packets数量必须为2")
private List<@Valid MasterKeyBackupPacket> packets;
public List<MasterKeyBackupPacket> getPackets() {
@@ -8,12 +8,12 @@ import java.util.List;
public class MasterKeyRecoverRequest {
@NotBlank(message = "lmkMac must not be blank")
@NotBlank(message = "lmkMac不能为空")
private String lmkMac;
@NotEmpty(message = "components must not be empty")
@Size(min = 2, max = 2, message = "components size must be 2")
private List<@NotBlank(message = "component must not be blank") String> components;
@NotEmpty(message = "components不能为空")
@Size(min = 2, max = 2, message = "components数量必须为2")
private List<@NotBlank(message = "component不能为空") String> components;
public String getLmkMac() {
return lmkMac;
@@ -12,19 +12,19 @@ public class UKeySignDTO {
/**
* UKey 公钥
*/
@NotBlank(message = "pubKey is required")
@NotBlank(message = "pubKey不能为空")
private String pubKey;
/**
* UKey 序列号。发行签名接口会直接完成绑定,因此必传。
*/
@NotBlank(message = "ukeySerial is required")
@NotBlank(message = "ukeySerial不能为空")
private String ukeySerial;
/**
* 角色内固定席位编号。
*/
@NotNull(message = "uid is required")
@NotNull(message = "uid不能为空")
private Integer uid;
}
@@ -220,16 +220,16 @@ public class LmkServiceImpl implements LmkService {
private String masterKeyCompose(String lmkMacHex, List<String> components) {
if (lmkMacHex == null || lmkMacHex.isBlank()) {
throw new IllegalArgumentException("lmkMac must not be blank");
throw new IllegalArgumentException("lmkMac不能为空");
}
if (components == null || components.size() != LMKConstant.COMPONENT_NUM) {
throw new IllegalArgumentException("master key components size must be " + LMKConstant.COMPONENT_NUM);
throw new IllegalArgumentException("主密钥分量数量必须为" + LMKConstant.COMPONENT_NUM);
}
StringBuilder lmk = new StringBuilder(lmkMacHex);
for (int index = 0; index < LMKConstant.COMPONENT_NUM; index++) {
String component = components.get(index);
if (component == null || component.isBlank()) {
throw new IllegalArgumentException("master key component missing: " + (index + 1));
throw new IllegalArgumentException("主密钥分量缺失:" + (index + 1));
}
lmk.append(component);
}
@@ -307,7 +307,7 @@ public class LmkServiceImpl implements LmkService {
BackupDataResult backupData = pcieCryptoService.backupLmkEx();
byte[] normalized = normalizeBackupData(backupData);
if (normalized.length < LMK_MAC_BYTE_LENGTH) {
throw new IllegalStateException("backup lmk data length is too short: " + normalized.length);
throw new IllegalStateException("备份LMK数据长度过短:" + normalized.length);
}
// 厂商返回格式:LMK_MAC + component1 + component2 + component3。
// 这里先按原生结构完整解析,后续 buildPacket 时再投影成两份外部包。
@@ -320,7 +320,7 @@ public class LmkServiceImpl implements LmkService {
BackupDataResult backupData = pcieCryptoService.backupIkComponent(keyType);
byte[] normalized = normalizeBackupData(backupData);
if (normalized.length < IK_COMPONENT_TOTAL_BYTE_LENGTH) {
throw new IllegalStateException("backup ik data length is too short: " + normalized.length);
throw new IllegalStateException("备份IK数据长度过短:" + normalized.length);
}
// 厂商返回格式:component1 + component2 + component3 + mac + publicKey + publicKeyMac。
// 与 LMK 一样,先解析成原生三分量,再由外层决定如何投影到两份包中。
@@ -402,7 +402,7 @@ public class LmkServiceImpl implements LmkService {
private int requirePacketIndex(int componentIndex) {
if (componentIndex < PACKET_INDEX_ONE || componentIndex > PACKET_INDEX_TWO) {
throw new IllegalArgumentException("packetIndex must be 1 or 2");
throw new IllegalArgumentException("packetIndex必须为1或2");
}
// 这里校验的是“外部包编号”,不是原生分量编号。
return componentIndex;
@@ -450,19 +450,19 @@ public class LmkServiceImpl implements LmkService {
private String requireConsistentLmkMac(List<MasterKeyBackupPacket> packets) {
if (packets == null || packets.size() != LMKConstant.COMPONENT_NUM) {
throw new IllegalArgumentException("packets size must be " + LMKConstant.COMPONENT_NUM);
throw new IllegalArgumentException("packets数量必须为" + LMKConstant.COMPONENT_NUM);
}
String lmkMac = null;
for (MasterKeyBackupPacket packet : packets) {
if (packet == null || packet.getLmk() == null || packet.getLmk().getLmkMac() == null || packet.getLmk().getLmkMac().isBlank()) {
throw new IllegalArgumentException("lmk packet missing");
throw new IllegalArgumentException("LMK分包缺失");
}
if (lmkMac == null) {
lmkMac = packet.getLmk().getLmkMac();
continue;
}
if (!lmkMac.equals(packet.getLmk().getLmkMac())) {
throw new IllegalArgumentException("lmk mac is inconsistent");
throw new IllegalArgumentException("LMK MAC不一致");
}
}
return lmkMac;
@@ -473,14 +473,14 @@ public class LmkServiceImpl implements LmkService {
for (MasterKeyBackupPacket packet : packets) {
MasterKeyPacketIkPart part = keyType == IKEnums.KEY_TYPE_AUTH.getCode() ? packet.getIkAuth() : packet.getIkDevice();
if (part == null || part.getMac() == null || part.getMac().isBlank()) {
throw new IllegalArgumentException("ik mac missing");
throw new IllegalArgumentException("IK MAC缺失");
}
if (mac == null) {
mac = part.getMac();
continue;
}
if (!mac.equals(part.getMac())) {
throw new IllegalArgumentException("ik mac is inconsistent");
throw new IllegalArgumentException("IK MAC不一致");
}
}
return mac;
@@ -491,14 +491,14 @@ public class LmkServiceImpl implements LmkService {
for (MasterKeyBackupPacket packet : packets) {
MasterKeyPacketIkPart part = keyType == IKEnums.KEY_TYPE_AUTH.getCode() ? packet.getIkAuth() : packet.getIkDevice();
if (part == null || part.getPublicKey() == null || part.getPublicKey().isBlank()) {
throw new IllegalArgumentException("ik publicKey missing");
throw new IllegalArgumentException("IK公钥缺失");
}
if (publicKey == null) {
publicKey = part.getPublicKey();
continue;
}
if (!publicKey.equals(part.getPublicKey())) {
throw new IllegalArgumentException("ik publicKey is inconsistent");
throw new IllegalArgumentException("IK公钥不一致");
}
}
return publicKey;
@@ -509,14 +509,14 @@ public class LmkServiceImpl implements LmkService {
for (MasterKeyBackupPacket packet : packets) {
MasterKeyPacketIkPart part = keyType == IKEnums.KEY_TYPE_AUTH.getCode() ? packet.getIkAuth() : packet.getIkDevice();
if (part == null || part.getPublicKeyMac() == null || part.getPublicKeyMac().isBlank()) {
throw new IllegalArgumentException("ik publicKeyMac missing");
throw new IllegalArgumentException("IK公钥MAC缺失");
}
if (publicKeyMac == null) {
publicKeyMac = part.getPublicKeyMac();
continue;
}
if (!publicKeyMac.equals(part.getPublicKeyMac())) {
throw new IllegalArgumentException("ik publicKeyMac is inconsistent");
throw new IllegalArgumentException("IK公钥MAC不一致");
}
}
return publicKeyMac;
@@ -526,12 +526,12 @@ public class LmkServiceImpl implements LmkService {
return packet.getUserKeys().stream()
.filter(item -> item.getKeyIndex() == keyIndex && item.getKeyType() == keyType)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("user key component missing in packet " + packet.getPacketIndex()));
.orElseThrow(() -> new IllegalArgumentException("用户密钥分量缺失,分包:" + packet.getPacketIndex()));
}
private List<MasterKeyBackupPacket> sortPackets(List<MasterKeyBackupPacket> packets) {
if (packets == null || packets.size() != LMKConstant.COMPONENT_NUM) {
throw new IllegalArgumentException("packets size must be " + LMKConstant.COMPONENT_NUM);
throw new IllegalArgumentException("packets数量必须为" + LMKConstant.COMPONENT_NUM);
}
List<MasterKeyBackupPacket> sorted = new ArrayList<>(packets);
sorted.sort(Comparator.comparingInt(MasterKeyBackupPacket::getPacketIndex));
@@ -539,7 +539,7 @@ public class LmkServiceImpl implements LmkService {
for (int index = 0; index < sorted.size(); index++) {
int expected = index + 1;
if (sorted.get(index).getPacketIndex() != expected) {
throw new IllegalArgumentException("packetIndex must be 1,2");
throw new IllegalArgumentException("packetIndex必须为1或2");
}
}
return sorted;
@@ -80,48 +80,48 @@ public class ReplayProtectionServiceImpl implements ReplayProtectionService {
return buildClaimedResult(entity);
}
} catch (RuntimeException ex) {
throw new ReplayProtectionException("failed to persist replay nonce", ex);
throw new ReplayProtectionException("保存防重放nonce失败", ex);
}
var reloaded = replayNonceRepository.findByScopeAndPrincipalIdAndNonce(scope, principalId, nonce);
if (reloaded.isEmpty()) {
throw new ReplayProtectionException("replay nonce claim failed without existing record");
throw new ReplayProtectionException("replay nonce claim 执行失败 without existing record");
}
return buildReplayResult(request, reloaded.get());
}
private void reclaimExpiredNonce(ReplayNonceEntity expiredEntity) {
if (expiredEntity.getId() == null) {
throw new ReplayProtectionException("expired replay nonce id is missing");
throw new ReplayProtectionException("过期防重放nonce ID缺失");
}
try {
replayNonceRepository.deleteById(expiredEntity.getId());
} catch (RuntimeException ex) {
throw new ReplayProtectionException("failed to remove expired replay nonce", ex);
throw new ReplayProtectionException("删除过期防重放nonce失败", ex);
}
}
private void validateRequest(ReplayCheckRequest request) {
if (request == null) {
throw new ReplayProtectionException("replay request must not be null");
throw new ReplayProtectionException("replay 请求不能为空");
}
if (request.getScope() == null) {
throw new ReplayProtectionException("replay scope is required");
throw new ReplayProtectionException("replay scope不能为空");
}
if (isBlank(request.getPrincipalId())) {
throw new ReplayProtectionException("principal id is required");
throw new ReplayProtectionException("principal id不能为空");
}
if (isBlank(request.getNonce())) {
throw new ReplayProtectionException("nonce is required");
throw new ReplayProtectionException("nonce不能为空");
}
if (request.getRequestTimestamp() <= 0L) {
throw new ReplayProtectionException("request timestamp is required");
throw new ReplayProtectionException("request timestamp不能为空");
}
}
private void ensureTimestampWithinWindow(long requestTimestamp, long nowSeconds, long allowedSkewSeconds) {
if (Math.abs(nowSeconds - requestTimestamp) > allowedSkewSeconds) {
throw new ReplayProtectionException("request timestamp is outside allowed window");
throw new ReplayProtectionException("请求时间戳超出允许窗口");
}
}
@@ -165,7 +165,7 @@ public class ReplayProtectionServiceImpl implements ReplayProtectionService {
try {
securityEventRepository.save(event);
} catch (RuntimeException ex) {
throw new ReplayProtectionException("failed to persist security event", ex);
throw new ReplayProtectionException("保存安全事件失败", ex);
}
}
@@ -6,7 +6,7 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "内部签名预览请求")
public class InternalSignPreviewRequest {
@NotBlank(message = "plainText is required")
@NotBlank(message = "plainText不能为空")
@Schema(description = "待签名明文", example = "hello-tms")
private String plainText;
@@ -4,10 +4,10 @@ import jakarta.validation.constraints.NotBlank;
public class OpenSignRequest {
@NotBlank(message = "requestId is required")
@NotBlank(message = "requestId不能为空")
private String requestId;
@NotBlank(message = "payload is required")
@NotBlank(message = "payload不能为空")
private String payload;
private String algorithm;
@@ -32,14 +32,14 @@ public class Sha256MockSignProvider implements SignProvider {
if ("SHA-256".equals(value)) {
return DEFAULT_ALGORITHM;
}
throw new IllegalArgumentException("Unsupported sign algorithm: " + algorithm);
throw new IllegalArgumentException("不支持的签名算法:" + algorithm);
}
private byte[] digest(String algorithm, byte[] source) {
try {
return MessageDigest.getInstance(algorithm).digest(source);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("Algorithm unavailable: " + algorithm, ex);
throw new IllegalStateException("算法不可用:" + algorithm, ex);
}
}
@@ -53,7 +53,7 @@ public class UpgradeTaskRunner {
Files.writeString(logPath, "");
FileRecordEntity record = fileRecordRepository.findByFileId(task.getPackageFileId())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包文件不存在:" + task.getPackageFileId()));
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId(), Path.of(record.getStoragePath()));
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
upgradePackageStagingService.extractPayloadZip(stagedDir);
@@ -114,7 +114,7 @@ public class UpgradeTaskRunner {
} catch (Exception ex) {
try {
Files.createDirectories(logPath.getParent());
Files.writeString(logPath, ex.getMessage() == null ? "upgrade failed" : ex.getMessage());
Files.writeString(logPath, ex.getMessage() == null ? "upgrade 执行失败" : ex.getMessage());
} catch (IOException ignored) {
}
upgradeTaskRepository.updateStatus(
@@ -136,13 +136,13 @@ public class UpgradeTaskRunner {
Files.writeString(logPath, "");
FileRecordEntity record = fileRecordRepository.findByFileId(task.getPackageFileId())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId()));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包文件不存在:" + task.getPackageFileId()));
Path stagedDir = upgradePackageStagingService.stage(task.getTaskId() + "-rollback", Path.of(record.getStoragePath()));
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
upgradePackageStagingService.extractPayloadZip(stagedDir);
String rollbackPath = manifest.getEntrypoints() == null ? "" : trim(manifest.getEntrypoints().getRollback());
if (rollbackPath.isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级回滚脚本不存在");
}
// 回滚不自动触发,只有外部调用 rollback 接口后才执行包内 rollback 脚本。
int exitCode = runRequiredScript("rollback", stagedDir.normalize().toAbsolutePath(), rollbackPath, task, logPath);
@@ -170,7 +170,7 @@ public class UpgradeTaskRunner {
} catch (Exception ex) {
try {
Files.createDirectories(logPath.getParent());
Files.writeString(logPath, ex.getMessage() == null ? "upgrade rollback failed" : ex.getMessage());
Files.writeString(logPath, ex.getMessage() == null ? "upgrade rollback 执行失败" : ex.getMessage());
} catch (IOException ignored) {
}
upgradeTaskRepository.updateStatus(
@@ -214,8 +214,8 @@ public class UpgradeTaskRunner {
return;
}
try {
writeLogLine(logPath, rollback ? "rollback success, scheduling detached TMS restart"
: "upgrade success, scheduling detached TMS restart");
writeLogLine(logPath, rollback ? "rollback 成功, scheduling detached TMS restart"
: "upgrade 成功, scheduling detached TMS restart");
triggerDetachedTmsRestart(logPath, rollback);
} catch (IOException ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to schedule detached tms restart");
@@ -97,16 +97,16 @@ public class UpgradePackageService {
try {
return objectMapper.readValue(Files.readString(manifestPath), UpgradeManifest.class);
} catch (IOException ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json无效");
}
}
private StagedUpgradePackage stagePackage(String fileId) {
FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包文件不存在:" + normalize(fileId)));
Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath();
if (!Files.exists(packagePath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "磁盘上的升级包文件不存在");
}
// 同一个 fileId 每次预检都会重新解压,确保读取的是当前上传文件内容。
@@ -114,25 +114,25 @@ public class UpgradePackageService {
Path manifestPath = stagedDir.resolve("manifest.json");
Path signaturePath = stagedDir.resolve("signature.sig");
if (!Files.exists(manifestPath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包结构无效");
}
return new StagedUpgradePackage(stagedDir, manifestPath, signaturePath, readManifest(manifestPath));
}
private void validateManifest(UpgradeManifest manifest) {
if (manifest == null) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json无效");
}
if (normalize(manifest.getPackageId()).isEmpty()
|| normalize(manifest.getTaskType()).isEmpty()
|| normalize(manifest.getProductType()).isEmpty()
|| normalize(manifest.getVersion()).isEmpty()
|| normalize(manifest.getMinCompatibleVersion()).isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json无效");
}
UpgradeManifest.Entrypoints entrypoints = manifest.getEntrypoints();
if (entrypoints == null || normalize(entrypoints.getExecute()).isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级执行脚本不存在");
}
}
@@ -152,15 +152,15 @@ public class UpgradePackageService {
private void validatePayloadZipSm3(Path stagedDir, UpgradeManifest manifest) {
String expected = normalize(manifest.getPayloadSm3()).toLowerCase(Locale.ROOT);
if (expected.isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package payload sm3 missing");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包payload SM3缺失");
}
if (!expected.matches("[0-9a-f]{64}")) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package payload sm3 invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包payload SM3无效");
}
Path root = stagedDir.normalize().toAbsolutePath();
Path payloadZip = root.resolve("payload.zip").normalize().toAbsolutePath();
if (!payloadZip.startsWith(root) || !Files.exists(payloadZip) || !Files.isRegularFile(payloadZip)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade payload.zip not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级payload.zip不存在");
}
if (!expected.equals(sm3Hex(payloadZip))) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package payload sm3 mismatch");
@@ -171,7 +171,7 @@ public class UpgradePackageService {
Path executePath = stagedDir.resolve(normalize(manifest.getEntrypoints().getExecute())).normalize().toAbsolutePath();
// execute 路径必须在 staging 目录内,避免 manifest 声明外部系统脚本。
if (!executePath.startsWith(stagedDir.normalize().toAbsolutePath()) || !Files.exists(executePath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade execute script not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级执行脚本不存在");
}
}
@@ -202,7 +202,7 @@ public class UpgradePackageService {
}
return builder.toString();
} catch (IOException ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package checksum invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包校验和无效");
}
}
@@ -91,28 +91,28 @@ public class UpgradeService {
public UpgradePreviewResponse preview(UpgradePreviewRequest request) {
if (request == null || request.getFileId() == null || request.getFileId().isBlank()) {
throw new IllegalArgumentException("fileId must not be blank");
throw new IllegalArgumentException("fileId不能为空");
}
return upgradePackageService.preview(request.getFileId());
}
public UpgradeCreateTaskResponse createTask(UpgradeCreateTaskRequest request) {
if (request == null || request.getFileId() == null || request.getFileId().isBlank()) {
throw new IllegalArgumentException("fileId must not be blank");
throw new IllegalArgumentException("fileId不能为空");
}
if (request.getTaskType() == null || request.getTaskType().isBlank()) {
throw new IllegalArgumentException("taskType must not be blank");
throw new IllegalArgumentException("taskType不能为空");
}
// 创建任务前重新预检一次,避免用户上传后替换文件或设备版本状态发生变化。
UpgradePreviewResponse preview = upgradePackageService.preview(request.getFileId());
if (!preview.isAllowed()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), blankAs(preview.getBlockReason(), "upgrade preview not allowed"));
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), blankAs(preview.getBlockReason(), "升级预检不允许继续"));
}
if (!trim(request.getTaskType()).equals(trim(preview.getTaskType()))) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "selected taskType does not match package taskType");
}
FileRecordEntity fileRecord = fileRecordRepository.findByFileId(request.getFileId().trim())
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + request.getFileId().trim()));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包文件不存在:" + request.getFileId().trim()));
UpgradeTaskEntity entity = new UpgradeTaskEntity();
entity.setTaskId(buildTaskId());
@@ -175,7 +175,7 @@ public class UpgradeService {
// 回滚能力由升级包声明;TMS 不假定所有包都能安全回滚。
UpgradeManifest manifest = upgradePackageService.loadManifest(task.getPackageFileId());
if (manifest.getEntrypoints() == null || trim(manifest.getEntrypoints().getRollback()).isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级回滚脚本不存在");
}
UpgradeTaskEntity runningTask = upgradeTaskRepository.findRunningTask().orElse(null);
if (runningTask != null && !runningTask.getTaskId().equals(taskId)) {
@@ -259,7 +259,7 @@ public class UpgradeService {
private UpgradeTaskEntity loadTask(String taskId) {
return upgradeTaskRepository.findByTaskId(trim(taskId))
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade task not found: " + trim(taskId)));
.orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级任务不存在:" + trim(taskId)));
}
private UpgradeTaskResponse toResponse(UpgradeTaskEntity entity) {
@@ -312,7 +312,7 @@ public class UpgradeService {
try {
return LocalDate.parse(upgradeDate, UPGRADE_DATE_FORMAT);
} catch (DateTimeParseException ex) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "upgradeDate must be yyyy-MM-dd");
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "upgradeDate必须为yyyy-MM-dd");
}
}
}
@@ -11,7 +11,7 @@ public class NoopUpgradePackageSignatureVerifier implements UpgradePackageSignat
@Override
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
if (!Files.exists(signaturePath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包签名文件不存在");
}
}
}
@@ -41,7 +41,7 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
@Override
public void verify(UpgradeManifest manifest, Path manifestPath, Path signaturePath) {
if (!Files.exists(signaturePath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包签名文件不存在");
}
// 第一版升级包仅对 manifest.json 做离线软验签,避免在包内脚本执行前信任未授权包。
@@ -54,12 +54,12 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
signature.initVerify(publicKey);
signature.update(manifestBytes);
if (!signature.verify(signatureBytes)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification 执行失败");
}
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification failed");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature verification 执行失败");
}
}
@@ -69,7 +69,7 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem path not configured");
}
if (!Files.exists(pemPath)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem file not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级签名公钥PEM文件不存在");
}
try {
String pemContent = Files.readString(pemPath, StandardCharsets.UTF_8);
@@ -77,7 +77,7 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
Object parsed = parser.readObject();
// 约定使用 X.509 SubjectPublicKeyInfo 格式的 PUBLIC KEY PEM。
if (!(parsed instanceof SubjectPublicKeyInfo publicKeyInfo)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级签名公钥PEM无效");
}
return new JcaPEMKeyConverter().setProvider(PROVIDER).getPublicKey(publicKeyInfo);
}
@@ -86,14 +86,14 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
} catch (IOException ex) {
throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to read upgrade signature public key pem");
} catch (Exception ex) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key pem is invalid");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级签名公钥PEM无效");
}
}
private byte[] loadSignature(Path signaturePath) {
byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file");
if (raw.length == 0) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级包签名文件为空");
}
String text = new String(raw, StandardCharsets.UTF_8).trim();
@@ -39,7 +39,7 @@ public class UpgradePackageStagingService {
Path root = stagedDir.normalize().toAbsolutePath();
Path payloadZip = root.resolve("payload.zip").normalize().toAbsolutePath();
if (!payloadZip.startsWith(root) || !Files.exists(payloadZip) || !Files.isRegularFile(payloadZip)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade payload.zip not found");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "升级payload.zip不存在");
}
try {
deleteDirectoryIfExists(root.resolve("payload"));
@@ -53,17 +53,17 @@ public class InternalApiAuthInterceptor implements HandlerInterceptor {
String sessionToken = extractSessionToken(request);
if (sessionToken == null || sessionToken.isBlank()) {
writeSessionInvalid(response, "session token missing");
writeSessionInvalid(response, "会话令牌缺失");
return false;
}
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken).orElse(null);
if (session == null || session.getLogoutAt() != null) {
writeSessionInvalid(response, "session not found");
writeSessionInvalid(response, "会话不存在");
return false;
}
if (session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) {
writeSessionInvalid(response, "session expired");
writeSessionInvalid(response, "会话已过期");
return false;
}
@@ -51,7 +51,7 @@ public class InternalApiReplayInterceptor implements HandlerInterceptor {
String sessionToken = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN);
if (isBlank(sessionToken)) {
writeJson(response, HttpServletResponse.SC_UNAUTHORIZED, ErrorCode.UNAUTHORIZED.getCode(), "session token missing");
writeJson(response, HttpServletResponse.SC_UNAUTHORIZED, ErrorCode.UNAUTHORIZED.getCode(), "会话令牌缺失");
return false;
}
@@ -122,7 +122,7 @@ public class InternalApiReplayInterceptor implements HandlerInterceptor {
}
return builder.toString();
} catch (Exception ex) {
throw new ReplayProtectionException("failed to hash request body", ex);
throw new ReplayProtectionException("请求体哈希计算失败", ex);
}
}
@@ -75,7 +75,7 @@ public class OpenApiSignAuthInterceptor implements HandlerInterceptor {
long now = Instant.ofEpochMilli(clock.millis()).getEpochSecond();
long skew = securityProperties.getOpenapi().getTimestampSkewSeconds();
if (Math.abs(now - requestEpoch) > skew) {
writeUnauthorized(response, "timestamp expired");
writeUnauthorized(response, "时间戳已过期");
return false;
}
}
@@ -84,7 +84,7 @@ public class OpenApiSignAuthInterceptor implements HandlerInterceptor {
String expected = HmacUtil.hmacSha256Hex(appSecret, content);
if (!secureEquals(expected, signature.toLowerCase())) {
writeUnauthorized(response, "signature invalid");
writeUnauthorized(response, "签名无效");
return false;
}
+8 -2
View File
@@ -5,8 +5,8 @@ spring:
servlet:
multipart:
# Spring/Tomcat multipart 解析上限;必须不小于业务上传上限,否则请求会在进入 Controller 前被拒绝。
max-file-size: ${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600}
max-request-size: ${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600}
max-file-size: ${TMS_MULTIPART_MAX_FILE_SIZE_BYTES:524288000}
max-request-size: ${TMS_MULTIPART_MAX_REQUEST_SIZE_BYTES:536870912}
profiles:
# 默认激活环境;可通过 --spring.profiles.active=prod 覆盖。
active: dev
@@ -49,6 +49,12 @@ tms:
upload-base-dir: ${TMS_FILE_STORAGE_UPLOAD_BASE_DIR:/home/tms/uploads}
# 上传文件大小上限(字节)。
max-file-size-bytes: ${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600}
cert:
crl:
# CRL 导入临时目录;原始文件只在解析过程中暂存,导入结束后删除。
temp-dir: ${TMS_CERT_CRL_TEMP_DIR:/home/tms/tmp/crl-import}
# CRL 文件大小上限,默认 500MiB。
max-file-size-bytes: ${TMS_CERT_CRL_MAX_FILE_SIZE_BYTES:524288000}
init:
executor:
# 初始化执行模式:SIMULATE(仅模拟,不执行外部命令) / LOCAL(本机执行脚本命令)。
@@ -542,7 +542,7 @@ CREATE TABLE IF NOT EXISTS t_key_entity (
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 / RSA',
key_len INT NOT NULL COMMENT 'SM2 fixed 256 in V1',
key_idx INT NOT NULL COMMENT 'card key index, allowed range [2..8]',
backup_data VARCHAR(500) NOT NULL COMMENT 'SDFE_BackupUserKey output',
backup_data TEXT NOT NULL COMMENT 'SDFE_BackupUserKey output',
public_key VARCHAR(500) NOT NULL COMMENT 'public key, Base64/PEM',
p10_data VARCHAR(2000) NULL COMMENT 'PKCS#10 PEM content',
subject_dn VARCHAR(500) NULL COMMENT 'DN used when generating P10',
@@ -587,7 +587,7 @@ CREATE TABLE IF NOT EXISTS t_certificate (
CREATE TABLE IF NOT EXISTS t_trusted_cert (
id BIGINT PRIMARY KEY,
alias VARCHAR(128) NOT NULL,
alias VARCHAR(128) NULL,
subject_dn VARCHAR(1024) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
@@ -614,7 +614,7 @@ CREATE TABLE IF NOT EXISTS t_certificate_crl (
crl_number VARCHAR(128) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
crl_data MEDIUMTEXT NOT NULL COMMENT 'CRL PEM content',
crl_data MEDIUMTEXT NOT NULL COMMENT 'reserved legacy CRL PEM content; large CRLs are not stored inline',
this_update DATETIME(3) NOT NULL,
next_update DATETIME(3) NULL,
revoked_count INT NOT NULL,
@@ -646,6 +646,25 @@ CREATE TABLE IF NOT EXISTS t_certificate_crl_revoked (
KEY idx_t_certificate_crl_revoked_revocation_time (revocation_time)
);
CREATE TABLE IF NOT EXISTS t_certificate_crl_import_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
original_filename VARCHAR(255) NULL,
file_size BIGINT NULL,
status VARCHAR(32) NOT NULL COMMENT 'PENDING/RUNNING/SUCCESS/FAILED',
fingerprint VARCHAR(128) NULL COMMENT 'SHA-256 fingerprint after successful parse',
crl_id BIGINT NULL,
revoked_count INT NOT NULL DEFAULT 0,
error_message VARCHAR(512) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_t_certificate_crl_import_task_task_id (task_id),
KEY idx_t_certificate_crl_import_task_status (status),
KEY idx_t_certificate_crl_import_task_create_time (create_time)
);
-- -----------------------------------------------------------------------------
-- 日志表
@@ -714,4 +733,4 @@ CREATE TABLE IF NOT EXISTS tms_backup_config (
last_backup_time datetime DEFAULT NULL COMMENT '上次执行时间',
sign_data varchar(255) NOT NULL COMMENT '其他字段的签名值',
PRIMARY KEY (`id`)
)
)
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS t_key_entity (
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 / RSA',
key_len INT NOT NULL COMMENT 'SM2 fixed 256 in V1',
key_idx INT NOT NULL COMMENT 'card key index, allowed range [2..8]',
backup_data VARCHAR(500) NOT NULL COMMENT 'SDFE_BackupUserKey output',
backup_data TEXT NOT NULL COMMENT 'SDFE_BackupUserKey output',
public_key VARCHAR(500) NOT NULL COMMENT 'public key, Base64/PEM',
p10_data VARCHAR(2000) NULL COMMENT 'PKCS#10 PEM content',
subject_dn VARCHAR(500) NULL COMMENT 'DN used when generating P10',
@@ -54,7 +54,7 @@ CREATE TABLE IF NOT EXISTS t_certificate (
CREATE TABLE IF NOT EXISTS t_trusted_cert (
id BIGINT PRIMARY KEY,
alias VARCHAR(128) NOT NULL,
alias VARCHAR(128) NULL,
subject_dn VARCHAR(1024) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
@@ -81,7 +81,7 @@ CREATE TABLE IF NOT EXISTS t_certificate_crl (
crl_number VARCHAR(128) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
crl_data MEDIUMTEXT NOT NULL COMMENT 'CRL PEM content',
crl_data MEDIUMTEXT NOT NULL COMMENT 'reserved legacy CRL PEM content; large CRLs are not stored inline',
this_update DATETIME(3) NOT NULL,
next_update DATETIME(3) NULL,
revoked_count INT NOT NULL,
@@ -112,3 +112,22 @@ CREATE TABLE IF NOT EXISTS t_certificate_crl_revoked (
KEY idx_t_certificate_crl_revoked_subject_dn (subject_dn(512)),
KEY idx_t_certificate_crl_revoked_revocation_time (revocation_time)
);
CREATE TABLE IF NOT EXISTS t_certificate_crl_import_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
original_filename VARCHAR(255) NULL,
file_size BIGINT NULL,
status VARCHAR(32) NOT NULL COMMENT 'PENDING/RUNNING/SUCCESS/FAILED',
fingerprint VARCHAR(128) NULL COMMENT 'SHA-256 fingerprint after successful parse',
crl_id BIGINT NULL,
revoked_count INT NOT NULL DEFAULT 0,
error_message VARCHAR(512) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_t_certificate_crl_import_task_task_id (task_id),
KEY idx_t_certificate_crl_import_task_status (status),
KEY idx_t_certificate_crl_import_task_create_time (create_time)
);
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cisd.tms.modules.cert.mapper.CertificateCrlImportTaskMapper">
<resultMap id="CertificateCrlImportTaskResultMap" type="com.cisd.tms.modules.cert.entity.CertificateCrlImportTaskEntity">
<id property="id" column="id"/>
<result property="taskId" column="task_id"/>
<result property="originalFilename" column="original_filename"/>
<result property="fileSize" column="file_size"/>
<result property="status" column="status"/>
<result property="fingerprint" column="fingerprint"/>
<result property="crlId" column="crl_id"/>
<result property="revokedCount" column="revoked_count"/>
<result property="errorMessage" column="error_message"/>
<result property="startedAt" column="started_at"/>
<result property="finishedAt" column="finished_at"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="CertificateCrlImportTaskColumns">
id, task_id, original_filename, file_size, status, fingerprint, crl_id, revoked_count,
error_message, started_at, finished_at, create_time, update_time
</sql>
<select id="selectByTaskId" parameterType="string" resultMap="CertificateCrlImportTaskResultMap">
SELECT <include refid="CertificateCrlImportTaskColumns"/>
FROM t_certificate_crl_import_task
WHERE task_id = #{taskId}
LIMIT 1
</select>
<update id="updateRunning">
UPDATE t_certificate_crl_import_task
SET status = 'RUNNING',
error_message = NULL,
started_at = #{startedAt},
update_time = NOW(3)
WHERE task_id = #{taskId}
</update>
<update id="updateSuccess">
UPDATE t_certificate_crl_import_task
SET status = 'SUCCESS',
crl_id = #{crlId},
fingerprint = #{fingerprint},
revoked_count = #{revokedCount},
error_message = NULL,
finished_at = #{finishedAt},
update_time = NOW(3)
WHERE task_id = #{taskId}
</update>
<update id="updateFailed">
UPDATE t_certificate_crl_import_task
SET status = 'FAILED',
error_message = #{errorMessage},
finished_at = #{finishedAt},
update_time = NOW(3)
WHERE task_id = #{taskId}
</update>
</mapper>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cisd.tms.modules.cert.mapper.CertificateCrlRevokedEntityMapper">
<insert id="insertBatch">
INSERT INTO t_certificate_crl_revoked (
id, crl_id, issuer_dn, serial_number, subject_dn, algo_type, crl_number,
revocation_time, revocation_reason, create_time, update_time
)
VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.id}, #{item.crlId}, #{item.issuerDn}, #{item.serialNumber},
#{item.subjectDn}, #{item.algoType}, #{item.crlNumber},
#{item.revocationTime}, #{item.revocationReason},
#{item.createTime}, #{item.updateTime}
)
</foreach>
</insert>
</mapper>
@@ -0,0 +1,67 @@
package com.cisd.tms.common;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ChineseErrorMessageContractTest {
private static final Path MAIN_SOURCE = Path.of("src/main/java/com/cisd/tms");
private static final Pattern CHINESE = Pattern.compile("\\p{IsHan}");
private static final Pattern USER_FACING_MESSAGE = Pattern.compile(
"(?:new\\s+(?:BizException|IllegalArgumentException|IllegalStateException|RuntimeException|ReplayProtectionException)\\(|"
+ "ApiResponse\\.fail\\(|"
+ "write(?:Unauthorized|Forbidden|SessionInvalid|Json)\\(|"
+ "InitStepExecutionResult\\.(?:成功|failure)\\(|"
+ "PcieCryptoException\\([^\\n]*,\\s*)"
+ "\"([^\"]*[A-Za-z][^\"]*)\""
);
private static final Pattern VALIDATION_MESSAGE = Pattern.compile(
"@(?:NotNull|NotBlank|NotEmpty|Size|Pattern|Min|Max|Positive|AssertTrue)\\([^\\n]*message\\s*=\\s*\"([^\"]+)\""
);
@Test
void productionErrorMessagesShouldBeChinese() throws IOException {
List<String> violations = new ArrayList<>();
try (var files = Files.walk(MAIN_SOURCE)) {
files.filter(path -> path.toString().endsWith(".java"))
.forEach(path -> collectViolations(path, violations));
}
assertTrue(violations.isEmpty(), String.join("\n", violations));
}
private static void collectViolations(Path path, List<String> violations) {
try {
List<String> lines = Files.readAllLines(path);
for (int index = 0; index < lines.size(); index++) {
String line = lines.get(index);
if (line.trim().startsWith("//") || line.contains("log.")) {
continue;
}
collect(line, USER_FACING_MESSAGE, path, index, violations);
collect(line, VALIDATION_MESSAGE, path, index, violations);
}
} catch (IOException ex) {
throw new IllegalStateException("读取生产源码失败", ex);
}
}
private static void collect(String line, Pattern pattern, Path path, int index, List<String> violations) {
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
String message = matcher.group(1);
if (!CHINESE.matcher(message).find()) {
violations.add(path + ":" + (index + 1) + " -> " + message);
}
}
}
}
@@ -26,7 +26,7 @@ class GlobalExceptionHandlerTest {
Assertions.assertNotNull(response.getBody());
Assertions.assertFalse(response.getBody().isSuccess());
Assertions.assertEquals(ErrorCode.VALIDATE_FAILED.getCode(), response.getBody().getCode());
Assertions.assertEquals("upload file size exceeds limit", response.getBody().getMsg());
Assertions.assertEquals("上传文件大小超过限制", response.getBody().getMsg());
Assertions.assertEquals("/api/v1/upgrade-packages", response.getBody().getPath());
}
@@ -18,8 +18,8 @@ class MultipartUploadConfigurationTest {
Object maxFileSize = findProperty(propertySources, "spring.servlet.multipart.max-file-size");
Object maxRequestSize = findProperty(propertySources, "spring.servlet.multipart.max-request-size");
Assertions.assertEquals("${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600}", maxFileSize);
Assertions.assertEquals("${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600}", maxRequestSize);
Assertions.assertEquals("${TMS_MULTIPART_MAX_FILE_SIZE_BYTES:524288000}", maxFileSize);
Assertions.assertEquals("${TMS_MULTIPART_MAX_REQUEST_SIZE_BYTES:536870912}", maxRequestSize);
}
private Object findProperty(List<PropertySource<?>> propertySources, String name) {
@@ -27,8 +27,8 @@ class InternalAuthorizationInterceptorTest {
Assertions.assertFalse(allowed);
Assertions.assertEquals(403, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.FORBIDDEN.getCode()));
Assertions.assertTrue(response.getContentAsString().contains("role not allowed"));
Mockito.verify(operationAuditService).recordDeniedLog(Mockito.eq(request), Mockito.any(HandlerMethod.class), Mockito.eq("role not allowed"));
Assertions.assertTrue(response.getContentAsString().contains("角色无权限访问"));
Mockito.verify(operationAuditService).recordDeniedLog(Mockito.eq(request), Mockito.any(HandlerMethod.class), Mockito.eq("角色无权限访问"));
}
@Test
@@ -45,8 +45,8 @@ class InternalAuthorizationInterceptorTest {
Assertions.assertFalse(allowed);
Assertions.assertEquals(403, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.FORBIDDEN.getCode()));
Assertions.assertTrue(response.getContentAsString().contains("auth level not allowed"));
Mockito.verify(operationAuditService).recordDeniedLog(Mockito.eq(request), Mockito.any(HandlerMethod.class), Mockito.eq("auth level not allowed"));
Assertions.assertTrue(response.getContentAsString().contains("认证级别无权限访问"));
Mockito.verify(operationAuditService).recordDeniedLog(Mockito.eq(request), Mockito.any(HandlerMethod.class), Mockito.eq("认证级别无权限访问"));
}
@Test
@@ -219,7 +219,7 @@ class AuthServiceTest {
.changeAccountPassword("token-expired-001", 1, "12345678", "Abc1234!"));
Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode());
Assertions.assertEquals("session expired", exception.getMessage());
Assertions.assertEquals("会话已过期", exception.getMessage());
}
private static AuthService newAuthService(
@@ -69,7 +69,7 @@ class PcieCompatUkeyVerifierTest {
() -> verifier().verifyLoginSignature(pubKey, "LOGIN-DATA", rawZeroSignatureHex)
);
Assertions.assertEquals("login signature verification failed", exception.getMessage());
Assertions.assertEquals("login signature verification 执行失败", exception.getMessage());
}
@Test
@@ -83,7 +83,7 @@ class PcieCompatUkeyVerifierTest {
() -> verifier().verifyLoginSignature(pubKey, payload, signature)
);
Assertions.assertEquals("login signature verification failed", exception.getMessage());
Assertions.assertEquals("login signature verification 执行失败", exception.getMessage());
}
@Test
@@ -95,7 +95,7 @@ class PcieCompatUkeyVerifierTest {
() -> verifier().verifyLoginSignature(pubKey, "LOGIN-DATA", null)
);
Assertions.assertEquals("login signature is required", exception.getMessage());
Assertions.assertEquals("login signature不能为空", exception.getMessage());
}
@Test
@@ -110,7 +110,7 @@ class PcieCompatUkeyVerifierTest {
() -> verifier().verifyLoginSignature(base64PublicKey, payload, signatureBase64)
);
Assertions.assertEquals("login signature verification failed", exception.getMessage());
Assertions.assertEquals("login signature verification 执行失败", exception.getMessage());
}
private PcieCompatUkeyVerifier verifier() {
@@ -41,6 +41,6 @@ class MqReplayContextBuilderTest {
IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> builder.build(task));
Assertions.assertTrue(ex.getMessage().contains("initPlanJson is empty"));
Assertions.assertTrue(ex.getMessage().contains("initPlanJson为空"));
}
}
@@ -78,7 +78,7 @@ class ResourcePayloadExtractorTest {
() -> extractor.extract(tempDir.resolve("backup.tmsbak"), tempDir.resolve("restore"))
);
Assertions.assertTrue(ex.getMessage().contains("escapes payload dir"));
Assertions.assertTrue(ex.getMessage().contains("越过payload目录"));
}
@Test
@@ -106,7 +106,7 @@ class ResourcePayloadExtractorTest {
() -> extractor.extract(tempDir.resolve("backup.tmsbak"), tempDir.resolve("restore"))
);
Assertions.assertTrue(ex.getMessage().contains("restore path is not allowed"));
Assertions.assertTrue(ex.getMessage().contains("恢复路径不允许"));
}
private static byte[] payloadArchive(String manifestJson, String extraEntry, String extraContent) throws Exception {

Some files were not shown because too many files have changed in this diff Show More