diff --git a/sec-api/src/main/java/com/imdroid/secapi/client/SchedulerClient.java b/sec-api/src/main/java/com/imdroid/secapi/client/SchedulerClient.java new file mode 100644 index 00000000..6c017921 --- /dev/null +++ b/sec-api/src/main/java/com/imdroid/secapi/client/SchedulerClient.java @@ -0,0 +1,26 @@ +package com.imdroid.secapi.client; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "SchedulerClient", url = "http://localhost:9904/gnss") +public interface SchedulerClient { + + @GetMapping("/rtk/scheduler/groups") + HttpResp groups(); + + @PostMapping("/rtk/scheduler/group/start") + HttpResp start(@RequestParam("groupId") Long groupId); + + @PostMapping("/rtk/scheduler/group/stop") + HttpResp stop(@RequestParam("groupId") Long groupId); + + @GetMapping("/rtk/scheduler/group/status") + HttpResp status(@RequestParam("groupId") Long groupId); + + @GetMapping("/rtk/scheduler/group/out/snapshot") + HttpResp out(@RequestParam("groupId") Long groupId, @RequestParam(value = "limit", required = false) Integer limit); +} + diff --git a/sec-api/src/main/java/com/imdroid/secapi/dto/RtkrcvGroup.java b/sec-api/src/main/java/com/imdroid/secapi/dto/RtkrcvGroup.java index 46a03a89..b13d499e 100644 --- a/sec-api/src/main/java/com/imdroid/secapi/dto/RtkrcvGroup.java +++ b/sec-api/src/main/java/com/imdroid/secapi/dto/RtkrcvGroup.java @@ -15,9 +15,6 @@ public class RtkrcvGroup { @TableId(value = "group_id", type = IdType.AUTO) private Long groupId; - @TableField("tenant_no") - private String tenantNo; - @TableField("group_name") private String groupName; diff --git a/sec-beidou-rtcm/src/main/java/com/imdroid/sideslope/rtkcluster/GroupRtkScheduler.java b/sec-beidou-rtcm/src/main/java/com/imdroid/sideslope/rtkcluster/GroupRtkScheduler.java new file mode 100644 index 00000000..be70486c --- /dev/null +++ b/sec-beidou-rtcm/src/main/java/com/imdroid/sideslope/rtkcluster/GroupRtkScheduler.java @@ -0,0 +1,187 @@ +package com.imdroid.sideslope.rtkcluster; + +import com.imdroid.secapi.dto.RtkrcvGroup; +import com.imdroid.secapi.dto.RtkrcvGroupMapper; +import com.imdroid.secapi.dto.RtkrcvProfile; +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 java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class GroupRtkScheduler { + private static final Logger LOGGER = LoggerFactory.getLogger(GroupRtkScheduler.class); + + @Autowired private RtkrcvGroupMapper groupMapper; + @Autowired private RtkrcvConfigService configService; + + @Value("${rtkrcv.bin:rtkrcv}") + private String rtkBinary; + + private static class RuntimeInfo { + volatile String state = "stopped"; // starting|running|stopped|failed + volatile Integer pid; + volatile Integer workPort; // reserved for future use + volatile String confPath; + volatile LocalDateTime startedAt; + volatile LocalDateTime lastOutAt; + volatile String message; + final Deque outRing = new ArrayDeque<>(1024); + Process process; + Thread readerThread; + synchronized void addOut(String line) { + if (outRing.size() >= 500) outRing.pollFirst(); + outRing.addLast(line); + lastOutAt = LocalDateTime.now(); + } + synchronized List snapshot(int limit) { + if (limit <= 0) limit = 200; + List list = new ArrayList<>(outRing); + int from = Math.max(0, list.size() - limit); + return list.subList(from, list.size()); + } + } + + private final Map runtimes = new ConcurrentHashMap<>(); + + public synchronized Map start(Long groupId) { + Map resp = new HashMap<>(); + try { + if (groupId == null) throw new IllegalArgumentException("groupId required"); + RtkrcvGroup group = groupMapper.selectById(groupId); + if (group == null) throw new IllegalStateException("group not found: " + groupId); + RuntimeInfo rt = runtimes.computeIfAbsent(groupId, k -> new RuntimeInfo()); + if (rt.process != null && rt.process.isAlive()) { + resp.put("code", 0); resp.put("msg", "already running"); + resp.put("pid", rt.pid); resp.put("state", rt.state); + return resp; + } + // Build a synthetic profile using group configuration + RtkrcvProfile profile = new RtkrcvProfile(); + profile.setDeviceId("group-" + groupId); + profile.setGroupId(groupId); + Path conf = configService.generateConfig(profile); + rt.confPath = conf.toString(); + ProcessBuilder pb = new ProcessBuilder(rtkBinary, "-nc", "-o", conf.toString()); + pb.directory(conf.getParent().toFile()); + pb.redirectErrorStream(true); + LOGGER.info("[scheduler] starting rtkrcv for group {} with {}", groupId, conf); + Process p = pb.start(); + rt.process = p; + rt.pid = tryGetPidCompat(p); + rt.state = "starting"; + rt.startedAt = LocalDateTime.now(); + startReader(groupId, rt); + resp.put("code", 0); resp.put("pid", rt.pid); resp.put("state", rt.state); + resp.put("confPath", rt.confPath); resp.put("startedAt", String.valueOf(rt.startedAt)); + } catch (Exception e) { + LOGGER.error("[scheduler] start group {} failed: {}", groupId, e.getMessage(), e); + Map r = new HashMap<>(); + r.put("code", 1); r.put("msg", e.getMessage()); + return r; + } + return resp; + } + + public synchronized Map stop(Long groupId) { + Map resp = new HashMap<>(); + RuntimeInfo rt = runtimes.get(groupId); + if (rt == null || rt.process == null || !rt.process.isAlive()) { + resp.put("code", 0); resp.put("msg", "already stopped"); + return resp; + } + try { + LOGGER.info("[scheduler] stopping rtkrcv for group {} pid {}", groupId, rt.pid); + rt.process.destroy(); + boolean exited = rt.process.waitFor(3, java.util.concurrent.TimeUnit.SECONDS); + if (!exited) rt.process.destroyForcibly(); + rt.state = "stopped"; + resp.put("code", 0); + } catch (Exception e) { + resp.put("code", 1); resp.put("msg", e.getMessage()); + } + return resp; + } + + public Map status(Long groupId) { + Map m = new HashMap<>(); + RuntimeInfo rt = runtimes.get(groupId); + if (rt == null) { m.put("code",0); m.put("state","stopped"); return m; } + m.put("code",0); m.put("state", rt.state); m.put("pid", rt.pid); + m.put("workPort", rt.workPort); m.put("confPath", rt.confPath); + m.put("startedAt", rt.startedAt != null ? String.valueOf(rt.startedAt) : null); + m.put("lastOutAt", rt.lastOutAt != null ? String.valueOf(rt.lastOutAt) : null); + m.put("message", rt.message); + return m; + } + + public Map out(Long groupId, Integer limit) { + Map m = new HashMap<>(); + RuntimeInfo rt = runtimes.get(groupId); + if (rt == null) { m.put("code",0); m.put("lines", Collections.emptyList()); return m; } + List lines = rt.snapshot(limit == null ? 200 : limit); + m.put("code",0); m.put("lines", lines); + return m; + } + + public List> listGroups() { + List groups = groupMapper.selectList(null); + List> rows = new ArrayList<>(); + if (groups != null) { + for (RtkrcvGroup g : groups) { + Map row = new HashMap<>(); + row.put("groupId", g.getGroupId()); + row.put("groupName", g.getGroupName()); + row.put("createdAt", g.getCreatedAt()); + row.put("updatedAt", g.getUpdatedAt()); + Map st = status(g.getGroupId()); + row.put("state", st.get("state")); + row.put("pid", st.get("pid")); + row.put("startedAt", st.get("startedAt")); + row.put("lastOutAt", st.get("lastOutAt")); + rows.add(row); + } + } + return rows; + } + + private void startReader(Long groupId, RuntimeInfo rt) { + rt.readerThread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(rt.process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + boolean first = true; + while ((line = reader.readLine()) != null) { + rt.addOut(line); + if (first) { rt.state = "running"; first = false; } + LOGGER.info("[rtk-group:{}] {}", groupId, line); + } + int code = rt.process.waitFor(); + rt.state = code == 0 ? "stopped" : "failed"; + } catch (Exception e) { + rt.state = "failed"; + rt.message = e.getMessage(); + } + }, "rtk-group-reader-" + groupId); + rt.readerThread.setDaemon(true); + rt.readerThread.start(); + } + + private Integer tryGetPidCompat(Process p) { + try { + java.lang.reflect.Field f = p.getClass().getDeclaredField("pid"); + f.setAccessible(true); + Object v = f.get(p); + if (v instanceof Integer) return (Integer) v; + } catch (Exception ignore) {} + return null; + } +} + diff --git a/sec-beidou-rtcm/src/main/java/com/imdroid/sideslope/web/SchedulerController.java b/sec-beidou-rtcm/src/main/java/com/imdroid/sideslope/web/SchedulerController.java new file mode 100644 index 00000000..5a94af5d --- /dev/null +++ b/sec-beidou-rtcm/src/main/java/com/imdroid/sideslope/web/SchedulerController.java @@ -0,0 +1,60 @@ +package com.imdroid.sideslope.web; + +import com.imdroid.secapi.client.HttpResp; +import com.imdroid.sideslope.rtkcluster.GroupRtkScheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +public class SchedulerController { + + @Autowired private GroupRtkScheduler scheduler; + + @GetMapping("/rtk/scheduler/groups") + public HttpResp groups() { + HttpResp r = new HttpResp(); + r.setResponseObject(scheduler.listGroups()); + return r; + } + + @PostMapping("/rtk/scheduler/group/start") + public HttpResp start(@RequestParam("groupId") Long groupId) { + Map m = scheduler.start(groupId); + HttpResp r = new HttpResp(); + r.setCode((int) m.getOrDefault("code", 0)); + r.setResponseObject(m); + return r; + } + + @PostMapping("/rtk/scheduler/group/stop") + public HttpResp stop(@RequestParam("groupId") Long groupId) { + Map m = scheduler.stop(groupId); + HttpResp r = new HttpResp(); + r.setCode((int) m.getOrDefault("code", 0)); + r.setResponseObject(m); + return r; + } + + @GetMapping("/rtk/scheduler/group/status") + public HttpResp status(@RequestParam("groupId") Long groupId) { + Map m = scheduler.status(groupId); + HttpResp r = new HttpResp(); + r.setCode((int) m.getOrDefault("code", 0)); + r.setResponseObject(m); + return r; + } + + @GetMapping("/rtk/scheduler/group/out/snapshot") + public HttpResp out(@RequestParam("groupId") Long groupId, + @RequestParam(value = "limit", required = false) Integer limit) { + Map m = scheduler.out(groupId, limit); + HttpResp r = new HttpResp(); + r.setCode((int) m.getOrDefault("code", 0)); + r.setResponseObject(m); + return r; + } +} + diff --git a/sec-beidou-rtcm/src/test/java/RtkrcvConfigServiceTest.java b/sec-beidou-rtcm/src/test/java/RtkrcvConfigServiceTest.java new file mode 100644 index 00000000..cb19bc6b --- /dev/null +++ b/sec-beidou-rtcm/src/test/java/RtkrcvConfigServiceTest.java @@ -0,0 +1,75 @@ +import com.imdroid.secapi.dto.RtkrcvProfile; +import com.imdroid.sideslope.rtkcluster.RtkrcvConfigService; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RtkrcvConfigServiceTest { + + private RtkrcvConfigService service; + private Path tempDir; + + @Before + public void setUp() throws IOException { + service = new RtkrcvConfigService(); + tempDir = Files.createTempDirectory("rtkrcv-conf-test-"); + } + + @After + public void tearDown() throws IOException { + if (tempDir != null) { + // best-effort cleanup + Files.walk(tempDir) + .sorted((a, b) -> b.compareTo(a)) + .forEach(p -> { + try { Files.deleteIfExists(p); } catch (IOException ignored) {} + }); + } + } + + @Test + public void testGenerateConfigReplacesKeys() throws IOException { + RtkrcvProfile profile = new RtkrcvProfile(); + profile.setDeviceId("6541883"); + profile.setInpstr1Path("beidou:29832611@8.134.185.53:8001/6541883"); + profile.setInpstr2Path("ytcors14847:fyx25943@gnss.ytcors.cn:8003/RTCM33GRCEJpro"); + profile.setInpstr3Path("Ming:@Zhang12345@ntrip.data.gnss.ga.gov.au:2101/BCEP00BKG0"); + profile.setOutHeight(0); + + Path confPath = service.generateConfig(profile, tempDir); + Assert.assertTrue(Files.exists(confPath)); + + List lines = Files.readAllLines(confPath); + String content = String.join("\n", lines); + + // Ensure template baseline content exists + Assert.assertTrue(content.contains("pos1-posmode =single")); + + // Assert replacements occurred + assertKeyValue(content, "inpstr1-path", profile.getInpstr1Path()); + assertKeyValue(content, "inpstr2-path", profile.getInpstr2Path()); + assertKeyValue(content, "inpstr3-path", profile.getInpstr3Path()); + + // out-height 0 + Pattern outHeight = Pattern.compile("^\\s*out-height\\s*=\\s*0\\b", Pattern.MULTILINE); + Matcher m = outHeight.matcher(content); + Assert.assertTrue("out-height should be 0", m.find()); + } + + private void assertKeyValue(String content, String key, String expected) { + Pattern p = Pattern.compile("^\\s*" + key + "\\s*=\\s*([^#\\r\\n]*)", Pattern.MULTILINE); + Matcher m = p.matcher(content); + Assert.assertTrue("Missing key: " + key, m.find()); + String actual = m.group(1).trim(); + Assert.assertEquals("Mismatch for key " + key, expected, actual); + } +} + diff --git a/sec-beidou/src/main/java/com/imdroid/beidou/controller/RtkProfileBatchController.java b/sec-beidou/src/main/java/com/imdroid/beidou/controller/RtkProfileBatchController.java new file mode 100644 index 00000000..407a8cb0 --- /dev/null +++ b/sec-beidou/src/main/java/com/imdroid/beidou/controller/RtkProfileBatchController.java @@ -0,0 +1,409 @@ +package com.imdroid.beidou.controller; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.imdroid.secapi.dto.GnssDevice; +import com.imdroid.secapi.dto.GnssDeviceMapper; +import com.imdroid.secapi.dto.RtkrcvGroup; +import com.imdroid.secapi.dto.RtkrcvGroupMapper; +import com.imdroid.secapi.dto.RtkrcvProfile; +import com.imdroid.secapi.dto.RtkrcvProfileMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpSession; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Controller +public class RtkProfileBatchController extends BasicController { + + @Autowired + private RtkrcvGroupMapper rtkrcvGroupMapper; + @Autowired + private RtkrcvProfileMapper rtkrcvProfileMapper; + @Autowired + private GnssDeviceMapper gnssDeviceMapper; + @Autowired + private com.imdroid.secapi.dto.TenantMapper tenantMapper; + + @RequestMapping("/page/rtk_profile_batch") + public String page(Model m, HttpSession session) { + initModel(m, session); + return "/page/rtk_profile_batch"; + } + + @GetMapping("/rtk/group/all") + @ResponseBody + public JSONObject listGroups() { + List groups = rtkrcvGroupMapper.selectList(null); + JSONObject ret = new JSONObject(); + ret.put("code", 0); + ret.put("data", groups); + ret.put("count", groups == null ? 0 : groups.size()); + return ret; + } + + @GetMapping("/rtk/group/get") + @ResponseBody + public JSONObject getGroup(@RequestParam("groupId") Long groupId) { + JSONObject ret = new JSONObject(); + if (groupId == null) { ret.put("code",1); ret.put("msg","groupId required"); return ret; } + RtkrcvGroup g = rtkrcvGroupMapper.selectById(groupId); + ret.put("code", 0); + ret.put("data", g); + return ret; + } + + @GetMapping("/rtk/devices/options") + @ResponseBody + public JSONObject deviceOptions(@RequestParam(value = "deviceId", required = false) String deviceId, + @RequestParam(value = "parentId", required = false) String parentId, + @RequestParam(value = "projectId", required = false) String projectId, + @RequestParam(value = "tenantName", required = false) String tenantName, + @RequestParam(value = "opmode", required = false) Short opmode) { + QueryWrapper qw = new QueryWrapper<>(); + if (StringUtils.hasText(deviceId)) qw.like("deviceid", deviceId.trim()); + if (StringUtils.hasText(parentId)) qw.like("parentid", parentId.trim()); + if (StringUtils.hasText(projectId)) qw.like("project_id", projectId.trim()); + if (StringUtils.hasText(tenantName)) qw.like("tenantname", tenantName.trim()); + if (opmode != null) qw.eq("opmode", opmode); + qw.orderByAsc("deviceid"); + qw.last("limit 1000"); + List list = gnssDeviceMapper.selectList(qw); + JSONObject ret = new JSONObject(); + ret.put("code", 0); + ret.put("count", list == null ? 0 : list.size()); + // layui transfer 需要 value/title + ret.put("data", list.stream().map(d -> { + Map m = new HashMap<>(); + m.put("value", d.getDeviceid()); + String title = d.getDeviceid(); + if (d.getName() != null) title += "(" + d.getName() + ")"; + if (d.getTenantname() != null) title += "-" + d.getTenantname(); + m.put("title", title); + return m; + }).toArray()); + return ret; + } + + @GetMapping("/rtk/devices/for_transfer") + @ResponseBody + public JSONObject devicesForTransfer(@RequestParam(value = "groupId", required = false) Long groupId, + @RequestParam(value = "deviceId", required = false) String deviceId, + @RequestParam(value = "parentId", required = false) String parentId, + @RequestParam(value = "projectId", required = false) String projectId, + @RequestParam(value = "tenantName", required = false) String tenantName, + @RequestParam(value = "opmode", required = false) Short opmode) { + QueryWrapper qw = new QueryWrapper<>(); + if (StringUtils.hasText(deviceId)) qw.like("deviceid", deviceId.trim()); + if (StringUtils.hasText(parentId)) qw.like("parentid", parentId.trim()); + if (StringUtils.hasText(projectId)) qw.like("project_id", projectId.trim()); + if (StringUtils.hasText(tenantName)) qw.like("tenantname", tenantName.trim()); + if (opmode != null) qw.eq("opmode", opmode); + qw.orderByAsc("deviceid"); + List devices = gnssDeviceMapper.selectList(qw); + + // 当前分组的成员集合 + java.util.Set assigned = new java.util.HashSet<>(); + java.util.List groupProfiles = null; + if (groupId != null) { + QueryWrapper qp = new QueryWrapper<>(); + qp.eq("group_id", groupId); + groupProfiles = rtkrcvProfileMapper.selectList(qp); + if (groupProfiles != null) { + for (RtkrcvProfile p : groupProfiles) { + assigned.add(p.getDeviceId()); + } + } + } + + // 确保所有已分组的设备都被包含(即使不满足筛选条件) + java.util.Map deviceMap = new java.util.LinkedHashMap<>(); + if (devices != null) { + for (GnssDevice d : devices) deviceMap.put(d.getDeviceid(), d); + } + if (assigned != null && !assigned.isEmpty()) { + if (deviceMap.isEmpty()) deviceMap = new java.util.LinkedHashMap<>(); + QueryWrapper qAssigned = new QueryWrapper<>(); + qAssigned.in("deviceid", assigned); + List assignedDevices = gnssDeviceMapper.selectList(qAssigned); + if (assignedDevices != null) { + for (GnssDevice d : assignedDevices) { + deviceMap.put(d.getDeviceid(), d); + } + } + } + JSONObject ret = new JSONObject(); + ret.put("code", 0); + java.util.List> data = new java.util.ArrayList<>(); + if (!deviceMap.isEmpty()) { + for (GnssDevice d : deviceMap.values()) { + Map m = new HashMap<>(); + m.put("value", d.getDeviceid()); + String title = d.getDeviceid(); + if (d.getName() != null) title += "(" + d.getName() + ")"; + if (d.getTenantname() != null) title += "-" + d.getTenantname(); + m.put("title", title); + if (assigned.contains(d.getDeviceid())) m.put("checked", true); + data.add(m); + } + } + ret.put("data", data); + ret.put("count", data.size()); + return ret; + } + + @PostMapping("/rtk/group/create_with_devices") + @ResponseBody + public Map createGroupWithDevices(@RequestParam("groupName") String groupName, + @RequestParam("deviceIds") String deviceIdsCsv, + @RequestParam(value = "inpstr2_type", required = false) String inpstr2Type, + @RequestParam(value = "inpstr2_path", required = false) String inpstr2Path, + @RequestParam(value = "inpstr2_format", required = false) String inpstr2Format, + @RequestParam(value = "inpstr3_type", required = false) String inpstr3Type, + @RequestParam(value = "inpstr3_path", required = false) String inpstr3Path, + @RequestParam(value = "inpstr3_format", required = false) String inpstr3Format, + @RequestParam(value = "outstr1_format", required = false) String outstr1Format, + @RequestParam(value = "outstr1_path", required = false) String outstr1Path, + @RequestParam(value = "outstr1_type", required = false) String outstr1Type, + @RequestParam(value = "outstr2_type", required = false) String outstr2Type, + @RequestParam(value = "outstr2_format", required = false) String outstr2Format, + @RequestParam(value = "outstr2_path", required = false) String outstr2Path) { + Map res = new HashMap<>(); + if (!StringUtils.hasText(groupName)) { + res.put("code", 1); + res.put("msg", "groupName is required"); + return res; + } + // create group + RtkrcvGroup g = new RtkrcvGroup(); + g.setGroupName(groupName.trim()); + g.setCreatedAt(LocalDateTime.now()); + g.setUpdatedAt(LocalDateTime.now()); + // apply defaults if blank + if (!StringUtils.hasText(inpstr2Type)) inpstr2Type = "ntripcli"; + if (!StringUtils.hasText(inpstr2Path)) inpstr2Path = "ytcors38369:fyx85534@gnss.ytcors.cn:8003/RTCM33GRCEJpro"; + if (!StringUtils.hasText(inpstr2Format)) inpstr2Format = "rtcm3"; + if (!StringUtils.hasText(inpstr3Type)) inpstr3Type = "ntripcli"; + if (!StringUtils.hasText(inpstr3Path)) inpstr3Path = "Ming:@Zhang12345@ntrip.data.gnss.ga.gov.au:2101/BCEP00BKG0"; + if (!StringUtils.hasText(inpstr3Format)) inpstr3Format = "rtcm3"; + if (!StringUtils.hasText(outstr1Format)) outstr1Format = "llh"; + if (!StringUtils.hasText(outstr1Type)) outstr1Type = "off"; + if (!StringUtils.hasText(outstr2Type)) outstr2Type = "tcpcli"; + if (!StringUtils.hasText(outstr2Format)) outstr2Format = "llh"; + + g.setInpstr2Type(inpstr2Type); + g.setInpstr2Path(inpstr2Path); + g.setInpstr2Format(inpstr2Format); + g.setInpstr3Type(inpstr3Type); + g.setInpstr3Path(inpstr3Path); + g.setInpstr3Format(inpstr3Format); + g.setOutstr1Format(outstr1Format); + g.setOutstr1Path(outstr1Path); + g.setOutstr1Type(outstr1Type); + g.setOutstr2Type(outstr2Type); + g.setOutstr2Format(outstr2Format); + g.setOutstr2Path(outstr2Path); + rtkrcvGroupMapper.insert(g); + + int bound = 0; + if (StringUtils.hasText(deviceIdsCsv)) { + String[] ids = deviceIdsCsv.split(","); + for (String id : ids) { + String devId = id.trim(); + if (devId.isEmpty()) continue; + RtkrcvProfile existing = rtkrcvProfileMapper.selectById(devId); + if (existing == null) { + RtkrcvProfile p = new RtkrcvProfile(); + p.setDeviceId(devId); + p.setGroupId(g.getGroupId()); + p.setUpdatedAt(LocalDateTime.now()); + rtkrcvProfileMapper.insert(p); + } else { + RtkrcvProfile upd = new RtkrcvProfile(); + upd.setDeviceId(devId); + upd.setGroupId(g.getGroupId()); + upd.setUpdatedAt(LocalDateTime.now()); + rtkrcvProfileMapper.updateById(upd); + } + bound++; + } + } + res.put("code", 0); + res.put("groupId", g.getGroupId()); + res.put("boundDevices", bound); + return res; + } + + @PostMapping("/rtk/group/update") + @ResponseBody + public Map updateGroup(@RequestParam("groupId") Long groupId, + @RequestParam(value = "groupName", required = false) String groupName, + @RequestParam(value = "inpstr2_type", required = false) String inpstr2Type, + @RequestParam(value = "inpstr2_path", required = false) String inpstr2Path, + @RequestParam(value = "inpstr2_format", required = false) String inpstr2Format, + @RequestParam(value = "inpstr3_type", required = false) String inpstr3Type, + @RequestParam(value = "inpstr3_path", required = false) String inpstr3Path, + @RequestParam(value = "inpstr3_format", required = false) String inpstr3Format, + @RequestParam(value = "outstr1_format", required = false) String outstr1Format, + @RequestParam(value = "outstr1_path", required = false) String outstr1Path, + @RequestParam(value = "outstr1_type", required = false) String outstr1Type, + @RequestParam(value = "outstr2_type", required = false) String outstr2Type, + @RequestParam(value = "outstr2_format", required = false) String outstr2Format, + @RequestParam(value = "outstr2_path", required = false) String outstr2Path) { + Map res = new HashMap<>(); + if (groupId == null) { res.put("code",1); res.put("msg","groupId required"); return res; } + RtkrcvGroup g = new RtkrcvGroup(); + g.setGroupId(groupId); + if (StringUtils.hasText(groupName)) g.setGroupName(groupName.trim()); + g.setInpstr2Type(inpstr2Type); + g.setInpstr2Path(inpstr2Path); + g.setInpstr2Format(inpstr2Format); + g.setInpstr3Type(inpstr3Type); + g.setInpstr3Path(inpstr3Path); + g.setInpstr3Format(inpstr3Format); + g.setOutstr1Format(outstr1Format); + g.setOutstr1Path(outstr1Path); + g.setOutstr1Type(outstr1Type); + g.setOutstr2Type(outstr2Type); + g.setOutstr2Format(outstr2Format); + g.setOutstr2Path(outstr2Path); + g.setUpdatedAt(LocalDateTime.now()); + rtkrcvGroupMapper.updateById(g); + res.put("code",0); + return res; + } + + @PostMapping("/rtk/group/rebind_devices") + @ResponseBody + public Map rebindDevices(@RequestParam("groupId") Long groupId, + @RequestParam(value = "deviceIds", required = false) String deviceIdsCsv) { + Map res = new HashMap<>(); + if (groupId == null) { res.put("code",1); res.put("msg","groupId required"); return res; } + java.util.Set target = new java.util.HashSet<>(); + if (StringUtils.hasText(deviceIdsCsv)) { + for (String id : deviceIdsCsv.split(",")) { + if (StringUtils.hasText(id)) target.add(id.trim()); + } + } + // fetch existing members + QueryWrapper qw = new QueryWrapper<>(); + qw.eq("group_id", groupId); + List existingList = rtkrcvProfileMapper.selectList(qw); + java.util.Set existing = new java.util.HashSet<>(); + if (existingList != null) for (RtkrcvProfile p : existingList) existing.add(p.getDeviceId()); + + int toAdd = 0, toRemove = 0; + // add or update selected + for (String devId : target) { + if (!existing.contains(devId)) { + RtkrcvProfile p = rtkrcvProfileMapper.selectById(devId); + if (p == null) { + p = new RtkrcvProfile(); + p.setDeviceId(devId); + p.setGroupId(groupId); + p.setUpdatedAt(LocalDateTime.now()); + rtkrcvProfileMapper.insert(p); + } else { + RtkrcvProfile upd = new RtkrcvProfile(); + upd.setDeviceId(devId); + upd.setGroupId(groupId); + upd.setUpdatedAt(LocalDateTime.now()); + rtkrcvProfileMapper.updateById(upd); + } + toAdd++; + } + } + // remove not in target -> mark ungrouped (NULL) + for (String devId : existing) { + if (!target.contains(devId)) { + UpdateWrapper uw = new UpdateWrapper<>(); + uw.eq("device_id", devId).set("group_id", null).set("updated_at", LocalDateTime.now()); + rtkrcvProfileMapper.update(null, uw); + toRemove++; + } + } + res.put("code",0); + res.put("added", toAdd); + res.put("removed", toRemove); + return res; + } + + @GetMapping("/rtk/tenants/simple") + @ResponseBody + public JSONObject tenantsSimple() { + java.util.List list = tenantMapper.selectList(null); + JSONObject ret = new JSONObject(); + ret.put("code",0); + ret.put("data", list); + return ret; + } + + @RequestMapping("/page/table/rtk_add_group") + public String addGroupPage(Model m, HttpSession session) { + initModel(m, session); + return "/page/table/rtk_add_group"; + } + + @PostMapping("/rtk/profile/batch_upsert") + @ResponseBody + public Map batchUpsert(@RequestParam("groupId") Long groupId, + @RequestParam(value = "deviceIdLike", required = false) String deviceIdLike, + @RequestParam(value = "tenantId", required = false) Integer tenantId) { + Map result = new HashMap<>(); + if (groupId == null) { + result.put("code", 1); + result.put("msg", "groupId is required"); + return result; + } + + QueryWrapper qw = new QueryWrapper<>(); + if (tenantId != null) { + qw.eq("tenantid", tenantId); + } + if (StringUtils.hasText(deviceIdLike)) { + qw.like("deviceid", deviceIdLike.trim()); + } + // 仅选择使用中的设备(可按需放开) + // qw.eq("opmode", GnssDevice.OP_MODE_USE); + + List devices = gnssDeviceMapper.selectList(qw); + int created = 0, updated = 0; + if (devices != null) { + for (GnssDevice d : devices) { + if (d == null || d.getDeviceid() == null) continue; + String deviceId = d.getDeviceid(); + RtkrcvProfile existing = rtkrcvProfileMapper.selectById(deviceId); + if (existing == null) { + RtkrcvProfile p = new RtkrcvProfile(); + p.setDeviceId(deviceId); + p.setGroupId(groupId); + p.setUpdatedAt(LocalDateTime.now()); + // 允许空路径,后续可由分组/调度器决定 + rtkrcvProfileMapper.insert(p); + created++; + } else { + RtkrcvProfile upd = new RtkrcvProfile(); + upd.setDeviceId(deviceId); + upd.setGroupId(groupId); + upd.setUpdatedAt(LocalDateTime.now()); + rtkrcvProfileMapper.updateById(upd); + updated++; + } + } + } + + result.put("code", 0); + result.put("created", created); + result.put("updated", updated); + result.put("total", devices == null ? 0 : devices.size()); + return result; + } +} diff --git a/sec-beidou/src/main/java/com/imdroid/beidou/controller/RtkSchedulerProxyController.java b/sec-beidou/src/main/java/com/imdroid/beidou/controller/RtkSchedulerProxyController.java new file mode 100644 index 00000000..ec3847b5 --- /dev/null +++ b/sec-beidou/src/main/java/com/imdroid/beidou/controller/RtkSchedulerProxyController.java @@ -0,0 +1,33 @@ +package com.imdroid.beidou.controller; + +import com.imdroid.secapi.client.HttpResp; +import com.imdroid.secapi.client.SchedulerClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +@Controller +@ResponseBody +public class RtkSchedulerProxyController extends BasicController { + + @Autowired + private SchedulerClient schedulerClient; + + @GetMapping("/rtk/scheduler/groups") + public HttpResp groups() { return schedulerClient.groups(); } + + @PostMapping("/rtk/scheduler/group/start") + public HttpResp start(@RequestParam("groupId") Long groupId) { return schedulerClient.start(groupId); } + + @PostMapping("/rtk/scheduler/group/stop") + public HttpResp stop(@RequestParam("groupId") Long groupId) { return schedulerClient.stop(groupId); } + + @GetMapping("/rtk/scheduler/group/status") + public HttpResp status(@RequestParam("groupId") Long groupId) { return schedulerClient.status(groupId); } + + @GetMapping("/rtk/scheduler/group/out/snapshot") + public HttpResp out(@RequestParam("groupId") Long groupId, @RequestParam(value = "limit", required = false) Integer limit) { + return schedulerClient.out(groupId, limit); + } +} + diff --git a/sec-beidou/src/main/resources/static/api/init_super_admin.json b/sec-beidou/src/main/resources/static/api/init_super_admin.json index ddbf2f63..79c96075 100644 --- a/sec-beidou/src/main/resources/static/api/init_super_admin.json +++ b/sec-beidou/src/main/resources/static/api/init_super_admin.json @@ -59,6 +59,12 @@ "icon": "fa fa-clipboard", "target": "_self" }, + { + "title": "定位管理", + "href": "page/rtk_profile_batch", + "icon": "fa fa-clipboard", + "target": "_self" + }, { "title": "配置管理", "href": "", diff --git a/sec-beidou/src/main/resources/templates/page/rtk_profile_batch.html b/sec-beidou/src/main/resources/templates/page/rtk_profile_batch.html new file mode 100644 index 00000000..fd65a161 --- /dev/null +++ b/sec-beidou/src/main/resources/templates/page/rtk_profile_batch.html @@ -0,0 +1,240 @@ + + + + + RTK Profile 批处理 + + + + + + + +
+
+
+
    +
  • 分组管理
  • +
  • 批量生成/更新 Profile
  • +
  • 分组说明
  • +
  • RTK 调度
  • +
+
+
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + +
+
+
+ +
+
+
+ 说明:
+ - 在此页面选择分组并设置筛选条件后,可批量为匹配的设备创建/更新 rtkrcv_profile。
+ - 仅写入 profile 的设备ID与分组ID,其他字段后续由分组或调度器决定。
+ - 运行并发/调度策略将在后续页面实现,此处仅做配置准备。 +
+
+
+
+ + +
+
+
+
+
+ + + + + + diff --git a/sec-beidou/src/main/resources/templates/page/table/rtk_add_group.html b/sec-beidou/src/main/resources/templates/page/table/rtk_add_group.html new file mode 100644 index 00000000..c1f79951 --- /dev/null +++ b/sec-beidou/src/main/resources/templates/page/table/rtk_add_group.html @@ -0,0 +1,393 @@ + + + + + 新增RTK分组 + + + + + + + +
+
+
+
+ +
+ +
+
+
+ 分组输出/输入配置 +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ 选择设备(穿梭框,可搜索) +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+ +
+
+ + +
+
+
+
+ +
+ + + + +