feat: 新增RTK调度及前端页面

This commit is contained in:
yarnom 2025-10-30 15:44:39 +08:00
parent 131b0da71d
commit 2cb32e8923
10 changed files with 1429 additions and 3 deletions

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -59,6 +59,12 @@
"icon": "fa fa-clipboard",
"target": "_self"
},
{
"title": "定位管理",
"href": "page/rtk_profile_batch",
"icon": "fa fa-clipboard",
"target": "_self"
},
{
"title": "配置管理",
"href": "",

View File

@ -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>

View File

@ -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>