diff --git a/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java b/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java new file mode 100644 index 0000000..6f48707 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/controller/TimeConfigController.java @@ -0,0 +1,33 @@ +package com.cisd.tms.modules.device.controller; + + +import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.modules.device.dto.TimeConfigRequest; +import com.cisd.tms.modules.device.service.TimeConfigService; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/device") +@Tag(name = "device-time-config-controller") +public class TimeConfigController { + + private final TimeConfigService timeConfigService; + + public TimeConfigController(TimeConfigService timeConfigService) { + this.timeConfigService = timeConfigService; + } + + + @PostMapping("/time-config") + public ApiResponse configureTime(@RequestBody TimeConfigRequest request) { + //todo 鉴权 + timeConfigService.processTimeConfig(request); + + return ApiResponse.success(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/TimeConfigRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/TimeConfigRequest.java new file mode 100644 index 0000000..52e2da0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/TimeConfigRequest.java @@ -0,0 +1,35 @@ +package com.cisd.tms.modules.device.dto; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Data +@Schema(description = "设备时间配置请求参数") +public class TimeConfigRequest { + + @Schema(description = "配置模式", + allowableValues = {"MANUAL", "NTP"}, + example = "MANUAL", + requiredMode = Schema.RequiredMode.REQUIRED) + private String mode; + + @Schema(description = "手工调整输入时间 (仅 MANUAL 模式下必填,格式: yyyy-MM-dd HH:mm)", + example = "2026-01-30 00:00") + private String datetime; + + @Schema(description = "系统时区标识", + example = "Asia/Shanghai") + private String timezone; + + @Schema(description = "NTP服务器地址列表 (仅 NTP 模式下必填)", + example = "[\"ntp.aliyun.com\", \"ntp.tencent.com\"]") + private List ntpServers; + + @Schema(description = "NTP同步间隔时间 (单位: 秒。默认 600)", + example = "600") + private Integer syncInterval; + +} diff --git a/src/main/java/com/cisd/tms/modules/device/service/TimeConfigService.java b/src/main/java/com/cisd/tms/modules/device/service/TimeConfigService.java new file mode 100644 index 0000000..3ab3170 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/service/TimeConfigService.java @@ -0,0 +1,112 @@ +package com.cisd.tms.modules.device.service; + +import com.cisd.tms.modules.device.dto.TimeConfigRequest; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +@Service +public class TimeConfigService { + + + public void processTimeConfig(TimeConfigRequest request) { + String mode = request.getMode(); + + if ("MANUAL".equalsIgnoreCase(mode)) { + + String timezone = request.getTimezone(); + if (timezone == null || !timezone.matches("^[A-Za-z0-9/_-]+$")) { + throw new IllegalArgumentException("Invalid or empty timezone format"); + } + + String datetime = request.getDatetime(); + // 假设格式必须为 YYYY-MM-DD HH:MM + if (datetime == null || !datetime.matches("^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}$")) { + throw new IllegalArgumentException("Invalid or empty datetime format. Expected: YYYY-MM-DD HH:MM"); + } + + executeCommand("timedatectl set-timezone " + request.getTimezone().trim()); + executeCommand("systemctl stop chronyd"); + executeCommand("date -s '" + request.getDatetime() + "'"); + + } else if ("NTP".equalsIgnoreCase(mode)) { + + List servers = request.getNtpServers(); + if (servers == null || servers.isEmpty()) { + throw new IllegalArgumentException("At least one NTP server IP must be provided in NTP mode"); + } + + + int interval = request.getSyncInterval() != null ? request.getSyncInterval() : 600; + int pollExp = (int) Math.round(Math.log(interval) / Math.log(2)); + StringBuilder serverConfLines = new StringBuilder(); + for (String server : servers) { + if (!server.matches("^[A-Za-z0-9.-]+$")) { + throw new IllegalArgumentException("Invalid NTP server address format: " + 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); + } + serverConfLines.append("server ").append(server) + .append(" iburst minpoll ").append(pollExp) + .append(" maxpoll ").append(pollExp).append("\\n"); + } + String confContent = serverConfLines + + "driftfile /var/lib/chrony/drift\\n" + + "makestep 1.0 3\\n" + + "rtcsync\\n"; + executeCommand("echo -e '" + confContent + "' > /etc/chrony.conf"); + executeCommand("systemctl restart chronyd"); + executeCommand("chronyc -a makestep"); + String status = executeCommand("chronyc tracking"); + if (!status.contains("Reference ID")) { + throw new RuntimeException("NTP sync status check failed"); + } + + } else { + throw new IllegalArgumentException("Unknown mode: " + mode); + } + + } + + + private String executeCommand(String command){ + ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command); + processBuilder.redirectErrorStream(true); + + Process process = null; + try { + process = processBuilder.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new RuntimeException("command execution failed: [" + command + "] "); + } + return output.toString(); + + } catch (IOException e){ + throw new RuntimeException("I/O error executing command: [" + command + "]", e); + } catch (InterruptedException e) { + if (process != null) { + process.destroy(); + } + Thread.currentThread().interrupt(); + throw new RuntimeException("process was interrupted while executing command: [" + command + "]", e); + } + } + +}