feat: 新增RTK调度及前端页面
This commit is contained in:
parent
131b0da71d
commit
2cb32e8923
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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<String> 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<String> snapshot(int limit) {
|
||||
if (limit <= 0) limit = 200;
|
||||
List<String> list = new ArrayList<>(outRing);
|
||||
int from = Math.max(0, list.size() - limit);
|
||||
return list.subList(from, list.size());
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Long, RuntimeInfo> runtimes = new ConcurrentHashMap<>();
|
||||
|
||||
public synchronized Map<String,Object> start(Long groupId) {
|
||||
Map<String,Object> 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<String,Object> r = new HashMap<>();
|
||||
r.put("code", 1); r.put("msg", e.getMessage());
|
||||
return r;
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
public synchronized Map<String,Object> stop(Long groupId) {
|
||||
Map<String,Object> 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<String,Object> status(Long groupId) {
|
||||
Map<String,Object> 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<String,Object> out(Long groupId, Integer limit) {
|
||||
Map<String,Object> m = new HashMap<>();
|
||||
RuntimeInfo rt = runtimes.get(groupId);
|
||||
if (rt == null) { m.put("code",0); m.put("lines", Collections.emptyList()); return m; }
|
||||
List<String> lines = rt.snapshot(limit == null ? 200 : limit);
|
||||
m.put("code",0); m.put("lines", lines);
|
||||
return m;
|
||||
}
|
||||
|
||||
public List<Map<String,Object>> listGroups() {
|
||||
List<RtkrcvGroup> groups = groupMapper.selectList(null);
|
||||
List<Map<String,Object>> rows = new ArrayList<>();
|
||||
if (groups != null) {
|
||||
for (RtkrcvGroup g : groups) {
|
||||
Map<String,Object> row = new HashMap<>();
|
||||
row.put("groupId", g.getGroupId());
|
||||
row.put("groupName", g.getGroupName());
|
||||
row.put("createdAt", g.getCreatedAt());
|
||||
row.put("updatedAt", g.getUpdatedAt());
|
||||
Map<String,Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String,Object> 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<String,Object> 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<String,Object> 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<String,Object> m = scheduler.out(groupId, limit);
|
||||
HttpResp r = new HttpResp();
|
||||
r.setCode((int) m.getOrDefault("code", 0));
|
||||
r.setResponseObject(m);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
75
sec-beidou-rtcm/src/test/java/RtkrcvConfigServiceTest.java
Normal file
75
sec-beidou-rtcm/src/test/java/RtkrcvConfigServiceTest.java
Normal file
@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<RtkrcvGroup> 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<GnssDevice> 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<GnssDevice> 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<String, Object> 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<GnssDevice> 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<GnssDevice> devices = gnssDeviceMapper.selectList(qw);
|
||||
|
||||
// 当前分组的成员集合
|
||||
java.util.Set<String> assigned = new java.util.HashSet<>();
|
||||
java.util.List<RtkrcvProfile> groupProfiles = null;
|
||||
if (groupId != null) {
|
||||
QueryWrapper<RtkrcvProfile> 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<String, GnssDevice> 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<GnssDevice> qAssigned = new QueryWrapper<>();
|
||||
qAssigned.in("deviceid", assigned);
|
||||
List<GnssDevice> 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<Map<String, Object>> data = new java.util.ArrayList<>();
|
||||
if (!deviceMap.isEmpty()) {
|
||||
for (GnssDevice d : deviceMap.values()) {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String,Object> 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<String,Object> 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<String,Object> rebindDevices(@RequestParam("groupId") Long groupId,
|
||||
@RequestParam(value = "deviceIds", required = false) String deviceIdsCsv) {
|
||||
Map<String,Object> res = new HashMap<>();
|
||||
if (groupId == null) { res.put("code",1); res.put("msg","groupId required"); return res; }
|
||||
java.util.Set<String> 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<RtkrcvProfile> qw = new QueryWrapper<>();
|
||||
qw.eq("group_id", groupId);
|
||||
List<RtkrcvProfile> existingList = rtkrcvProfileMapper.selectList(qw);
|
||||
java.util.Set<String> 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<RtkrcvProfile> 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<com.imdroid.secapi.dto.Tenant> 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<String, Object> batchUpsert(@RequestParam("groupId") Long groupId,
|
||||
@RequestParam(value = "deviceIdLike", required = false) String deviceIdLike,
|
||||
@RequestParam(value = "tenantId", required = false) Integer tenantId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if (groupId == null) {
|
||||
result.put("code", 1);
|
||||
result.put("msg", "groupId is required");
|
||||
return result;
|
||||
}
|
||||
|
||||
QueryWrapper<GnssDevice> 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<GnssDevice> 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +59,12 @@
|
||||
"icon": "fa fa-clipboard",
|
||||
"target": "_self"
|
||||
},
|
||||
{
|
||||
"title": "定位管理",
|
||||
"href": "page/rtk_profile_batch",
|
||||
"icon": "fa fa-clipboard",
|
||||
"target": "_self"
|
||||
},
|
||||
{
|
||||
"title": "配置管理",
|
||||
"href": "",
|
||||
|
||||
@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>RTK Profile 批处理</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="../lib/layui-v2.6.3/css/layui.css" media="all">
|
||||
<link rel="stylesheet" href="../css/public.css" media="all">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layuimini-container">
|
||||
<div class="layuimini-main">
|
||||
<div class="layui-tab layui-tab-brief" lay-filter="rtkTabs">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">分组管理</li>
|
||||
<li>批量生成/更新 Profile</li>
|
||||
<li>分组说明</li>
|
||||
<li>RTK 调度</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<table class="layui-hide" id="rtkGroupTable" lay-filter="rtkGroupTableFilter"></table>
|
||||
<script type="text/html" id="toolbarGroup">
|
||||
<div class="layui-btn-container" th:if="${role=='SUPER_ADMIN' || role=='ADMIN'}">
|
||||
<button class="layui-btn layui-btn-normal layui-btn-sm" lay-event="add">添加分组</button>
|
||||
</div>
|
||||
</script>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<form class="layui-form" lay-filter="batchForm" style="max-width: 720px;">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">目标分组</label>
|
||||
<div class="layui-input-block">
|
||||
<select id="groupId" name="groupId" lay-search>
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">设备筛选</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="deviceIdLike" placeholder="设备号模糊匹配,如 23 或 2301" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">租户ID</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="number" name="tenantId" placeholder="可选,限制到特定租户" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="doBatch">执行批处理</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<blockquote class="layui-elem-quote" id="batchResult" style="display:none;"></blockquote>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<blockquote class="layui-elem-quote">
|
||||
说明:<br>
|
||||
- 在此页面选择分组并设置筛选条件后,可批量为匹配的设备创建/更新 rtkrcv_profile。<br>
|
||||
- 仅写入 profile 的设备ID与分组ID,其他字段后续由分组或调度器决定。<br>
|
||||
- 运行并发/调度策略将在后续页面实现,此处仅做配置准备。
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<table class="layui-hide" id="rtkSchedulerTable" lay-filter="rtkSchedulerTableFilter"></table>
|
||||
<script type="text/html" id="toolbarScheduler">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-normal layui-btn-sm" lay-event="refresh">刷新</button>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="schedulerOpsTpl">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="start">启动</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="stop">停止</a>
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="out">输出</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
||||
<script th:inline="none">
|
||||
layui.use(['form','element','layer','table'], function(){
|
||||
var $ = layui.$, form = layui.form, layer = layui.layer, table = layui.table;
|
||||
|
||||
function loadGroups() {
|
||||
$.getJSON('/rtk/group/all', function(resp){
|
||||
if(resp && resp.data) {
|
||||
var sel = $('#groupId');
|
||||
sel.empty();
|
||||
sel.append('<option value="">请选择分组</option>');
|
||||
resp.data.forEach(function(g){
|
||||
var name = (g.groupName || ('#'+g.groupId));
|
||||
sel.append('<option value="'+ g.groupId +'">['+ g.groupId +'] ' + name + '</option>');
|
||||
});
|
||||
form.render('select');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
form.on('submit(doBatch)', function(data){
|
||||
var params = data.field;
|
||||
if(!params.groupId) { layer.msg('请选择目标分组'); return false; }
|
||||
layer.load(2);
|
||||
$.ajax({
|
||||
url: '/rtk/profile/batch_upsert',
|
||||
type: 'POST',
|
||||
data: params,
|
||||
success: function(resp){
|
||||
layer.closeAll('loading');
|
||||
if(resp && resp.code === 0){
|
||||
$('#batchResult').show().html('总计匹配设备:' + (resp.total||0) + ',新建:' + (resp.created||0) + ',更新:' + (resp.updated||0));
|
||||
} else {
|
||||
layer.msg((resp && resp.msg) || '处理失败');
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
layer.closeAll('loading');
|
||||
layer.msg('网络错误');
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
loadGroups();
|
||||
// 分组管理表
|
||||
table.render({
|
||||
elem: '#rtkGroupTable',
|
||||
url: '/rtk/group/all',
|
||||
toolbar: '#toolbarGroup',
|
||||
defaultToolbar: [],
|
||||
cols: [[
|
||||
{field:'groupId', title:'组号', sort:true, width:100},
|
||||
{field:'groupName', title:'分组名称'},
|
||||
{field:'createdAt', title:'创建时间'},
|
||||
{field:'updatedAt', title:'更新时间'},
|
||||
{title:'操作', align:'center', templet:'#groupOpsTpl', width:120}
|
||||
]],
|
||||
page: false,
|
||||
skin: 'line'
|
||||
});
|
||||
|
||||
table.on('toolbar(rtkGroupTableFilter)', function(obj){
|
||||
if (obj.event === 'add') {
|
||||
var index = layer.open({
|
||||
title: '新增分组并分配设备',
|
||||
type: 2,
|
||||
shade: 0.2,
|
||||
maxmin:true,
|
||||
shadeClose: true,
|
||||
area: ['100%', '100%'],
|
||||
content: '../page/table/rtk_add_group'
|
||||
});
|
||||
$(window).on('resize', function(){ try { (parent && parent.layer ? parent.layer : layer).full(index); } catch(e){} });
|
||||
}
|
||||
});
|
||||
|
||||
// 行内编辑
|
||||
table.on('tool(rtkGroupTableFilter)', function(obj){
|
||||
var data = obj.data;
|
||||
if (obj.event === 'edit') {
|
||||
var index = layer.open({
|
||||
title: '编辑分组配置',
|
||||
type: 2,
|
||||
shade: 0.2,
|
||||
maxmin:true,
|
||||
shadeClose: true,
|
||||
area: ['100%', '100%'],
|
||||
content: '../page/table/rtk_add_group?groupId=' + data.groupId
|
||||
});
|
||||
$(window).on('resize', function(){ try { (parent && parent.layer ? parent.layer : layer).full(index); } catch(e){} });
|
||||
}
|
||||
});
|
||||
|
||||
// RTK 调度表
|
||||
function renderScheduler(){
|
||||
table.render({
|
||||
elem: '#rtkSchedulerTable',
|
||||
url: '/rtk/scheduler/groups',
|
||||
parseData: function(res){
|
||||
// HttpResp wrapper
|
||||
return {code: 0, data: res.responseObject||[]};
|
||||
},
|
||||
toolbar: '#toolbarScheduler',
|
||||
defaultToolbar: [],
|
||||
cols: [[
|
||||
{field:'groupId', title:'组号', width:100, sort:true},
|
||||
{field:'groupName', title:'分组名称'},
|
||||
{field:'state', title:'状态'},
|
||||
{field:'pid', title:'PID', width:100},
|
||||
{field:'startedAt', title:'启动时间'},
|
||||
{field:'lastOutAt', title:'最近输出'},
|
||||
{title:'操作', align:'center', templet:'#schedulerOpsTpl', width:180}
|
||||
]],
|
||||
page: false,
|
||||
skin: 'line'
|
||||
});
|
||||
}
|
||||
|
||||
table.on('toolbar(rtkSchedulerTableFilter)', function(obj){
|
||||
if (obj.event === 'refresh') { renderScheduler(); }
|
||||
});
|
||||
|
||||
table.on('tool(rtkSchedulerTableFilter)', function(obj){
|
||||
var data = obj.data;
|
||||
if (obj.event === 'start') {
|
||||
$.post('/rtk/scheduler/group/start',{groupId:data.groupId}, function(){ renderScheduler(); });
|
||||
} else if (obj.event === 'stop') {
|
||||
$.post('/rtk/scheduler/group/stop',{groupId:data.groupId}, function(){ renderScheduler(); });
|
||||
} else if (obj.event === 'out') {
|
||||
var idx = layer.open({
|
||||
title: '组 '+data.groupId+' 输出', type: 1, area: ['800px','500px'], shadeClose:true,
|
||||
content: '<pre id="outbox" style="padding:10px;white-space:pre-wrap;max-height:440px;overflow:auto;"></pre>'
|
||||
});
|
||||
var timer = setInterval(function(){
|
||||
$.get('/rtk/scheduler/group/out/snapshot',{groupId:data.groupId,limit:200}, function(resp){
|
||||
var lines = (resp && resp.responseObject && resp.responseObject.lines) || [];
|
||||
$('#outbox').text(lines.join('\n'));
|
||||
});
|
||||
}, 2000);
|
||||
layer.full ? layer.full(idx) : null;
|
||||
layer.getChildFrame && $(window).one('beforeunload', function(){ clearInterval(timer); });
|
||||
}
|
||||
});
|
||||
|
||||
renderScheduler();
|
||||
});
|
||||
</script>
|
||||
<script type="text/html" id="groupOpsTpl">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="edit">编辑</a>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,393 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>新增RTK分组</title>
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="stylesheet" href="../../lib/layui-v2.6.3/css/layui.css" media="all">
|
||||
<link rel="stylesheet" href="../../css/public.css" media="all">
|
||||
</head>
|
||||
<body>
|
||||
<div class="layuimini-container">
|
||||
<div class="layuimini-main">
|
||||
<form class="layui-form" lay-filter="groupForm" style="max-width: 980px;">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">分组名称</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="groupName" required lay-verify="required" placeholder="请输入分组名称" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="layui-elem-field" id="fieldsetConfig">
|
||||
<legend>分组输出/输入配置</legend>
|
||||
<div class="layui-field-box">
|
||||
<div class="layui-row layui-col-space10">
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">inpstr2_type</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="inpstr2_type">
|
||||
<option value="ntripcli" selected>ntripcli</option>
|
||||
<option value="tcpcli">tcpcli</option>
|
||||
<option value="tcpsvr">tcpsvr</option>
|
||||
<option value="serial">serial</option>
|
||||
<option value="file">file</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">inpstr2_format</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="inpstr2_format">
|
||||
<option value="rtcm3" selected>rtcm3</option>
|
||||
<option value="rtcm2">rtcm2</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">inpstr2_path</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="inpstr2_path" class="layui-input" value="ytcors38369:fyx85534@gnss.ytcors.cn:8003/RTCM33GRCEJpro">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">inpstr3_type</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="inpstr3_type">
|
||||
<option value="ntripcli" selected>ntripcli</option>
|
||||
<option value="tcpcli">tcpcli</option>
|
||||
<option value="tcpsvr">tcpsvr</option>
|
||||
<option value="serial">serial</option>
|
||||
<option value="file">file</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">inpstr3_format</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="inpstr3_format">
|
||||
<option value="rtcm3" selected>rtcm3</option>
|
||||
<option value="rtcm2">rtcm2</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">inpstr3_path</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="inpstr3_path" class="layui-input" value="Ming:@Zhang12345@ntrip.data.gnss.ga.gov.au:2101/BCEP00BKG0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">outstr1_type</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="outstr1_type">
|
||||
<option value="off" selected>off</option>
|
||||
<option value="tcpcli">tcpcli</option>
|
||||
<option value="tcpsvr">tcpsvr</option>
|
||||
<option value="ntripsvr">ntripsvr</option>
|
||||
<option value="serial">serial</option>
|
||||
<option value="file">file</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">outstr1_format</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="outstr1_format">
|
||||
<option value="llh" selected>llh</option>
|
||||
<option value="xyz">xyz</option>
|
||||
<option value="enu">enu</option>
|
||||
<option value="nmea">nmea</option>
|
||||
<option value="stat">stat</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">outstr1_path</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="outstr1_path" class="layui-input" placeholder="可留空">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">outstr2_type</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="outstr2_type">
|
||||
<option value="tcpcli" selected>tcpcli</option>
|
||||
<option value="tcpsvr">tcpsvr</option>
|
||||
<option value="ntripsvr">ntripsvr</option>
|
||||
<option value="serial">serial</option>
|
||||
<option value="file">file</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12 layui-col-sm6">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">outstr2_format</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="outstr2_format">
|
||||
<option value="llh" selected>llh</option>
|
||||
<option value="xyz">xyz</option>
|
||||
<option value="enu">enu</option>
|
||||
<option value="nmea">nmea</option>
|
||||
<option value="stat">stat</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">outstr2_path</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="outstr2_path" class="layui-input" placeholder="可留空">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="layui-elem-field" id="fieldsetPick">
|
||||
<legend>选择设备(穿梭框,可搜索)</legend>
|
||||
<div class="layui-field-box">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">所属部门</label>
|
||||
<div class="layui-input-inline">
|
||||
<select id="q_tenantSelect">
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">设备号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" id="q_deviceId" placeholder="模糊查询" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">关联基站</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" id="q_parentId" placeholder="模糊查询" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">项目号</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" id="q_projectId" placeholder="模糊查询" autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<label class="layui-form-label">使用状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<select id="q_opmode">
|
||||
<option value="">全部</option>
|
||||
<option value="0">使用</option>
|
||||
<option value="1">检修</option>
|
||||
<option value="2">停用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button type="button" class="layui-btn" id="btnQuery">查询</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transferDevices"></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit lay-filter="doSave">保存</button>
|
||||
<button type="button" class="layui-btn layui-btn-primary" id="btnCancel">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="../../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
|
||||
<script>
|
||||
layui.use(['form','transfer','layer'], function(){
|
||||
var $ = layui.$, form = layui.form, transfer = layui.transfer, layer = layui.layer;
|
||||
|
||||
// 渲染穿梭框
|
||||
function renderTransfer(list){
|
||||
var selected = [];
|
||||
(list||[]).forEach(function(x){ if(x.checked){ selected.push(x.value); }});
|
||||
transfer.render({
|
||||
elem: '#transferDevices',
|
||||
id: 'devPick',
|
||||
data: list || [],
|
||||
value: selected,
|
||||
title: ['设备列表','已选设备'],
|
||||
showSearch: true,
|
||||
height: 420,
|
||||
parseData: function(res){ return res; }
|
||||
});
|
||||
}
|
||||
|
||||
function queryDevices(){
|
||||
var p = {
|
||||
groupId: getQuery('groupId') || '',
|
||||
deviceId: $('#q_deviceId').val(),
|
||||
parentId: $('#q_parentId').val(),
|
||||
projectId: $('#q_projectId').val(),
|
||||
tenantName: $('#q_tenantSelect').val(),
|
||||
opmode: $('#q_opmode').val()
|
||||
};
|
||||
$.getJSON('/rtk/devices/for_transfer', p, function(resp){
|
||||
if(resp && resp.code === 0){
|
||||
renderTransfer(resp.data || []);
|
||||
} else {
|
||||
renderTransfer([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadTenants(){
|
||||
$.getJSON('/rtk/tenants/simple', function(resp){
|
||||
var sel = $('#q_tenantSelect');
|
||||
sel.empty(); sel.append('<option value="">全部</option>');
|
||||
if(resp && resp.data){
|
||||
resp.data.forEach(function(t){
|
||||
sel.append('<option value="'+ (t.name||'') +'">'+ (t.name||'') +'</option>');
|
||||
});
|
||||
}
|
||||
form.render('select');
|
||||
});
|
||||
}
|
||||
|
||||
$('#btnQuery').on('click', queryDevices);
|
||||
|
||||
$('#btnCancel').on('click', function(){
|
||||
var index = parent.layer.getFrameIndex(window.name);
|
||||
parent.layer.close(index);
|
||||
});
|
||||
|
||||
form.on('submit(doSave)', function(data){
|
||||
var group = data.field;
|
||||
var picked = transfer.getData('devPick') || [];
|
||||
var ids = picked.map(function(x){ return x.value; }).join(',');
|
||||
if(!group.groupName){ layer.msg('请填写分组名称'); return false; }
|
||||
layer.load(2);
|
||||
var groupId = getQuery('groupId');
|
||||
var payload = {
|
||||
groupName: group.groupName,
|
||||
inpstr2_type: group.inpstr2_type,
|
||||
inpstr2_path: group.inpstr2_path,
|
||||
inpstr2_format: group.inpstr2_format,
|
||||
inpstr3_type: group.inpstr3_type,
|
||||
inpstr3_path: group.inpstr3_path,
|
||||
inpstr3_format: group.inpstr3_format,
|
||||
outstr1_format: group.outstr1_format,
|
||||
outstr1_path: group.outstr1_path,
|
||||
outstr1_type: group.outstr1_type,
|
||||
outstr2_type: group.outstr2_type,
|
||||
outstr2_format: group.outstr2_format,
|
||||
outstr2_path: group.outstr2_path
|
||||
};
|
||||
var url, okMsg;
|
||||
if (groupId){
|
||||
// 编辑:先更新分组配置,再重绑设备
|
||||
payload.groupId = groupId;
|
||||
$.post('/rtk/group/update', payload, function(r1){
|
||||
var selIds = (transfer.getData('devPick')||[]).map(function(x){return x.value;}).join(',');
|
||||
$.post('/rtk/group/rebind_devices', {groupId: groupId, deviceIds: selIds}, function(r2){
|
||||
layer.closeAll('loading');
|
||||
if(r2 && r2.code===0){
|
||||
layer.msg('已更新分组配置,并调整设备('+ (r2.added||0) +'/'+ (r2.removed||0) +')');
|
||||
if(parent && parent.layui && parent.layui.table){ parent.layui.table.reload('rtkGroupTable'); }
|
||||
var index = parent.layer.getFrameIndex(window.name);
|
||||
parent.layer.close(index);
|
||||
} else {
|
||||
layer.msg('设备调整失败');
|
||||
}
|
||||
});
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
payload.deviceIds = ids;
|
||||
url = '/rtk/group/create_with_devices';
|
||||
okMsg = '已创建分组并绑定 ';
|
||||
}
|
||||
$.post(url, payload, function(resp){
|
||||
layer.closeAll('loading');
|
||||
if(resp && resp.code === 0){
|
||||
if (groupId){
|
||||
layer.msg(okMsg);
|
||||
} else {
|
||||
layer.msg(okMsg + (resp.boundDevices||0) + ' 台设备');
|
||||
}
|
||||
// 刷新父页面表格
|
||||
if(parent && parent.layui && parent.layui.table){ parent.layui.table.reload('rtkGroupTable'); }
|
||||
var index = parent.layer.getFrameIndex(window.name);
|
||||
parent.layer.close(index);
|
||||
} else {
|
||||
layer.msg((resp && resp.msg) || '保存失败');
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
function getQuery(k){
|
||||
var m = location.search.match(new RegExp('[?&]'+k+'=([^&]+)'));
|
||||
return m ? decodeURIComponent(m[1]) : '';
|
||||
}
|
||||
|
||||
function loadGroupIfEdit(){
|
||||
var gid = getQuery('groupId');
|
||||
if(!gid) return;
|
||||
// 编辑模式显示设备穿梭,并加载当前分组的选中项(通过 for_transfer 的 checked)
|
||||
$.getJSON('/rtk/group/get',{groupId: gid}, function(resp){
|
||||
if(resp && resp.code===0 && resp.data){
|
||||
var g = resp.data;
|
||||
$('input[name=groupName]').val(g.groupName||'');
|
||||
$('select[name=inpstr2_type]').val(g.inpstr2Type||'ntripcli');
|
||||
$('input[name=inpstr2_path]').val(g.inpstr2Path||'');
|
||||
$('select[name=inpstr2_format]').val(g.inpstr2Format||'rtcm3');
|
||||
$('select[name=inpstr3_type]').val(g.inpstr3Type||'ntripcli');
|
||||
$('input[name=inpstr3_path]').val(g.inpstr3Path||'');
|
||||
$('select[name=inpstr3_format]').val(g.inpstr3Format||'rtcm3');
|
||||
$('select[name=outstr1_type]').val(g.outstr1Type||'off');
|
||||
$('select[name=outstr1_format]').val(g.outstr1Format||'llh');
|
||||
$('input[name=outstr1_path]').val(g.outstr1Path||'');
|
||||
$('select[name=outstr2_type]').val(g.outstr2Type||'tcpcli');
|
||||
$('select[name=outstr2_format]').val(g.outstr2Format||'llh');
|
||||
$('input[name=outstr2_path]').val(g.outstr2Path||'');
|
||||
layui.form.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初次加载
|
||||
loadTenants();
|
||||
queryDevices();
|
||||
loadGroupIfEdit();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user