新增MES模块,包含供应商、客户、车辆和地磅数据记录管理功能,支持免密接口和数据同步。更新相关控制器、实体、服务和数据库配置,优化权限管理和数据字典支持,确保系统的灵活性和可扩展性。

This commit is contained in:
geht
2026-04-30 15:28:20 +08:00
parent 142a0bdaba
commit b03cbeff9b
121 changed files with 10540 additions and 424 deletions

View File

@@ -0,0 +1,61 @@
-- 客户端连接列表权限初始化可直接执行带防重复
-- 权限标识xslmes:mes_xsl_client_connection:list
-- 1) 请按你们实际菜单树确认 parent_id
-- 优先使用你指定的父菜单 ID若不指定脚本会优先挂到MES基础资料菜单下
SET @input_parent_id = NULL;
SET @parent_id = COALESCE(
@input_parent_id,
(SELECT id FROM sys_permission WHERE name = 'MES基础资料' LIMIT 1),
(SELECT id FROM sys_permission WHERE perms = 'xslmes' LIMIT 1)
);
-- 2) 新增菜单目录/菜单避免重复插入
SET @menu_url = '/xslmes/mesXslClientConnection/list';
SET @menu_name = '客户端连接列表';
SET @menu_id = (
SELECT id FROM sys_permission
WHERE url = @menu_url AND menu_type = '1'
LIMIT 1
);
INSERT INTO sys_permission (
id, parent_id, name, perms, perms_type, menu_type, url, component, sort_no, status, del_flag, create_by, create_time
)
SELECT
REPLACE(UUID(), '-', ''), @parent_id, @menu_name, NULL, '1', '1',
@menu_url, 'xslmes/mesXslClientConnection/MesXslClientConnectionList', 100, '1', 0, 'admin', NOW()
FROM dual
WHERE @menu_id IS NULL AND @parent_id IS NOT NULL;
SET @menu_id = (
SELECT id FROM sys_permission
WHERE url = @menu_url AND menu_type = '1'
LIMIT 1
);
-- 2.1) 若菜单已存在则强制迁移到MES基础资料父菜单并修正组件路径
UPDATE sys_permission
SET parent_id = @parent_id,
component = 'xslmes/mesXslClientConnection/MesXslClientConnectionList',
update_by = 'admin',
update_time = NOW()
WHERE id = @menu_id
AND @menu_id IS NOT NULL
AND @parent_id IS NOT NULL;
-- 3) 新增按钮权限接口权限点避免重复插入
INSERT INTO sys_permission (
id, parent_id, name, perms, perms_type, menu_type, url, component, sort_no, status, del_flag, create_by, create_time
)
SELECT
REPLACE(UUID(), '-', ''), @menu_id, '查询', 'xslmes:mes_xsl_client_connection:list', '1', '2',
NULL, NULL, 1, '1', 0, 'admin', NOW()
FROM dual
WHERE @menu_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM sys_permission WHERE perms = 'xslmes:mes_xsl_client_connection:list' LIMIT 1
);
-- 4) 如果 @parent_id 为空表示没找到 xslmes 菜单需要你手工设置 @input_parent_id 再执行
SELECT @parent_id AS resolved_parent_id, @menu_id AS resolved_menu_id;

View File

@@ -0,0 +1,59 @@
package org.jeecg.modules.xslmes.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.xslmes.model.MesXslClientConnectionDTO;
import org.jeecg.modules.xslmes.service.IMesXslClientConnectionStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* MES 客户端连接列表
*/
@Tag(name = "MES客户端连接管理")
@RestController
@RequestMapping("/xslmes/mesXslClientConnection")
public class MesXslClientConnectionController {
private final IMesXslClientConnectionStore connectionStore;
public MesXslClientConnectionController(IMesXslClientConnectionStore connectionStore) {
this.connectionStore = connectionStore;
}
@Operation(summary = "客户端连接列表查询")
@RequiresPermissions("xslmes:mes_xsl_client_connection:list")
@GetMapping("/list")
public Result<Map<String, Object>> list(
@RequestParam(name = "type", required = false, defaultValue = "all") String type,
@RequestParam(name = "pageNo", required = false, defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", required = false, defaultValue = "20") Integer pageSize) {
List<MesXslClientConnectionDTO> all = connectionStore.listByType(type);
int safePageNo = pageNo == null || pageNo < 1 ? 1 : pageNo;
int safePageSize = pageSize == null || pageSize < 1 ? 20 : pageSize;
int fromIndex = (safePageNo - 1) * safePageSize;
int total = all.size();
List<MesXslClientConnectionDTO> records;
if (fromIndex >= total) {
records = List.of();
} else {
int toIndex = Math.min(fromIndex + safePageSize, total);
records = all.subList(fromIndex, toIndex);
}
Map<String, Object> page = new HashMap<>(8);
page.put("records", records);
page.put("total", total);
page.put("current", safePageNo);
page.put("size", safePageSize);
page.put("pages", safePageSize == 0 ? 0 : (total + safePageSize - 1) / safePageSize);
return Result.OK(page);
}
}

View File

@@ -17,6 +17,7 @@ import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus;
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
import org.jeecg.modules.xslmes.service.IMesXslCustomerService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -45,6 +46,9 @@ public class MesXslCustomerController extends JeecgController<MesXslCustomer, IM
@Autowired
private IMesXslCustomerService mesXslCustomerService;
@Autowired
private MesXslStompNotifyService stompNotify;
@Operation(summary = "MES客户管理-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<MesXslCustomer>> queryPageList(
@@ -82,6 +86,7 @@ public class MesXslCustomerController extends JeecgController<MesXslCustomer, IM
}
mesXslCustomerService.syncIzEnableWithStatus(mesXslCustomer);
mesXslCustomerService.save(mesXslCustomer);
stompNotify.publishCustomerChanged("add", mesXslCustomer.getId());
return Result.OK("添加成功!");
}
@@ -107,6 +112,7 @@ public class MesXslCustomerController extends JeecgController<MesXslCustomer, IM
}
mesXslCustomerService.syncIzEnableWithStatus(mesXslCustomer);
mesXslCustomerService.updateById(mesXslCustomer);
stompNotify.publishCustomerChanged("edit", mesXslCustomer.getId());
return Result.OK("编辑成功!");
}
@@ -131,11 +137,12 @@ public class MesXslCustomerController extends JeecgController<MesXslCustomer, IM
.set(MesXslCustomer::getIzEnable, izEnable)
.update();
if (updated) {
stompNotify.publishCustomerChanged("status", id);
return Result.OK("操作成功");
}
// MySQL 在 SET 值与库中完全一致时可能返回 0 行;或需二次确认是否已是目标状态
MesXslCustomer cur = mesXslCustomerService.getById(id);
if (cur != null && Objects.equals(status, cur.getStatus())) {
stompNotify.publishCustomerChanged("status", id);
return Result.OK("操作成功");
}
return Result.error("操作失败请确认记录存在且租户与当前登录一致tenant_id 勿为空)");
@@ -147,6 +154,7 @@ public class MesXslCustomerController extends JeecgController<MesXslCustomer, IM
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
mesXslCustomerService.removeById(id);
stompNotify.publishCustomerChanged("delete", id);
return Result.OK("删除成功!");
}
@@ -156,6 +164,7 @@ public class MesXslCustomerController extends JeecgController<MesXslCustomer, IM
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
mesXslCustomerService.removeByIds(Arrays.asList(ids.split(",")));
stompNotify.publishCustomerChanged("batchDelete", ids);
return Result.OK("批量删除成功!");
}
@@ -263,4 +272,5 @@ public class MesXslCustomerController extends JeecgController<MesXslCustomer, IM
}
return Result.error("文件导入失败!");
}
}

View File

@@ -0,0 +1,390 @@
package org.jeecg.modules.xslmes.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.constant.MesXslCustomerBizStatus;
import org.jeecg.modules.xslmes.entity.MesXslCustomer;
import org.jeecg.modules.xslmes.entity.MesXslSupplier;
import org.jeecg.modules.xslmes.entity.MesXslVehicle;
import org.jeecg.modules.xslmes.service.IMesXslCustomerService;
import org.jeecg.modules.xslmes.service.IMesXslSupplierService;
import org.jeecg.modules.xslmes.service.IMesXslVehicleService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.Objects;
/**
* 桌面端免密接口 — 统一入口
*
* <p>所有供 WPF 桌面端(匿名模式)调用的接口集中于此,便于统一维护权限白名单和版本演进。
* ShiroConfig 白名单:
* /xslmes/mesXslVehicle/anon/**
* /xslmes/mesXslCustomer/anon/**
*/
@Tag(name = "桌面端免密接口")
@RestController
@Slf4j
@RequiredArgsConstructor
public class MesXslDesktopAnonController {
private final IMesXslVehicleService vehicleService;
private final IMesXslCustomerService customerService;
private final IMesXslSupplierService supplierService;
private final MesXslStompNotifyService stompNotify;
// ═══════════════════════════ 车辆管理 ═══════════════════════════
@Operation(summary = "车辆-免密分页列表查询")
@GetMapping("/xslmes/mesXslVehicle/anon/list")
public Result<IPage<MesXslVehicle>> vehicleAnonList(
MesXslVehicle mesXslVehicle,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslVehicle> qw = QueryGenerator.initQueryWrapper(mesXslVehicle, req.getParameterMap());
IPage<MesXslVehicle> page = vehicleService.page(new Page<>(pageNo, pageSize), qw);
return Result.OK(page);
}
@Operation(summary = "车辆-免密通过id查询")
@GetMapping("/xslmes/mesXslVehicle/anon/queryById")
public Result<MesXslVehicle> vehicleAnonQueryById(@RequestParam(name = "id") String id) {
MesXslVehicle entity = vehicleService.getById(id);
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
}
@Operation(summary = "车辆-免密添加")
@PostMapping("/xslmes/mesXslVehicle/anon/add")
public Result<String> vehicleAnonAdd(@RequestBody MesXslVehicle mesXslVehicle) {
if (oConvertUtils.isEmpty(mesXslVehicle.getVehicleBelong())) {
return Result.error("车辆归属不能为空");
}
if (mesXslVehicle.getStatus() == null || mesXslVehicle.getStatus().isEmpty()) {
mesXslVehicle.setStatus("0");
}
applyVehicleBelong(mesXslVehicle);
vehicleService.save(mesXslVehicle);
stompNotify.publishVehicleChanged("add", mesXslVehicle.getId());
return Result.OK("添加成功!");
}
@Operation(summary = "车辆-免密编辑")
@RequestMapping(value = "/xslmes/mesXslVehicle/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> vehicleAnonEdit(@RequestBody MesXslVehicle mesXslVehicle) {
if (oConvertUtils.isEmpty(mesXslVehicle.getVehicleBelong())) {
return Result.error("车辆归属不能为空");
}
applyVehicleBelong(mesXslVehicle);
boolean ok = vehicleService.updateById(mesXslVehicle);
if (!ok) {
return Result.error("数据已被他人修改,请刷新后重试");
}
forceNullOppositeVehicleFieldsInDb(mesXslVehicle.getId(), mesXslVehicle.getVehicleBelong());
stompNotify.publishVehicleChanged("edit", mesXslVehicle.getId());
return Result.OK("编辑成功!");
}
@Operation(summary = "车辆-免密删除")
@DeleteMapping("/xslmes/mesXslVehicle/anon/delete")
public Result<String> vehicleAnonDelete(@RequestParam(name = "id") String id) {
vehicleService.removeById(id);
stompNotify.publishVehicleChanged("delete", id);
return Result.OK("删除成功!");
}
@Operation(summary = "车辆-免密批量删除")
@DeleteMapping("/xslmes/mesXslVehicle/anon/deleteBatch")
public Result<String> vehicleAnonDeleteBatch(@RequestParam(name = "ids") String ids) {
vehicleService.removeByIds(Arrays.asList(ids.split(",")));
stompNotify.publishVehicleChanged("batchDelete", ids);
return Result.OK("批量删除成功!");
}
@Operation(summary = "车辆-免密停用/启用")
@PostMapping("/xslmes/mesXslVehicle/anon/updateStatus")
public Result<String> vehicleAnonUpdateStatus(
@RequestParam(name = "id") String id,
@RequestParam(name = "status") String status) {
if (!"0".equals(status) && !"1".equals(status)) {
return Result.error("状态参数非法");
}
boolean ok = vehicleService.lambdaUpdate()
.eq(MesXslVehicle::getId, id)
.set(MesXslVehicle::getStatus, status)
.update();
if (ok) {
stompNotify.publishVehicleChanged("status", id);
}
return ok ? Result.OK("操作成功") : Result.error("操作失败");
}
// ═══════════════════════════ 客户管理 ═══════════════════════════
@Operation(summary = "客户-免密分页列表查询")
@GetMapping("/xslmes/mesXslCustomer/anon/list")
public Result<IPage<MesXslCustomer>> customerAnonList(
MesXslCustomer mesXslCustomer,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslCustomer> qw = QueryGenerator.initQueryWrapper(mesXslCustomer, req.getParameterMap());
IPage<MesXslCustomer> page = customerService.page(new Page<>(pageNo, pageSize), qw);
return Result.OK(page);
}
@Operation(summary = "客户-免密通过id查询")
@GetMapping("/xslmes/mesXslCustomer/anon/queryById")
public Result<MesXslCustomer> customerAnonQueryById(@RequestParam(name = "id") String id) {
MesXslCustomer entity = customerService.getById(id);
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
}
@Operation(summary = "客户-免密添加")
@PostMapping("/xslmes/mesXslCustomer/anon/add")
public Result<String> customerAnonAdd(@RequestBody MesXslCustomer mesXslCustomer) {
if (oConvertUtils.isEmpty(mesXslCustomer.getCustomerCode())) {
return Result.error("客户编码不能为空");
}
String code = mesXslCustomer.getCustomerCode().trim();
mesXslCustomer.setCustomerCode(code);
if (customerService.existsSameCustomerCode(code, null)) {
return Result.error("客户编码已存在,不允许重复");
}
String st = mesXslCustomer.getStatus();
if (st != null) {
st = st.trim();
mesXslCustomer.setStatus(st.isEmpty() ? null : st);
}
if (mesXslCustomer.getStatus() == null || mesXslCustomer.getStatus().isEmpty()) {
mesXslCustomer.setStatus(MesXslCustomerBizStatus.ENABLED);
}
customerService.syncIzEnableWithStatus(mesXslCustomer);
customerService.save(mesXslCustomer);
stompNotify.publishCustomerChanged("add", mesXslCustomer.getId());
return Result.OK("添加成功!");
}
@Operation(summary = "客户-免密编辑")
@RequestMapping(value = "/xslmes/mesXslCustomer/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> customerAnonEdit(@RequestBody MesXslCustomer mesXslCustomer) {
if (oConvertUtils.isEmpty(mesXslCustomer.getId())) {
return Result.error("主键不能为空");
}
if (oConvertUtils.isEmpty(mesXslCustomer.getCustomerCode())) {
return Result.error("客户编码不能为空");
}
String code = mesXslCustomer.getCustomerCode().trim();
mesXslCustomer.setCustomerCode(code);
if (customerService.existsSameCustomerCode(code, mesXslCustomer.getId())) {
return Result.error("客户编码已存在,不允许重复");
}
if (mesXslCustomer.getStatus() != null) {
String s = mesXslCustomer.getStatus().trim();
mesXslCustomer.setStatus(s.isEmpty() ? null : s);
}
customerService.syncIzEnableWithStatus(mesXslCustomer);
boolean ok = customerService.updateById(mesXslCustomer);
if (!ok) {
return Result.error("数据已被他人修改,请刷新后重试");
}
stompNotify.publishCustomerChanged("edit", mesXslCustomer.getId());
return Result.OK("编辑成功!");
}
@Operation(summary = "客户-免密删除")
@DeleteMapping("/xslmes/mesXslCustomer/anon/delete")
public Result<String> customerAnonDelete(@RequestParam(name = "id") String id) {
customerService.removeById(id);
stompNotify.publishCustomerChanged("delete", id);
return Result.OK("删除成功!");
}
@Operation(summary = "客户-免密批量删除")
@DeleteMapping("/xslmes/mesXslCustomer/anon/deleteBatch")
public Result<String> customerAnonDeleteBatch(@RequestParam(name = "ids") String ids) {
customerService.removeByIds(Arrays.asList(ids.split(",")));
stompNotify.publishCustomerChanged("batchDelete", ids);
return Result.OK("批量删除成功!");
}
@Operation(summary = "客户-免密启用/停用")
@PostMapping("/xslmes/mesXslCustomer/anon/updateStatus")
public Result<String> customerAnonUpdateStatus(
@RequestParam(name = "id") String id,
@RequestParam(name = "status") String status) {
if (status != null) {
status = status.trim();
}
if (!MesXslCustomerBizStatus.ENABLED.equals(status) && !MesXslCustomerBizStatus.DISABLED.equals(status)) {
return Result.error("状态参数非法");
}
int izEnable = MesXslCustomerBizStatus.DISABLED.equals(status) ? 0 : 1;
boolean updated = customerService.lambdaUpdate()
.eq(MesXslCustomer::getId, id)
.set(MesXslCustomer::getStatus, status)
.set(MesXslCustomer::getIzEnable, izEnable)
.update();
if (updated) {
stompNotify.publishCustomerChanged("status", id);
return Result.OK("操作成功");
}
MesXslCustomer cur = customerService.getById(id);
if (cur != null && Objects.equals(status, cur.getStatus())) {
stompNotify.publishCustomerChanged("status", id);
return Result.OK("操作成功");
}
return Result.error("操作失败");
}
@Operation(summary = "客户-免密校验客户编码是否重复")
@GetMapping("/xslmes/mesXslCustomer/anon/checkCustomerCode")
public Result<String> customerAnonCheckCustomerCode(
@RequestParam(name = "customerCode") String customerCode,
@RequestParam(name = "dataId", required = false) String dataId) {
if (oConvertUtils.isEmpty(customerCode) || customerCode.trim().isEmpty()) {
return Result.OK("该值可用!");
}
if (customerService.existsSameCustomerCode(customerCode.trim(), dataId)) {
return Result.error("该客户编码已存在");
}
return Result.OK("该值可用!");
}
// ═══════════════════════════ 供应商管理 ═══════════════════════════
@Operation(summary = "供应商-免密分页列表查询")
@GetMapping("/xslmes/mesXslSupplier/anon/list")
public Result<IPage<MesXslSupplier>> supplierAnonList(
MesXslSupplier mesXslSupplier,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslSupplier> qw = QueryGenerator.initQueryWrapper(mesXslSupplier, req.getParameterMap());
IPage<MesXslSupplier> page = supplierService.page(new Page<>(pageNo, pageSize), qw);
return Result.OK(page);
}
@Operation(summary = "供应商-免密通过id查询")
@GetMapping("/xslmes/mesXslSupplier/anon/queryById")
public Result<MesXslSupplier> supplierAnonQueryById(@RequestParam(name = "id") String id) {
MesXslSupplier entity = supplierService.getById(id);
return entity != null ? Result.OK(entity) : Result.error("未找到对应数据");
}
@Operation(summary = "供应商-免密添加")
@PostMapping("/xslmes/mesXslSupplier/anon/add")
public Result<String> supplierAnonAdd(@RequestBody MesXslSupplier mesXslSupplier) {
if (oConvertUtils.isEmpty(mesXslSupplier.getSupplierCode())) {
return Result.error("供应商编码不能为空");
}
if (mesXslSupplier.getStatus() == null || mesXslSupplier.getStatus().isEmpty()) {
mesXslSupplier.setStatus("0");
}
supplierService.save(mesXslSupplier);
stompNotify.publishSupplierChanged("add", mesXslSupplier.getId());
return Result.OK("添加成功!");
}
@Operation(summary = "供应商-免密编辑")
@RequestMapping(value = "/xslmes/mesXslSupplier/anon/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> supplierAnonEdit(@RequestBody MesXslSupplier mesXslSupplier) {
boolean ok = supplierService.updateById(mesXslSupplier);
if (!ok) {
return Result.error("数据已被他人修改,请刷新后重试");
}
stompNotify.publishSupplierChanged("edit", mesXslSupplier.getId());
return Result.OK("编辑成功!");
}
@Operation(summary = "供应商-免密删除")
@DeleteMapping("/xslmes/mesXslSupplier/anon/delete")
public Result<String> supplierAnonDelete(@RequestParam(name = "id") String id) {
supplierService.removeById(id);
stompNotify.publishSupplierChanged("delete", id);
return Result.OK("删除成功!");
}
@Operation(summary = "供应商-免密批量删除")
@DeleteMapping("/xslmes/mesXslSupplier/anon/deleteBatch")
public Result<String> supplierAnonDeleteBatch(@RequestParam(name = "ids") String ids) {
supplierService.removeByIds(Arrays.asList(ids.split(",")));
stompNotify.publishSupplierChanged("batchDelete", ids);
return Result.OK("批量删除成功!");
}
@Operation(summary = "供应商-免密启用/停用")
@PostMapping("/xslmes/mesXslSupplier/anon/updateStatus")
public Result<String> supplierAnonUpdateStatus(
@RequestParam(name = "id") String id,
@RequestParam(name = "status") String status) {
if (!"0".equals(status) && !"1".equals(status)) {
return Result.error("状态参数非法");
}
boolean ok = supplierService.lambdaUpdate()
.eq(MesXslSupplier::getId, id)
.set(MesXslSupplier::getStatus, status)
.update();
if (ok) {
stompNotify.publishSupplierChanged("status", id);
}
return ok ? Result.OK("操作成功") : Result.error("操作失败");
}
// ─────────────────────────── 车辆私有辅助 ────────────────────────────
private void applyVehicleBelong(MesXslVehicle v) {
if (oConvertUtils.isEmpty(v.getVehicleBelong())) {
return;
}
String b = v.getVehicleBelong();
if ("1".equals(b)) {
v.setSupplierId(null);
v.setSupplierName(null);
v.setSupplierShortName(null);
} else if ("2".equals(b)) {
v.setCustomerIds(null);
v.setCustomerShortName(null);
} else if ("3".equals(b)) {
v.setCustomerIds(null);
v.setCustomerShortName(null);
v.setSupplierId(null);
v.setSupplierName(null);
v.setSupplierShortName(null);
}
}
private void forceNullOppositeVehicleFieldsInDb(String id, String vehicleBelong) {
if (oConvertUtils.isEmpty(id) || oConvertUtils.isEmpty(vehicleBelong)) {
return;
}
LambdaUpdateWrapper<MesXslVehicle> uw = new LambdaUpdateWrapper<MesXslVehicle>().eq(MesXslVehicle::getId, id);
if ("1".equals(vehicleBelong)) {
uw.set(MesXslVehicle::getSupplierId, null)
.set(MesXslVehicle::getSupplierName, null)
.set(MesXslVehicle::getSupplierShortName, null);
} else if ("2".equals(vehicleBelong)) {
uw.set(MesXslVehicle::getCustomerIds, null).set(MesXslVehicle::getCustomerShortName, null);
} else if ("3".equals(vehicleBelong)) {
uw.set(MesXslVehicle::getCustomerIds, null)
.set(MesXslVehicle::getCustomerShortName, null)
.set(MesXslVehicle::getSupplierId, null)
.set(MesXslVehicle::getSupplierName, null)
.set(MesXslVehicle::getSupplierShortName, null);
} else {
return;
}
vehicleService.update(null, uw);
}
}

View File

@@ -15,6 +15,7 @@ import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.xslmes.entity.MesXslSupplier;
import org.jeecg.modules.xslmes.service.IMesXslSupplierService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
@@ -32,6 +33,8 @@ public class MesXslSupplierController extends JeecgController<MesXslSupplier, IM
@Autowired
private IMesXslSupplierService mesXslSupplierService;
@Autowired
private MesXslStompNotifyService stompNotify;
@Operation(summary = "MES供应商管理-分页列表查询")
@GetMapping(value = "/list")
@@ -55,6 +58,7 @@ public class MesXslSupplierController extends JeecgController<MesXslSupplier, IM
mesXslSupplier.setStatus("0");
}
mesXslSupplierService.save(mesXslSupplier);
stompNotify.publishSupplierChanged("add", mesXslSupplier.getId());
return Result.OK("添加成功!");
}
@@ -64,6 +68,7 @@ public class MesXslSupplierController extends JeecgController<MesXslSupplier, IM
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody MesXslSupplier mesXslSupplier) {
mesXslSupplierService.updateById(mesXslSupplier);
stompNotify.publishSupplierChanged("edit", mesXslSupplier.getId());
return Result.OK("编辑成功!");
}
@@ -81,6 +86,9 @@ public class MesXslSupplierController extends JeecgController<MesXslSupplier, IM
.eq(MesXslSupplier::getId, id)
.set(MesXslSupplier::getStatus, status)
.update();
if (ok) {
stompNotify.publishSupplierChanged("status", id);
}
return ok ? Result.OK("操作成功") : Result.error("操作失败");
}
@@ -90,6 +98,7 @@ public class MesXslSupplierController extends JeecgController<MesXslSupplier, IM
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
mesXslSupplierService.removeById(id);
stompNotify.publishSupplierChanged("delete", id);
return Result.OK("删除成功!");
}
@@ -99,6 +108,7 @@ public class MesXslSupplierController extends JeecgController<MesXslSupplier, IM
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
mesXslSupplierService.removeByIds(Arrays.asList(ids.split(",")));
stompNotify.publishSupplierChanged("batchDelete", ids);
return Result.OK("批量删除成功!");
}

View File

@@ -17,6 +17,7 @@ import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.entity.MesXslVehicle;
import org.jeecg.modules.xslmes.service.IMesXslVehicleService;
import org.jeecg.modules.xslmes.service.MesXslStompNotifyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
@@ -35,6 +36,9 @@ public class MesXslVehicleController extends JeecgController<MesXslVehicle, IMes
@Autowired
private IMesXslVehicleService mesXslVehicleService;
@Autowired
private MesXslStompNotifyService stompNotify;
@Operation(summary = "MES车辆管理-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<MesXslVehicle>> queryPageList(
@@ -61,6 +65,7 @@ public class MesXslVehicleController extends JeecgController<MesXslVehicle, IMes
}
applyVehicleBelong(mesXslVehicle);
mesXslVehicleService.save(mesXslVehicle);
stompNotify.publishVehicleChanged("add", mesXslVehicle.getId());
return Result.OK("添加成功!");
}
@@ -76,6 +81,7 @@ public class MesXslVehicleController extends JeecgController<MesXslVehicle, IMes
mesXslVehicleService.updateById(mesXslVehicle);
// updateById 默认不更新 null 字段,互斥侧需在库中显式置 NULL
forceNullOppositeFieldsInDb(mesXslVehicle.getId(), mesXslVehicle.getVehicleBelong());
stompNotify.publishVehicleChanged("edit", mesXslVehicle.getId());
return Result.OK("编辑成功!");
}
@@ -143,6 +149,9 @@ public class MesXslVehicleController extends JeecgController<MesXslVehicle, IMes
.eq(MesXslVehicle::getId, id)
.set(MesXslVehicle::getStatus, status)
.update();
if (ok) {
stompNotify.publishVehicleChanged("status", id);
}
return ok ? Result.OK("操作成功") : Result.error("操作失败");
}
@@ -152,6 +161,7 @@ public class MesXslVehicleController extends JeecgController<MesXslVehicle, IMes
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
mesXslVehicleService.removeById(id);
stompNotify.publishVehicleChanged("delete", id);
return Result.OK("删除成功!");
}
@@ -161,6 +171,7 @@ public class MesXslVehicleController extends JeecgController<MesXslVehicle, IMes
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
mesXslVehicleService.removeByIds(Arrays.asList(ids.split(",")));
stompNotify.publishVehicleChanged("batchDelete", ids);
return Result.OK("批量删除成功!");
}
@@ -185,4 +196,5 @@ public class MesXslVehicleController extends JeecgController<MesXslVehicle, IMes
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, MesXslVehicle.class);
}
}

View File

@@ -0,0 +1,127 @@
package org.jeecg.modules.xslmes.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Random;
/**
* 地磅数据记录
*/
@Tag(name = "地磅数据记录")
@RestController
@RequestMapping("/xslmes/mesXslWeightRecord")
@Slf4j
public class MesXslWeightRecordController extends JeecgController<MesXslWeightRecord, IMesXslWeightRecordService> {
@Autowired
private IMesXslWeightRecordService mesXslWeightRecordService;
@Operation(summary = "地磅数据记录-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<MesXslWeightRecord>> queryPageList(MesXslWeightRecord mesXslWeightRecord,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslWeightRecord> queryWrapper = QueryGenerator.initQueryWrapper(mesXslWeightRecord, req.getParameterMap());
queryWrapper.orderByDesc("create_time");
Page<MesXslWeightRecord> page = new Page<>(pageNo, pageSize);
IPage<MesXslWeightRecord> pageList = mesXslWeightRecordService.page(page, queryWrapper);
return Result.OK(pageList);
}
@AutoLog(value = "地磅数据记录-添加")
@Operation(summary = "地磅数据记录-添加")
@RequiresPermissions("xslmes:mes_xsl_weight_record:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody MesXslWeightRecord mesXslWeightRecord) {
// 自动生成榜单号BDH-yyyyMMddHHmmss + 3位随机数
if (mesXslWeightRecord.getBillNo() == null || mesXslWeightRecord.getBillNo().isBlank()) {
String dateStr = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String seq = String.format("%03d", new Random().nextInt(1000));
mesXslWeightRecord.setBillNo("BDH-" + dateStr + seq);
}
computeNetWeight(mesXslWeightRecord);
mesXslWeightRecordService.save(mesXslWeightRecord);
return Result.OK("添加成功!");
}
@AutoLog(value = "地磅数据记录-编辑")
@Operation(summary = "地磅数据记录-编辑")
@RequiresPermissions("xslmes:mes_xsl_weight_record:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody MesXslWeightRecord mesXslWeightRecord) {
computeNetWeight(mesXslWeightRecord);
mesXslWeightRecordService.updateById(mesXslWeightRecord);
return Result.OK("编辑成功!");
}
@AutoLog(value = "地磅数据记录-通过id删除")
@Operation(summary = "地磅数据记录-通过id删除")
@RequiresPermissions("xslmes:mes_xsl_weight_record:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
mesXslWeightRecordService.removeById(id);
return Result.OK("删除成功!");
}
@AutoLog(value = "地磅数据记录-批量删除")
@Operation(summary = "地磅数据记录-批量删除")
@RequiresPermissions("xslmes:mes_xsl_weight_record:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
this.mesXslWeightRecordService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
@Operation(summary = "地磅数据记录-通过id查询")
@GetMapping(value = "/queryById")
public Result<MesXslWeightRecord> queryById(@RequestParam(name = "id", required = true) String id) {
MesXslWeightRecord record = mesXslWeightRecordService.getById(id);
if (record == null) {
return Result.error("未找到对应数据");
}
return Result.OK(record);
}
@RequiresPermissions("xslmes:mes_xsl_weight_record:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, MesXslWeightRecord mesXslWeightRecord) {
return super.exportXls(request, mesXslWeightRecord, MesXslWeightRecord.class, "地磅数据记录");
}
@RequiresPermissions("xslmes:mes_xsl_weight_record:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, MesXslWeightRecord.class);
}
private void computeNetWeight(MesXslWeightRecord record) {
BigDecimal gross = record.getGrossWeight();
BigDecimal tare = record.getTareWeight();
if (gross != null && tare != null) {
BigDecimal net = gross.subtract(tare);
record.setNetWeight(net.compareTo(BigDecimal.ZERO) >= 0 ? net : BigDecimal.ZERO);
}
}
}

View File

@@ -2,6 +2,7 @@ package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -52,7 +53,10 @@ public class MesXslCustomer extends JeecgEntity implements Serializable {
@Schema(description = "业务状态(字典 xslmes_customer_status0启用1停用2删除逻辑删除见 del_flag")
private String status;
@Schema(description = "删除状态0正常 1已删除")
@Schema(description = "乐观锁版本MyBatis-Plus @Version更新时自增")
@Version
private Integer version;
@TableLogic
private Integer delFlag;

View File

@@ -2,6 +2,7 @@ package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -49,7 +50,10 @@ public class MesXslSupplier extends JeecgEntity implements Serializable {
@Schema(description = "状态0启用 1停用")
private String status;
@Schema(description = "删除状态0正常 1已删除")
@Schema(description = "乐观锁版本MyBatis-Plus @Version更新时自增")
@Version
private Integer version;
@TableLogic
private Integer delFlag;

View File

@@ -2,6 +2,7 @@ package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -95,7 +96,10 @@ public class MesXslVehicle extends JeecgEntity implements Serializable {
@Schema(description = "状态0启用 1停用")
private String status;
@Schema(description = "删除状态0正常 1已删除")
@Schema(description = "乐观锁版本MyBatis-Plus @Version更新时自增")
@Version
private Integer version;
@TableLogic
private Integer delFlag;

View File

@@ -0,0 +1,86 @@
package org.jeecg.modules.xslmes.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.system.base.entity.JeecgEntity;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 地磅数据记录
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("mes_xsl_weight_record")
@Schema(description = "地磅数据记录")
public class MesXslWeightRecord extends JeecgEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Excel(name = "榜单号", width = 18)
@Schema(description = "榜单号")
private String billNo;
@Excel(name = "称重日期", width = 15, format = "yyyy-MM-dd")
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@Schema(description = "称重日期")
private Date weighDate;
@Excel(name = "进出方向", width = 10, dicCode = "xslmes_inout_direction")
@Dict(dicCode = "xslmes_inout_direction")
@Schema(description = "进出方向1进厂 2出厂")
private String inoutDirection;
@Schema(description = "车辆ID")
private String vehicleId;
@Excel(name = "车号", width = 15)
@Schema(description = "车号(车牌号)")
private String plateNumber;
@Excel(name = "发货单位", width = 25)
@Schema(description = "发货单位(进厂时为供应商名称)")
private String senderUnit;
@Excel(name = "收货单位", width = 25)
@Schema(description = "收货单位(出厂时为客户简称)")
private String receiverUnit;
@Excel(name = "货物名称", width = 20)
@Schema(description = "货物名称")
private String goodsName;
@Excel(name = "毛重(KG)", width = 12)
@Schema(description = "毛重(KG),实际称量")
private BigDecimal grossWeight;
@Excel(name = "皮重(KG)", width = 12)
@Schema(description = "皮重(KG),从车辆档案带出")
private BigDecimal tareWeight;
@Excel(name = "净重(KG)", width = 12)
@Schema(description = "净重(KG)=毛重-皮重,自动计算")
private BigDecimal netWeight;
@Excel(name = "司机", width = 12)
@Schema(description = "司机")
private String driverName;
@Excel(name = "手机号", width = 15)
@Schema(description = "手机号")
private String driverPhone;
@Schema(description = "租户ID")
private Integer tenantId;
}

View File

@@ -0,0 +1,117 @@
package org.jeecg.modules.xslmes.http;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.model.MesXslClientConnectionDTO;
import org.jeecg.modules.xslmes.service.IMesXslClientConnectionStore;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Date;
/**
* xslmes 免密接口 lastSeen 采集
*/
@Component
public class MesXslHttpLastSeenFilter extends OncePerRequestFilter {
private final IMesXslClientConnectionStore connectionStore;
public MesXslHttpLastSeenFilter(IMesXslClientConnectionStore connectionStore) {
this.connectionStore = connectionStore;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String uri = request.getRequestURI();
if (oConvertUtils.isEmpty(uri)) {
return true;
}
String lower = uri.toLowerCase();
return !(lower.contains("/xslmes/") && lower.contains("/anon/"));
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
filterChain.doFilter(request, response);
String ip = resolveIp(request);
String hostName = resolveHostName(request);
// 优先用客户端显式传来的设备ID否则回退到 http:ip
String explicitDeviceId = request.getHeader("X-Device-Id");
String deviceId = oConvertUtils.isNotEmpty(explicitDeviceId)
? explicitDeviceId.trim()
: "http:" + ip;
Date now = new Date();
String userName = request.getHeader("X-User-Name");
String realName = request.getHeader("X-Real-Name");
MesXslClientConnectionDTO dto = new MesXslClientConnectionDTO();
dto.setDeviceId(deviceId);
dto.setSessionId(null);
dto.setUserName(oConvertUtils.isNotEmpty(userName) ? userName.trim() : "unknown");
dto.setRealName(oConvertUtils.isNotEmpty(realName) ? realName.trim() : "");
dto.setPlatform(resolvePlatform(request));
dto.setIp(ip);
dto.setHostName(hostName);
dto.setConnectTime(now);
dto.setLastSeen(now);
connectionStore.upsertHttp(dto);
}
private String resolveIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (oConvertUtils.isNotEmpty(xff)) {
int comma = xff.indexOf(',');
return comma > 0 ? xff.substring(0, comma).trim() : xff.trim();
}
String realIp = request.getHeader("X-Real-IP");
if (oConvertUtils.isNotEmpty(realIp)) {
return realIp.trim();
}
String remote = request.getRemoteAddr();
return oConvertUtils.isNotEmpty(remote) ? remote : "unknown";
}
private String resolveHostName(HttpServletRequest request) {
String value = request.getHeader("X-Host-Name");
if (oConvertUtils.isNotEmpty(value)) {
return value.trim();
}
return "unknown";
}
private String resolvePlatform(HttpServletRequest request) {
// 优先读客户端显式传来的平台标识
String explicit = request.getHeader("X-Platform");
if (oConvertUtils.isNotEmpty(explicit)) {
String v = explicit.trim().toLowerCase();
if ("desktop".equals(v) || "pc".equals(v) || "windows".equals(v) || "win".equals(v)) return "desktop";
if ("pda".equals(v) || "mobile".equals(v) || "android".equals(v) || "ios".equals(v)) return "pda";
return v;
}
// 回退到 User-Agent 分析
String ua = request.getHeader("User-Agent");
if (oConvertUtils.isEmpty(ua)) {
return "unknown";
}
String lower = ua.toLowerCase();
if (lower.contains("android") || lower.contains("iphone") || lower.contains("ipad") || lower.contains("mobile")) {
return "pda";
}
if (lower.contains("windows") || lower.contains("macintosh") || lower.contains("linux")) {
return "desktop";
}
// WPF HttpClient 默认 UA 为 "dotnet-httpclient",归为 desktop
if (lower.contains("dotnet")) {
return "desktop";
}
return "unknown";
}
}

View File

@@ -0,0 +1,10 @@
package org.jeecg.modules.xslmes.mapper;
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* 地磅数据记录 Mapper
*/
public interface MesXslWeightRecordMapper extends BaseMapper<MesXslWeightRecord> {
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.xslmes.mapper.MesXslWeightRecordMapper">
</mapper>

View File

@@ -0,0 +1,113 @@
package org.jeecg.modules.xslmes.model;
import java.io.Serializable;
import java.util.Date;
/**
* 客户端连接信息
*/
public class MesXslClientConnectionDTO implements Serializable {
private static final long serialVersionUID = 1L;
/** 来源stomp/http */
private String source;
/** STOMP 会话 IDHTTP 场景为空) */
private String sessionId;
/** 设备标识:当前默认使用 sessionId 或 http:ip */
private String deviceId;
/** 用户账号/登录名,未知时为 unknown */
private String userName;
/** 用户姓名(真实姓名) */
private String realName;
/** 平台desktop/pda/unknown */
private String platform;
/** 客户端 IP */
private String ip;
/** 客户端主机名机器名WPF 传 Environment.MachineName */
private String hostName;
/** 首次连接时间 */
private Date connectTime;
/** 最近活跃时间 */
private Date lastSeen;
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getRealName() {
return realName;
}
public void setRealName(String realName) {
this.realName = realName;
}
public String getPlatform() {
return platform;
}
public void setPlatform(String platform) {
this.platform = platform;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getHostName() {
return hostName;
}
public void setHostName(String hostName) {
this.hostName = hostName;
}
public Date getConnectTime() {
return connectTime;
}
public void setConnectTime(Date connectTime) {
this.connectTime = connectTime;
}
public Date getLastSeen() {
return lastSeen;
}
public void setLastSeen(Date lastSeen) {
this.lastSeen = lastSeen;
}
}

View File

@@ -0,0 +1,25 @@
package org.jeecg.modules.xslmes.service;
import org.jeecg.modules.xslmes.model.MesXslClientConnectionDTO;
import java.util.Date;
import java.util.List;
/**
* 客户端连接状态存储
*/
public interface IMesXslClientConnectionStore {
void upsertStomp(MesXslClientConnectionDTO dto);
void touchStomp(String sessionId, Date lastSeen);
void removeStomp(String sessionId);
void upsertHttp(MesXslClientConnectionDTO dto);
List<MesXslClientConnectionDTO> listByType(String type);
/** 清空所有连接记录(后端重启时调用,清除进程失联后的残留记录) */
void clearAll();
}

View File

@@ -0,0 +1,10 @@
package org.jeecg.modules.xslmes.service;
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 地磅数据记录 Service
*/
public interface IMesXslWeightRecordService extends IService<MesXslWeightRecord> {
}

View File

@@ -0,0 +1,52 @@
package org.jeecg.modules.xslmes.service;
import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 桌面端 STOMP 实时通知服务
* 统一管理各业务实体的变更事件广播,供各 Controller 注入使用。
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MesXslStompNotifyService {
private final SimpMessagingTemplate messagingTemplate;
/** 广播车辆数据变更事件到 /topic/sync/mes-vehicles */
public void publishVehicleChanged(String action, String vehicleId) {
publish("/topic/sync/mes-vehicles", "MES_VEHICLE_CHANGED", "vehicleId", vehicleId, action);
}
/** 广播客户数据变更事件到 /topic/sync/mes-customers */
public void publishCustomerChanged(String action, String customerId) {
publish("/topic/sync/mes-customers", "MES_CUSTOMER_CHANGED", "customerId", customerId, action);
}
/** 广播供应商数据变更事件到 /topic/sync/mes-suppliers */
public void publishSupplierChanged(String action, String supplierId) {
publish("/topic/sync/mes-suppliers", "MES_SUPPLIER_CHANGED", "supplierId", supplierId, action);
}
// ─────────────────────────── 私有辅助 ────────────────────────────
private void publish(String topic, String cmd, String idKey, String idValue, String action) {
try {
Map<String, Object> event = new HashMap<>();
event.put("cmd", cmd);
event.put("action", action);
event.put(idKey, idValue);
event.put("timestamp", System.currentTimeMillis());
messagingTemplate.convertAndSend(topic, JSON.toJSONString(event));
} catch (Exception e) {
log.debug("广播 STOMP 事件失败 [{}]: {}", cmd, e.getMessage());
}
}
}

View File

@@ -0,0 +1,154 @@
package org.jeecg.modules.xslmes.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.model.MesXslClientConnectionDTO;
import org.jeecg.modules.xslmes.service.IMesXslClientConnectionStore;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 客户端连接状态 Redis 存储
*/
@Slf4j
@Service
public class MesXslClientConnectionStore implements IMesXslClientConnectionStore {
private static final String STOMP_MAP_KEY = "mes:xsl:client-connections:stomp:map";
private static final String HTTP_MAP_KEY = "mes:xsl:client-connections:http:map";
private static final long TTL_SECONDS = 180L;
private final RedisUtil redisUtil;
public MesXslClientConnectionStore(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
@Override
public synchronized void upsertStomp(MesXslClientConnectionDTO dto) {
if (dto == null || oConvertUtils.isEmpty(dto.getSessionId())) {
return;
}
dto.setSource("stomp");
Map<String, MesXslClientConnectionDTO> map = loadMap(STOMP_MAP_KEY);
MesXslClientConnectionDTO old = map.get(dto.getSessionId());
if (old != null && dto.getConnectTime() == null) {
dto.setConnectTime(old.getConnectTime());
}
if (dto.getConnectTime() == null) {
dto.setConnectTime(new Date());
}
if (dto.getLastSeen() == null) {
dto.setLastSeen(new Date());
}
map.put(dto.getSessionId(), dto);
saveMap(STOMP_MAP_KEY, map);
}
@Override
public synchronized void touchStomp(String sessionId, Date lastSeen) {
if (oConvertUtils.isEmpty(sessionId)) {
return;
}
Map<String, MesXslClientConnectionDTO> map = loadMap(STOMP_MAP_KEY);
MesXslClientConnectionDTO dto = map.get(sessionId);
if (dto == null) {
dto = new MesXslClientConnectionDTO();
dto.setSource("stomp");
dto.setSessionId(sessionId);
dto.setDeviceId(sessionId);
dto.setUserName("unknown");
dto.setPlatform("unknown");
dto.setIp("unknown");
dto.setConnectTime(lastSeen == null ? new Date() : lastSeen);
}
dto.setLastSeen(lastSeen == null ? new Date() : lastSeen);
map.put(sessionId, dto);
saveMap(STOMP_MAP_KEY, map);
}
@Override
public synchronized void removeStomp(String sessionId) {
if (oConvertUtils.isEmpty(sessionId)) {
return;
}
Map<String, MesXslClientConnectionDTO> map = loadMap(STOMP_MAP_KEY);
if (map.remove(sessionId) != null) {
saveMap(STOMP_MAP_KEY, map);
}
}
@Override
public synchronized void upsertHttp(MesXslClientConnectionDTO dto) {
if (dto == null || oConvertUtils.isEmpty(dto.getDeviceId())) {
return;
}
dto.setSource("http");
Map<String, MesXslClientConnectionDTO> map = loadMap(HTTP_MAP_KEY);
MesXslClientConnectionDTO old = map.get(dto.getDeviceId());
if (old != null && dto.getConnectTime() == null) {
dto.setConnectTime(old.getConnectTime());
}
if (dto.getConnectTime() == null) {
dto.setConnectTime(new Date());
}
if (dto.getLastSeen() == null) {
dto.setLastSeen(new Date());
}
map.put(dto.getDeviceId(), dto);
saveMap(HTTP_MAP_KEY, map);
}
@Override
public synchronized List<MesXslClientConnectionDTO> listByType(String type) {
String t = oConvertUtils.getString(type);
List<MesXslClientConnectionDTO> result = new ArrayList<>();
if ("stomp".equalsIgnoreCase(t)) {
result.addAll(loadMap(STOMP_MAP_KEY).values());
} else if ("http".equalsIgnoreCase(t)) {
result.addAll(loadMap(HTTP_MAP_KEY).values());
} else {
result.addAll(loadMap(STOMP_MAP_KEY).values());
result.addAll(loadMap(HTTP_MAP_KEY).values());
}
result.sort(Comparator.comparing(MesXslClientConnectionDTO::getLastSeen,
Comparator.nullsLast(Comparator.reverseOrder())));
return result;
}
@Override
public synchronized void clearAll() {
try {
redisUtil.del(STOMP_MAP_KEY);
redisUtil.del(HTTP_MAP_KEY);
log.info("[连接管理] 已清空所有客户端连接记录(后端重启)");
} catch (Exception e) {
log.warn("[连接管理] 清空连接记录失败: {}", e.getMessage());
}
}
@SuppressWarnings("unchecked")
private Map<String, MesXslClientConnectionDTO> loadMap(String key) {
Object obj = redisUtil.get(key);
if (obj instanceof Map) {
return (Map<String, MesXslClientConnectionDTO>) obj;
}
return new HashMap<>();
}
private void saveMap(String key, Map<String, MesXslClientConnectionDTO> map) {
try {
redisUtil.set(key, map);
redisUtil.expire(key, TTL_SECONDS);
} catch (Exception e) {
log.warn("保存客户端连接状态失败, key={}, error={}", key, e.getMessage());
}
}
}

View File

@@ -0,0 +1,15 @@
package org.jeecg.modules.xslmes.service.impl;
import org.jeecg.modules.xslmes.entity.MesXslWeightRecord;
import org.jeecg.modules.xslmes.mapper.MesXslWeightRecordMapper;
import org.jeecg.modules.xslmes.service.IMesXslWeightRecordService;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
/**
* 地磅数据记录 ServiceImpl
*/
@Service
public class MesXslWeightRecordServiceImpl extends ServiceImpl<MesXslWeightRecordMapper, MesXslWeightRecord>
implements IMesXslWeightRecordService {
}

View File

@@ -0,0 +1,29 @@
package org.jeecg.modules.xslmes.websocket;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.xslmes.service.IMesXslClientConnectionStore;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 后端启动/关闭时维护客户端连接状态一致性。
*
* <p>后端进程重启时,所有 WebSocket 连接已断开,但 Redis 里的 STOMP/HTTP 记录
* 可能仍在 TTL 内残留,导致旧 sessionId 与新连接共存。
* 在 ApplicationReadyEvent容器全部就绪后清空全部记录保证重启后列表干净。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MesXslConnectionLifecycleListener {
private final IMesXslClientConnectionStore connectionStore;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
connectionStore.clearAll();
log.info("[连接管理] 后端启动完成,已清空历史连接记录");
}
}

View File

@@ -0,0 +1,129 @@
package org.jeecg.modules.xslmes.websocket;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.model.MesXslClientConnectionDTO;
import org.jeecg.modules.xslmes.service.IMesXslClientConnectionStore;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
import java.security.Principal;
import java.util.Date;
import java.util.List;
/**
* STOMP 连接状态采集拦截器
*/
@Component
public class MesXslStompConnectionChannelInterceptor implements ChannelInterceptor {
private final IMesXslClientConnectionStore connectionStore;
public MesXslStompConnectionChannelInterceptor(IMesXslClientConnectionStore connectionStore) {
this.connectionStore = connectionStore;
}
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor == null) {
return message;
}
String sessionId = accessor.getSessionId();
if (oConvertUtils.isEmpty(sessionId)) {
return message;
}
Date now = new Date();
StompCommand command = accessor.getCommand();
if (StompCommand.DISCONNECT.equals(command)) {
connectionStore.removeStomp(sessionId);
return message;
}
if (StompCommand.CONNECT.equals(command)) {
MesXslClientConnectionDTO dto = new MesXslClientConnectionDTO();
dto.setSessionId(sessionId);
dto.setDeviceId(resolveDeviceId(accessor, sessionId));
dto.setUserName(resolveUserName(accessor));
dto.setRealName(resolveRealName(accessor));
dto.setPlatform(resolvePlatform(accessor));
dto.setIp(resolveIp(accessor));
dto.setHostName(resolveHostName(accessor));
dto.setConnectTime(now);
dto.setLastSeen(now);
connectionStore.upsertStomp(dto);
return message;
}
// 对 SEND/SUBSCRIBE/HEARTBEAT 等其他活动统一刷新活跃时间
connectionStore.touchStomp(sessionId, now);
return message;
}
private String resolveUserName(StompHeaderAccessor accessor) {
Principal principal = accessor.getUser();
if (principal != null && oConvertUtils.isNotEmpty(principal.getName())) {
return principal.getName();
}
String byHeader = firstNotEmptyHeader(accessor, "userName", "username", "userId", "x-user");
return oConvertUtils.isNotEmpty(byHeader) ? byHeader : "unknown";
}
private String resolvePlatform(StompHeaderAccessor accessor) {
String value = firstNotEmptyHeader(accessor, "platform", "x-platform", "deviceType");
if (oConvertUtils.isEmpty(value)) {
return "unknown";
}
String v = value.trim().toLowerCase();
if ("desktop".equals(v) || "pc".equals(v) || "windows".equals(v) || "win".equals(v)) {
return "desktop";
}
if ("pda".equals(v) || "mobile".equals(v) || "android".equals(v) || "ios".equals(v) || "iphone".equals(v)) {
return "pda";
}
return v;
}
private String resolveIp(StompHeaderAccessor accessor) {
String value = firstNotEmptyHeader(accessor, "ip", "clientIp", "X-Forwarded-For", "x-forwarded-for");
if (oConvertUtils.isEmpty(value)) {
return "unknown";
}
int comma = value.indexOf(',');
return comma > 0 ? value.substring(0, comma).trim() : value.trim();
}
private String firstNotEmptyHeader(StompHeaderAccessor accessor, String... keys) {
for (String key : keys) {
List<String> values = accessor.getNativeHeader(key);
if (values != null) {
for (String v : values) {
if (oConvertUtils.isNotEmpty(v)) {
return v;
}
}
}
}
return null;
}
private String resolveRealName(StompHeaderAccessor accessor) {
String value = firstNotEmptyHeader(accessor, "realName", "real-name", "x-real-name", "displayName");
return oConvertUtils.isNotEmpty(value) ? value.trim() : "";
}
private String resolveHostName(StompHeaderAccessor accessor) {
String value = firstNotEmptyHeader(accessor, "hostName", "hostname", "x-host-name", "machineName");
return oConvertUtils.isNotEmpty(value) ? value.trim() : "unknown";
}
private String resolveDeviceId(StompHeaderAccessor accessor, String sessionId) {
String value = firstNotEmptyHeader(accessor, "deviceId", "terminalNo", "terminalId", "clientId");
return oConvertUtils.isNotEmpty(value) ? value : sessionId;
}
}

View File

@@ -0,0 +1,23 @@
package org.jeecg.modules.xslmes.websocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* xslmes WebSocket 入站通道配置
*/
@Configuration
public class MesXslWebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
private final MesXslStompConnectionChannelInterceptor connectionInterceptor;
public MesXslWebSocketBrokerConfig(MesXslStompConnectionChannelInterceptor connectionInterceptor) {
this.connectionInterceptor = connectionInterceptor;
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(connectionInterceptor);
}
}