fix:app映射

This commit is contained in:
waner 2026-04-08 15:32:56 +08:00
parent 2f81beb0d8
commit 3d28f84115
15 changed files with 70 additions and 25 deletions

View File

@ -83,9 +83,9 @@ public class DeviceProfileServiceImpl implements DeviceProfileService {
if ("ENTERPRISE".equals(productType) || "INDIRECT".equals(productType)) {
addSoftwareItem(items, "TIDB", "数据库", cached("TIDB"));
addSoftwareItem(items, "NGINX", "Nginx", cached("NGINX"));
addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion()));
addSoftwareItem(items, "RECEIVER", appName(productType), firstNonBlank(cached("RECEIVER"), cisdPresetProperties.getVersion()));
} else if ("DIRECT".equals(productType)) {
addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion()));
addSoftwareItem(items, "RECEIVER", appName(productType), firstNonBlank(cached("RECEIVER"), cisdPresetProperties.getVersion()));
}
return items;
}

View File

@ -114,17 +114,17 @@ public class DeviceRuntimeStatusServiceImpl implements DeviceRuntimeStatusServic
items.add(buildSoftwareItem("TLQ", "消息队列", cached("TLQ"), initialized && isTlqEnabled(mqType), softwareRuntimeProbe::isTlqRunning));
items.add(buildSoftwareItem("TIDB", "数据库", cached("TIDB"), initialized && isTidbEnabled(productType), softwareRuntimeProbe::isTidbRunning));
items.add(buildSoftwareItem("NGINX", "Nginx", cached("NGINX"), initialized && isNginxEnabled(productType), softwareRuntimeProbe::isNginxRunning));
items.add(buildAppItem(productType, initialized));
items.add(buildReceiverItem(productType, initialized));
items.add(buildSoftwareItem("TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion()), true, softwareRuntimeProbe::isTmsRunning));
return items;
}
private SoftwareRuntimeStatusItem buildAppItem(String productType, boolean initialized) {
private SoftwareRuntimeStatusItem buildReceiverItem(String productType, boolean initialized) {
boolean standard = "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType);
BooleanSupplier supplier = standard ? softwareRuntimeProbe::isStandardAppRunning : softwareRuntimeProbe::isDirectAppRunning;
String name = "应用软件";
String displayName = firstNonBlank(cached("APP"), appName(productType));
return buildSoftwareItem("APP", name, displayName, initialized, supplier);
String name = "标准收发器";
String displayName = firstNonBlank(cached("RECEIVER"), appName(productType));
return buildSoftwareItem("RECEIVER", name, displayName, initialized, supplier);
}
private SoftwareRuntimeStatusItem buildSoftwareItem(

View File

@ -1,5 +1,8 @@
package com.cisd.tms.modules.upgrade.enums;
/**
* 升级任务类型具体应用名称由升级包脚本处理后端不按应用名拆分
*/
public enum UpgradeTargetComponent {
TMS,
RECEIVER,

View File

@ -1,5 +1,8 @@
package com.cisd.tms.modules.upgrade.enums;
/**
* 升级任务主状态脚本级细分状态当前通过日志体现尚未独立落表
*/
public enum UpgradeTaskStatus {
PENDING_CONFIRM,
RUNNING,

View File

@ -58,6 +58,7 @@ public class UpgradeTaskRunner {
UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class);
Path root = stagedDir.normalize().toAbsolutePath();
// 包内脚本按固定阶段执行可选阶段未声明时跳过execute 阶段必须存在
int precheckCode = runOptionalScript("precheck", root, manifest.getEntrypoints() == null ? "" : manifest.getEntrypoints().getPrecheck(), task, logPath);
if (precheckCode != 0) {
upgradeTaskRepository.updateStatus(
@ -97,6 +98,7 @@ public class UpgradeTaskRunner {
return;
}
// 只有 execute verify 都成功后才认为升级成功并回写设备软件版本表
DeviceSoftwareVersionEntity versionEntity = new DeviceSoftwareVersionEntity();
String componentCode = componentCode(task.getTaskType());
versionEntity.setComponentCode(componentCode);
@ -147,6 +149,7 @@ public class UpgradeTaskRunner {
if (rollbackPath.isEmpty()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade rollback script not found");
}
// 回滚不自动触发只有外部调用 rollback 接口后才执行包内 rollback 脚本
int exitCode = runRequiredScript("rollback", stagedDir.normalize().toAbsolutePath(), rollbackPath, task, logPath);
if (exitCode != 0) {
upgradeTaskRepository.updateStatus(
@ -198,6 +201,7 @@ public class UpgradeTaskRunner {
private int runRequiredScript(String phase, Path stagedDir, String scriptPath, UpgradeTaskEntity task, Path logPath) throws IOException, InterruptedException {
Path resolved = stagedDir.resolve(trim(scriptPath)).normalize().toAbsolutePath();
// 防止脚本路径通过 ../ 跳出升级包解压目录
if (!resolved.startsWith(stagedDir) || !Files.exists(resolved)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade " + phase + " script not found");
}
@ -212,16 +216,13 @@ public class UpgradeTaskRunner {
private String componentName(String componentCode) {
return switch (trim(componentCode)) {
case "TMS" -> "设备管理软件";
case "APP" -> "标准收发器";
case "RECEIVER" -> "标准收发器";
default -> componentCode;
};
}
private String componentCode(String taskType) {
return switch (trim(taskType)) {
case "RECEIVER" -> "APP";
default -> trim(taskType);
};
return trim(taskType);
}
private String trim(String value) {

View File

@ -51,11 +51,14 @@ public class UpgradePackageService {
public UpgradePreviewResponse preview(String fileId) {
StagedUpgradePackage stagedPackage = stagePackage(fileId);
UpgradeManifest manifest = stagedPackage.manifest();
// 预检顺序保持结构/产品/签名/脚本/版本先挡住不属于当前设备或不可信的包
validateManifest(manifest);
validateProductType(manifest.getProductType());
// 签名对象固定为 manifest.json 原始字节具体算法由 UpgradePackageSignatureVerifier 实现
signatureVerifier.verify(manifest, stagedPackage.manifestPath(), stagedPackage.signaturePath());
validateExecuteScript(stagedPackage.stagedDir(), manifest);
// 当前版本来自设备软件版本表空版本允许继续预检由前端展示未记录版本
String currentVersion = currentVersion(manifest.getTaskType());
String targetVersion = normalize(manifest.getVersion());
@ -101,6 +104,7 @@ public class UpgradePackageService {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk");
}
// 同一个 fileId 每次预检都会重新解压确保读取的是当前上传文件内容
Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath);
Path manifestPath = stagedDir.resolve("manifest.json");
Path signaturePath = stagedDir.resolve("signature.sig");
@ -142,6 +146,7 @@ public class UpgradePackageService {
private void validateExecuteScript(Path stagedDir, UpgradeManifest manifest) {
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");
}

View File

@ -43,6 +43,10 @@ public class UpgradeService {
private final UpgradeTaskRunner upgradeTaskRunner;
private final UpgradeProperties upgradeProperties;
private final Executor backgroundExecutor;
/**
* 进程内执行互斥表避免同一任务被前端重复点击后并发启动
* 跨进程/重启后的互斥仍以数据库中的 RUNNING 任务为准
*/
private final ConcurrentMap<String, CompletableFuture<Void>> activeExecutions;
@Autowired
@ -93,6 +97,7 @@ public class UpgradeService {
if (request == null || request.getFileId() == null || request.getFileId().isBlank()) {
throw new IllegalArgumentException("fileId must not be blank");
}
// 创建任务前重新预检一次避免用户上传后替换文件或设备版本状态发生变化
UpgradePreviewResponse preview = upgradePackageService.preview(request.getFileId());
if (!preview.isAllowed()) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), blankAs(preview.getBlockReason(), "upgrade preview not allowed"));
@ -131,6 +136,7 @@ public class UpgradeService {
public UpgradeTaskResponse executeTask(String taskId) {
UpgradeTaskEntity task = loadTask(taskId);
// 第一版按一体机单机串行升级处理避免同时升级 TMS收发器或固件造成状态不可控
UpgradeTaskEntity runningTask = upgradeTaskRepository.findRunningTask().orElse(null);
if (runningTask != null && !runningTask.getTaskId().equals(taskId)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "another upgrade task is running");
@ -144,6 +150,7 @@ public class UpgradeService {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
// 后台执行重新加载任务确保使用已落库的最新状态和日志路径
upgradeTaskRunner.run(loadTask(taskId));
} finally {
activeExecutions.remove(taskId);
@ -155,6 +162,7 @@ public class UpgradeService {
public UpgradeTaskResponse rollbackTask(String taskId) {
UpgradeTaskEntity task = loadTask(taskId);
// 回滚能力由升级包声明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");
@ -205,6 +213,7 @@ public class UpgradeService {
}
Path path = Path.of(logPath).normalize().toAbsolutePath();
Path allowedRoot = Path.of(trim(upgradeProperties.getLogDir())).normalize().toAbsolutePath();
// 日志路径必须落在升级日志根目录下防止通过篡改任务记录读取任意文件
if (!path.startsWith(allowedRoot)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade log path is out of allowed directory");
}

View File

@ -44,6 +44,7 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found");
}
// 第一版升级包仅对 manifest.json 做离线软验签避免在包内脚本执行前信任未授权包
PublicKey publicKey = loadPublicKey();
byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification");
byte[] signatureBytes = loadSignature(signaturePath);
@ -74,6 +75,7 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
String pemContent = Files.readString(pemPath, StandardCharsets.UTF_8);
try (Reader reader = new StringReader(pemContent); PEMParser parser = new PEMParser(reader)) {
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");
}
@ -97,12 +99,13 @@ public class SoftUpgradePackageSignatureVerifier implements UpgradePackageSignat
String text = new String(raw, StandardCharsets.UTF_8).trim();
if (!text.isEmpty()) {
try {
// 兼容联调阶段生成的 Base64 文本签名生产包可直接使用二进制签名
byte[] decoded = Base64.getDecoder().decode(text);
if (decoded.length > 0) {
return decoded;
}
} catch (IllegalArgumentException ignored) {
// Keep compatibility with raw binary signature files.
// Base64 文本时按原始二进制签名处理
}
}
return raw;

View File

@ -39,6 +39,7 @@ public class UpgradePackageStagingService {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
Path target = targetDir.resolve(entry.getName()).normalize().toAbsolutePath();
// 防止恶意 zip 条目通过 ../ 写出 staging 目录
if (!target.startsWith(targetDir)) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "invalid upgrade package entry");
}

View File

@ -25,6 +25,7 @@ public class UpgradeVersionComparator {
String value = version == null ? "" : version.trim();
List<Integer> parts = new ArrayList<>();
StringBuilder current = new StringBuilder();
// 只提取版本号中的数字段兼容 V1.2.31.2.3-build4 等简单格式
for (int i = 0; i < value.length(); i++) {
char ch = value.charAt(i);
if (Character.isDigit(ch)) {

View File

@ -110,7 +110,7 @@ VALUES
(1002, 'TLQ', '消息队列', 'V8.1.11', 'MANUAL', NOW(), '出厂预置'),
(1003, 'TIDB', '数据库', 'V8.1.0', 'MANUAL', NOW(), '出厂预置'),
(1004, 'NGINX', 'Nginx', 'V1.20', 'MANUAL', NOW(), '出厂预置'),
(1005, 'APP', '标准收发器企业版', 'V1.3.2', 'MANUAL', NOW(), '出厂预置'),
(1005, 'RECEIVER', '标准收发器', 'V1.3.2', 'MANUAL', NOW(), '出厂预置'),
(1006, 'TMS', 'CISD终端管理服务', 'V1.0.0', 'MANUAL', NOW(), '出厂预置')
ON DUPLICATE KEY UPDATE
component_name = VALUES(component_name),

View File

@ -0,0 +1,19 @@
-- 组件版本表统一使用 RECEIVER 表示标准收发器,废弃旧编码 APP。
-- 若环境中已存在 RECEIVER 行,则保留 RECEIVER 并删除旧 APP避免唯一键冲突。
DELETE FROM tms_device_software_version
WHERE component_code = 'APP'
AND EXISTS (
SELECT 1
FROM (SELECT id FROM tms_device_software_version WHERE component_code = 'RECEIVER') receiver_row
);
UPDATE tms_device_software_version
SET component_code = 'RECEIVER',
component_name = '标准收发器',
update_time = CURRENT_TIMESTAMP
WHERE component_code = 'APP';
UPDATE tms_device_software_version
SET component_name = '标准收发器',
update_time = CURRENT_TIMESTAMP
WHERE component_code = 'RECEIVER';

View File

@ -47,7 +47,7 @@ class DeviceProfileServiceTest {
versionRepository.save(component("TLQ", "消息队列", "tonglink/Q V8.1.11"));
versionRepository.save(component("TIDB", "数据库", "TiDB V8.1.0"));
versionRepository.save(component("NGINX", "Nginx", "Nginx V1.20"));
versionRepository.save(component("APP", "标准收发器间参版", "标准收发器间参版 V1.3.2"));
versionRepository.save(component("RECEIVER", "标准收发器间参版", "标准收发器间参版 V1.3.2"));
DeviceProfileService service = new DeviceProfileServiceImpl(
initService,
@ -73,7 +73,7 @@ class DeviceProfileServiceTest {
Assertions.assertEquals("32GB", response.getHardware().getMemoryTotal());
Assertions.assertEquals("953.87GB", response.getHardware().getDiskTotal());
Assertions.assertEquals(
List.of("OS", "TMS", "RABBITMQ", "TLQ", "TIDB", "NGINX", "APP"),
List.of("OS", "TMS", "RABBITMQ", "TLQ", "TIDB", "NGINX", "RECEIVER"),
response.getSoftware().stream().map(DeviceProfileSoftwareItem::getComponentCode).toList()
);
}
@ -156,7 +156,7 @@ class DeviceProfileServiceTest {
DeviceProfileResponse response = service.profile();
Assertions.assertEquals(
List.of("OS", "TMS", "RABBITMQ", "TIDB", "NGINX", "APP"),
List.of("OS", "TMS", "RABBITMQ", "TIDB", "NGINX", "RECEIVER"),
response.getSoftware().stream().map(DeviceProfileSoftwareItem::getComponentCode).toList()
);
DeviceProfileSoftwareItem rabbitmq = response.getSoftware().stream()

View File

@ -48,7 +48,7 @@ class DeviceRuntimeStatusServiceTest {
versions.save(component("TLQ", "消息队列", "tonglink/Q V8.1.11"));
versions.save(component("TIDB", "数据库", "TiDB V8.1.0"));
versions.save(component("NGINX", "Nginx", "Nginx V1.20"));
versions.save(component("APP", "标准收发器企业版", "标准收发器企业版 V1.3.2"));
versions.save(component("RECEIVER", "标准收发器企业版", "标准收发器企业版 V1.3.2"));
DeviceRuntimeStatusService service = new DeviceRuntimeStatusServiceImpl(
initService,
@ -78,7 +78,7 @@ class DeviceRuntimeStatusServiceTest {
assertSoftwareStatus(response.getSoftware(), "TLQ", "NOT_INSTALLED");
assertSoftwareStatus(response.getSoftware(), "TIDB", "NORMAL");
assertSoftwareStatus(response.getSoftware(), "NGINX", "ABNORMAL");
assertSoftwareStatus(response.getSoftware(), "APP", "NORMAL");
assertSoftwareStatus(response.getSoftware(), "RECEIVER", "NORMAL");
assertSoftwareStatus(response.getSoftware(), "TMS", "NORMAL");
}
@ -100,7 +100,7 @@ class DeviceRuntimeStatusServiceTest {
assertSoftwareStatus(response.getSoftware(), "TLQ", "NOT_INSTALLED");
assertSoftwareStatus(response.getSoftware(), "TIDB", "NOT_INSTALLED");
assertSoftwareStatus(response.getSoftware(), "NGINX", "NOT_INSTALLED");
assertSoftwareStatus(response.getSoftware(), "APP", "NOT_INSTALLED");
assertSoftwareStatus(response.getSoftware(), "RECEIVER", "NOT_INSTALLED");
assertSoftwareStatus(response.getSoftware(), "TMS", "NORMAL");
}
@ -130,7 +130,7 @@ class DeviceRuntimeStatusServiceTest {
assertSoftwareStatus(response.getSoftware(), "TLQ", "UNKNOWN");
assertSoftwareStatus(response.getSoftware(), "TIDB", "UNKNOWN");
assertSoftwareStatus(response.getSoftware(), "NGINX", "UNKNOWN");
assertSoftwareStatus(response.getSoftware(), "APP", "UNKNOWN");
assertSoftwareStatus(response.getSoftware(), "RECEIVER", "UNKNOWN");
assertSoftwareStatus(response.getSoftware(), "TMS", "UNKNOWN");
}

View File

@ -57,7 +57,7 @@ class UpgradeTaskRunnerTest {
}
@Test
void shouldUpdateAppSoftwareVersionWhenReceiverUpgradeSucceeds() throws Exception {
void shouldUpdateReceiverSoftwareVersionWhenReceiverUpgradeSucceeds() throws Exception {
Path tempDir = Files.createTempDirectory("upgrade-runner-test");
FileRecordEntity fileRecord = buildPackage(tempDir, false, "#!/bin/sh\necho execute\nexit 0\n", false);
InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository();
@ -77,9 +77,9 @@ class UpgradeTaskRunnerTest {
runner.run(task("UPG-001-R", fileRecord.getFileId(), "RECEIVER", "V1.4.0"));
Assertions.assertEquals("SUCCESS", taskRepository.status);
Assertions.assertNull(versionRepository.store.get("RECEIVER"));
Assertions.assertEquals("V1.4.0", versionRepository.store.get("APP").getCurrentVersion());
Assertions.assertEquals("标准收发器", versionRepository.store.get("APP").getComponentName());
Assertions.assertNull(versionRepository.store.get("APP"));
Assertions.assertEquals("V1.4.0", versionRepository.store.get("RECEIVER").getCurrentVersion());
Assertions.assertEquals("标准收发器", versionRepository.store.get("RECEIVER").getComponentName());
}
@Test