Compare commits

20 Commits

Author SHA1 Message Date
cd1ba55b26 优化 2026-03-13 10:30:55 +08:00
zc
687be5840e Merge branch 'refs/heads/master_lz' 2026-03-12 20:20:46 +08:00
zc
d9f808ecc1 tcp服务启动 2026-03-12 19:45:07 +08:00
c1d84aaf81 优化 2026-03-12 16:54:55 +08:00
zc
dec15eb913 tcp服务启动 2026-03-11 17:53:58 +08:00
18e014d9cb Merge branch 'master' into master_lz 2026-03-11 17:44:57 +08:00
2cb03b146a tcp服务 2026-03-11 17:44:06 +08:00
zc
7d48c78c9b 海康工业相机优化关闭socket视频重连异常 2026-03-11 17:39:05 +08:00
zc
c80dce6419 海康工业相机对接彩色视频流 2026-03-11 15:05:14 +08:00
zc
4802e11f7c 海康工业相机对接优化视频流 2026-03-11 11:09:56 +08:00
zc
fde2f18ff5 海康工业相机对接黑白画面 2026-03-11 10:55:59 +08:00
zc
41e0b0b5b4 添加海康工业相机demo代码 2026-03-10 16:54:45 +08:00
zc
fb05360c5e 代码优化 2026-03-09 20:33:47 +08:00
df9479f870 抓取失败返回优化 2026-03-09 16:25:41 +08:00
e4d6de61ae Merge branch 'refs/heads/master' into master_lz 2026-03-06 17:59:47 +08:00
73c1bc9d01 称重抓取 2026-03-06 17:58:55 +08:00
zc
ae9a607da1 代码优化 2026-03-06 15:55:06 +08:00
32f88b1443 物料查询返回错误提示 2026-03-06 15:41:12 +08:00
zc
c59df81b38 电子秤设备&优化 2026-03-05 18:13:39 +08:00
zc
66449b0482 电子秤设备 2026-03-05 16:02:36 +08:00
35 changed files with 1419 additions and 41 deletions

View File

@@ -119,5 +119,10 @@
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
</dependencies>
</project>

View File

@@ -155,5 +155,12 @@
<artifactId>spring-test</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version> <!-- 使用较新稳定版本 -->
</dependency>
</dependencies>
</project>
</project>

View File

@@ -21,5 +21,11 @@
<groupId>top.wms</groupId>
<artifactId>wms-common</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -18,5 +18,12 @@
<groupId>top.wms</groupId>
<artifactId>wms-common</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -55,4 +55,9 @@ public interface MaterialInfoService extends BaseService<MaterialInfoResp, Mater
* 照片批量上传处理
* */
void uploadMaterialPhotos(MultipartFile file);
/*
* 称重时照片抓取
* */
String catchPhoto(MultipartFile file);
}

View File

@@ -413,4 +413,12 @@ public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper,
.forEach(code -> log.warn("物料编码 [{}] 不存在,照片更新失败", code));
}
@Override
public String catchPhoto(MultipartFile file) {
String photoStoragePath = "catch" + DateUtil.today() + "/";
FileInfo fileInfo = fileService.upload(file, photoStoragePath, null, true, true);
CheckUtils.throwIfNull(fileInfo, "照片上传失败");
return fileInfo.getUrl();
}
}

View File

@@ -100,13 +100,18 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
}
});
// 处理本地存储文件 URL
FileInfo fileInfo = uploadPretreatment.upload();
String domain = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH);
fileInfo.setUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getFilename()));
if (StrUtil.isNotBlank(fileInfo.getThFilename())) {
fileInfo.setThUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getThFilename()));
} else {
fileInfo.setThUrl(fileInfo.getUrl());
FileInfo fileInfo = null;
try {
fileInfo = uploadPretreatment.upload();
String domain = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH);
fileInfo.setUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getFilename()));
if (StrUtil.isNotBlank(fileInfo.getThFilename())) {
fileInfo.setThUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getThFilename()));
} else {
fileInfo.setThUrl(fileInfo.getUrl());
}
} catch (Exception e) {
}
return fileInfo;
}
@@ -146,4 +151,4 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode()));
}
}
}
}

View File

@@ -41,4 +41,10 @@ public class WorkOrderDO extends BaseDO {
* 总重量
*/
private BigDecimal totalWeight;
/**
* 总数量
*/
private Integer totalCount;
}

View File

@@ -0,0 +1,68 @@
package top.wms.admin.weighManage.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import jakarta.validation.constraints.*;
/**
* 创建或修改任务工单信息参数
*
* @author zc
* @since 2026/03/03 17:09
*/
@Data
@Schema(description = "创建或修改任务工单信息参数")
public class WorkOrderInfoReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 工单主键id
*/
@Schema(description = "工单主键id")
private Long workOrderId;
/**
* 物料主键id
*/
@Schema(description = "物料主键id")
@NotNull(message = "物料主键id不能为空")
private Long materialId;
/**
* 称重次数
*/
@Schema(description = "称重次数")
private Integer weightTime;
/**
* 物料数量
*/
@Schema(description = "物料数量")
private Integer quantity;
/**
* 称重重量
*/
@Schema(description = "称重重量")
private BigDecimal weight;
/**
* 计算重量
*/
@Schema(description = "计算重量")
private BigDecimal calculatedWeight;
/**
* 抓拍的图片
*/
@Schema(description = "抓拍的图片")
private String imgUrl;
}

View File

@@ -8,8 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.time.*;
import java.math.BigDecimal;
import java.util.List;
/**
* 创建或修改任务工单信息参数
@@ -25,23 +24,34 @@ public class WorkOrderReq implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总重量
* 物料主键id
*/
@Schema(description = "总重量")
@NotNull(message = "总重量不能为空")
private BigDecimal totalWeight;
@Schema(description = "物料主键id")
@NotNull(message = "物料主键id不能为空")
private Long materialId;
/**
* 创建人
* 物料名称
*/
@Schema(description = "创建人")
@NotNull(message = "创建人不能为空")
private Long createUser;
@Schema(description = "物料名称")
@NotBlank(message = "物料名称不能为空")
private String materialName;
/**
* 创建时间
* 称重列表
*/
@Schema(description = "创建时间")
@NotNull(message = "创建时间不能为空")
private LocalDateTime createTime;
@Schema(description = "称重列表")
@NotEmpty(message = "称重列表不能为空")
private List<WorkOrderInfoReq> workOrderInfos;
/**
* 手动填写的物料数量
*/
private String inputQuantity;
/**
* 电子秤重量
*/
private String ahDeviceWeight;
}

View File

@@ -30,6 +30,12 @@ public class WorkOrderResp extends BaseDetailResp {
@Schema(description = "卡号")
private String cardNo;
/**
* 工单编号
*/
@Schema(description = "工单编号")
private String orderNo;
/**
* 标题
*/
@@ -73,9 +79,15 @@ public class WorkOrderResp extends BaseDetailResp {
private String photoUrl;
/**
* 总重量
* 计算总重量(标重)
*/
@Schema(description = "总重量")
@Schema(description = "计算总重量(标重)")
private BigDecimal totalCalculatedWeight;
/**
* 实际总重量
*/
@Schema(description = "实际总重量")
private BigDecimal totalWeight;
/**

View File

@@ -22,5 +22,29 @@ public interface WorkOrderService extends BaseService<WorkOrderResp, WorkOrderRe
* @param id 任务工单主键id
* @return 任务工单详情信息
*/
List<WorkOrderInfoResp> getDetail(Long id);
WorkOrderResp getDetail(Long id);
/**
* 创建任务工单
*
* @param req 创建任务工单参数
* @return 任务工单信息
*/
WorkOrderResp addWorKerOrder(WorkOrderReq req);
/**
* 获取任务工单详情信息
*
* @param id 任务工单主键id
* @return 任务工单详情信息
*/
List<WorkOrderInfoResp> getWorkOrderInfos(Long id);
/**
* 校验物料数量和重量是否匹配
*
* @param req
* @return
*/
int validateWeighing(WorkOrderReq req);
}

View File

@@ -1,24 +1,36 @@
package top.wms.admin.weighManage.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseServiceImpl;
import top.wms.admin.common.context.UserContextHolder;
import top.wms.admin.material.mapper.MaterialInfoMapper;
import top.wms.admin.material.model.entity.MaterialInfoDO;
import top.wms.admin.system.service.ConfigService;
import top.wms.admin.weighManage.mapper.WorkOrderInfoMapper;
import top.wms.admin.weighManage.mapper.WorkOrderMapper;
import top.wms.admin.weighManage.model.entity.WorkOrderDO;
import top.wms.admin.weighManage.model.entity.WorkOrderInfoDO;
import top.wms.admin.weighManage.model.query.WorkOrderQuery;
import top.wms.admin.weighManage.model.req.WorkOrderInfoReq;
import top.wms.admin.weighManage.model.req.WorkOrderReq;
import top.wms.admin.weighManage.model.resp.WorkOrderInfoResp;
import top.wms.admin.weighManage.model.resp.WorkOrderResp;
import top.wms.admin.weighManage.service.WorkOrderService;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
@@ -28,15 +40,21 @@ import java.util.List;
* @since 2026/03/03 17:09
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class WorkOrderServiceImpl extends BaseServiceImpl<WorkOrderMapper, WorkOrderDO, WorkOrderResp, WorkOrderResp, WorkOrderQuery, WorkOrderReq> implements WorkOrderService {
private final WorkOrderInfoMapper workOrderInfoMapper;
private final MaterialInfoMapper materialInfoMapper;
private final ConfigService configService;
@Override
public PageResp<WorkOrderResp> page(WorkOrderQuery query, PageQuery pageQuery) {
QueryWrapper<WorkOrderDO> queryWrapper = new QueryWrapper<>();
queryWrapper.like(StrUtil.isNotBlank(query.getMaterialName()), "m.material_name", query.getMaterialName());
queryWrapper.eq(null != query.getOrderNo(), "w.order_no", query.getOrderNo());
queryWrapper.groupBy("w.id");
this.sort(queryWrapper, pageQuery);
IPage<WorkOrderResp> page = baseMapper.selectWorkOrderPage(new Page<>(pageQuery.getPage(), pageQuery
@@ -46,8 +64,103 @@ public class WorkOrderServiceImpl extends BaseServiceImpl<WorkOrderMapper, WorkO
}
@Override
public List<WorkOrderInfoResp> getDetail(Long id) {
public WorkOrderResp getDetail(Long id) {
WorkOrderDO workOrderDO = baseMapper.selectById(id);
WorkOrderResp workOrderResp = BeanUtil.copyProperties(workOrderDO, WorkOrderResp.class);
if (null != workOrderDO.getMaterialId()) {
MaterialInfoDO materialInfoDO = materialInfoMapper.selectById(workOrderDO.getMaterialId());
workOrderResp.setMaterialName(materialInfoDO.getMaterialName());
workOrderResp.setEncoding(materialInfoDO.getEncoding());
}
List<WorkOrderInfoDO> workOrderInfos = workOrderInfoMapper.selectList(new QueryWrapper<WorkOrderInfoDO>()
.eq("work_order_id", id));
if (CollUtil.isNotEmpty(workOrderInfos)) {
BigDecimal bigDecimal = new BigDecimal("0");
for (WorkOrderInfoDO workOrderInfoDO : workOrderInfos) {
bigDecimal = bigDecimal.add(workOrderInfoDO.getCalculatedWeight());
}
workOrderResp.setTotalCalculatedWeight(bigDecimal);
}
return workOrderResp;
}
@Override
public List<WorkOrderInfoResp> getWorkOrderInfos(Long id) {
return baseMapper.getDetail(id);
}
@Override
public void afterDelete(List<Long> ids) {
workOrderInfoMapper.delete(new QueryWrapper<WorkOrderInfoDO>().in("work_order_id", ids));
}
@Override
public WorkOrderResp addWorKerOrder(WorkOrderReq req) {
if (CollUtil.isEmpty(req.getWorkOrderInfos())) {
throw new IllegalArgumentException("称重列表不能为空");
}
BigDecimal totalWeight = new BigDecimal("0");
int totalCount = 0;
for (WorkOrderInfoReq workOrderInfoReq : req.getWorkOrderInfos()) {
totalWeight = workOrderInfoReq.getWeight().add(totalWeight);
totalCount += workOrderInfoReq.getQuantity();
}
WorkOrderDO workOrder = new WorkOrderDO();
// 生成纯数字订单号:年月日时分秒 + 6位随机数
String timestamp = DateUtil.format(new Date(), "yyyyMMddHHmmss");
String randomNum = String.format("%06d", (int)(Math.random() * 1000000));
workOrder.setOrderNo(timestamp + randomNum);
String title = DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN) + "-" + UserContextHolder
.getUsername() + "-" + req.getMaterialName();
workOrder.setTitle(title);
workOrder.setMaterialId(req.getMaterialId());
workOrder.setTotalWeight(totalWeight);
workOrder.setTotalCount(totalCount);
baseMapper.insert(workOrder);
//新增工单详情列表信息
List<WorkOrderInfoDO> workOrderInfoDOList = BeanUtil.copyToList(req.getWorkOrderInfos(), WorkOrderInfoDO.class);
workOrderInfoDOList.forEach(workOrderInfoDO -> workOrderInfoDO.setWorkOrderId(workOrder.getId()));
workOrderInfoMapper.insertBatch(workOrderInfoDOList);
return BeanUtil.copyProperties(workOrder, WorkOrderResp.class);
}
@Override
public int validateWeighing(WorkOrderReq req) {
log.info("validateWeighing req:{}", req);
String configValue = configService.getConfigValue("weight_float_ratio");
BigDecimal weightFloat = new BigDecimal("0.06");
if (StrUtil.isNotBlank(configValue)) {
weightFloat = new BigDecimal(configValue);
}
//计算标准重量
MaterialInfoDO materialInfoDO = materialInfoMapper.selectById(req.getMaterialId());
BigDecimal standardWeight = materialInfoDO.getUnitWeight().subtract(new BigDecimal(req.getInputQuantity()));
BigDecimal electronicWeight = new BigDecimal(req.getAhDeviceWeight());
// 检查 electronicWeight 是否大于 standardWeight
if (electronicWeight.compareTo(standardWeight) <= 0) {
log.error("电子秤重量必须大于标准重量");
return 500; // 电子秤重量必须大于标准重量
}
// 计算比值:(electronicWeight - standardWeight) / standardWeight
BigDecimal weightDifference = electronicWeight.subtract(standardWeight);
BigDecimal ratio = weightDifference.divide(standardWeight, 4, BigDecimal.ROUND_HALF_UP);
// 检查比值是否超过 6%
if (ratio.compareTo(weightFloat) > 0) {
log.error("比值超过 6%,当前比值: {}", ratio);
return 500; // 比值超过 6%
}
return 200; // 验证通过
}
}

View File

@@ -10,12 +10,10 @@
m.photo_url,
m.unit_weight,
u.card_no,
u.username createUserString,
count(*) as totalCount
u.username createUserString
from sys_work_order w
left join sys_material_info m on w.material_id = m.id
left join sys_user u on w.create_user = u.id
left join sys_work_order_info wi on w.id = wi.work_order_id
${ew.customSqlSegment}
</select>

View File

@@ -53,6 +53,23 @@
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.780</version>
</dependency>
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-api</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.hikvision</groupId>
<artifactId>MvCameraControlWrapper</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/Library/MvCameraControlWrapper.jar</systemPath>
</dependency>
</dependencies>
<build>
<finalName>${project.parent.name}</finalName>

View File

@@ -0,0 +1,50 @@
:: 检查java安装环境
@echo off
:: 设置全局错误码
set /A errorcode=0
:: 开启扩展功能
setlocal ENABLEEXTENSIONS
setlocal ENABLEDELAYEDEXPANSION
::通过向临时文件写内容,判断是否有文件读写权限
echo > tmp
if exist tmp (
del /F/Q tmp >nul 2>&1
) else (
echo Please switch to an administrator account to run this batch!!!
set /A errorcode=1
goto:END
)
::判断JAVA_HOME是否被定义
if "%JAVA_HOME%"=="" (
echo JAVA_HOME not set. Please make sure that java is correctly installed.
set /A errorcode=2
goto:END
)
:: 判断CLASSPATH是否被定义
if "%CLASSPATH%"=="" (
echo CLASSPATH not set. Please make sure that java is correctly installed.
set /A errorcode=3
goto:END
)
:: 判断java版本是否比 1.7.0高,需先将版本信息重定向到文件中,再对文件内容进行分析
java -version >nul 2> JavaVer.tmp
for /F "tokens=1,2,3*" %%i in (JavaVer.tmp) do (
if "%%j" == "version" (
if %%k LSS "1.7.0" (
echo Java version is less than "1.7.0", warnings or errors may occur.
)
)
)
del /F/Q JavaVer.tmp >nul 2>&1
:END
::返回错误码
exit /B %errorcode%

Binary file not shown.

View File

@@ -67,6 +67,28 @@
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.780</version>
</dependency>
<dependency>
<groupId>com.fazecast</groupId>
<artifactId>jSerialComm</artifactId>
<version>2.9.2</version>
</dependency>
<!-- WebSocket API -->
<dependency>
<groupId>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-api</artifactId>
<version>2.1.0</version>
</dependency>
<!-- 本地库MvCameraControlWrapper -->
<dependency>
<groupId>com.hikvision</groupId>
<artifactId>MvCameraControlWrapper</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/Library/MvCameraControlWrapper.jar</systemPath>
</dependency>
</dependencies>
<build>

View File

@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.spring.EnableFileStorage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.continew.starter.core.autoconfigure.project.ProjectProperties;
@@ -29,6 +30,7 @@ import top.continew.starter.web.model.R;
@RestController
@SpringBootApplication
@RequiredArgsConstructor
@EnableScheduling
public class WmsAdminApplication {
private final ProjectProperties projectProperties;

View File

@@ -0,0 +1,18 @@
package top.wms.admin.config.webSocket;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import top.wms.admin.controller.weighManage.ah.ScaleWebSocketHandler;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 注册WebSocket端点允许所有跨域请求
registry.addHandler(new ScaleWebSocketHandler(), "/ws/scale").setAllowedOrigins("*");
}
}

View File

@@ -7,6 +7,7 @@ import jakarta.validation.constraints.NotNull;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.core.validation.ValidationUtils;
import top.continew.starter.extension.crud.enums.Api;
@@ -43,8 +44,10 @@ import java.io.IOException;
public class MaterialInfoController extends BaseController<MaterialInfoService, MaterialInfoResp, MaterialInfoResp, MaterialInfoQuery, MaterialInfoReq> {
@GetMapping("/code/{code}")
public MaterialInfoDO getMaterialInfoByCode(@PathVariable String code) {
return baseService.getMaterialInfoByCode(code);
public MaterialInfoDO getMaterialInfoByCode(@PathVariable String code) {
MaterialInfoDO materialInfoDO = baseService.getMaterialInfoByCode(code);
CheckUtils.throwIfEmpty(materialInfoDO, "未查询到相关物料信息");
return materialInfoDO;
}
@Operation(summary = "下载导入模板", description = "下载导入模板")
@@ -79,4 +82,11 @@ public class MaterialInfoController extends BaseController<MaterialInfoService,
baseService.uploadMaterialPhotos(zipFile);
return R.ok();
}
@Operation(summary = "照片抓取", description = "照片抓取")
@PostMapping("/import/catch")
public String catchPhoto(@RequestParam("file") MultipartFile file) {
CheckUtils.throwIfEmpty(file, "照片抓取失败,请重新抓取");
return baseService.catchPhoto(file);
}
}

View File

@@ -0,0 +1,95 @@
package top.wms.admin.controller.tcp.config;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import top.wms.admin.controller.tcp.handler.TcpServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class NettyTcpServer {
@Value("${tcp.server.port:9005}")
private int port;
@Value("${tcp.server.boss-threads:1}")
private int bossThreads;
@Value("${tcp.server.worker-threads:4}")
private int workerThreads;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private Channel serverChannel;
@PostConstruct
public void start() throws InterruptedException {
log.info("正在启动TCP服务端端口: {}", port);
bossGroup = new NioEventLoopGroup(bossThreads);
workerGroup = new NioEventLoopGroup(workerThreads);
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 解决TCP粘包问题
pipeline.addLast(new LineBasedFrameDecoder(1024));
// 字符串编解码
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
// 关键修复直接new不要从Spring容器获取
pipeline.addLast(new TcpServerHandler());
log.debug("新连接接入,处理器已添加");
}
});
ChannelFuture future = bootstrap.bind(port).sync();
if (future.isSuccess()) {
serverChannel = future.channel();
log.info("TCP服务端启动成功端口: {}", port);
} else {
log.error("TCP服务端启动失败", future.cause());
}
}
@PreDestroy
public void stop() {
log.info("正在关闭TCP服务端...");
try {
if (serverChannel != null) {
serverChannel.close().sync();
}
} catch (InterruptedException e) {
log.error("关闭服务端通道时发生错误", e);
} finally {
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
}
log.info("TCP服务端已关闭");
}
}

View File

@@ -0,0 +1,69 @@
package top.wms.admin.controller.tcp.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class SimpleRequestMatcher {
// 用队列存储响应
private final BlockingQueue<String> responseQueue = new LinkedBlockingQueue<>(1);
// 当前等待的请求标识
private volatile boolean isWaiting = false;
/**
* 等待响应
*/
public String waitForResponse(int timeoutSeconds) {
isWaiting = true;
try {
log.info("等待响应, 超时={}秒", timeoutSeconds);
// 从队列取响应最多等待timeoutSeconds秒
String response = responseQueue.poll(timeoutSeconds, TimeUnit.SECONDS);
if (response != null) {
log.info("收到响应: {}", response);
return response;
} else {
log.error("等待响应超时");
return "TIMEOUT";
}
} catch (InterruptedException e) {
log.error("等待响应被中断: {}", e.getMessage());
return "ERROR";
} finally {
isWaiting = false;
}
}
/**
* 接收响应
*/
public void handleResponse(String response) {
try {
// 如果有请求在等待,把响应放入队列
if (isWaiting) {
responseQueue.offer(response);
log.info("响应接收成功: {}", response);
} else {
log.warn("没有正在等待的请求, 响应被丢弃: {}", response);
}
} catch (Exception e) {
log.error("处理响应失败: {}", e.getMessage());
}
}
/**
* 清空队列
*/
public void clear() {
responseQueue.clear();
isWaiting = false;
log.info("请求匹配器已清空");
}
}

View File

@@ -0,0 +1,67 @@
package top.wms.admin.controller.tcp.handler;
import top.wms.admin.controller.tcp.config.SimpleRequestMatcher;
import top.wms.admin.controller.tcp.util.SpringContextUtil;
import top.wms.admin.controller.tcp.manager.ChannelManager;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TcpServerHandler extends SimpleChannelInboundHandler<String> {
private ChannelManager channelManager;
private SimpleRequestMatcher requestMatcher;
private ChannelManager getChannelManager() {
if (channelManager == null) {
channelManager = SpringContextUtil.getBean(ChannelManager.class);
}
return channelManager;
}
private SimpleRequestMatcher getRequestMatcher() {
if (requestMatcher == null) {
requestMatcher = SpringContextUtil.getBean(SimpleRequestMatcher.class);
}
return requestMatcher;
}
private String getClientId(ChannelHandlerContext ctx) {
return ctx.channel().remoteAddress().toString();
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
String clientId = getClientId(ctx);
getChannelManager().addChannel(clientId, ctx.channel());
log.info("✅ VM客户端连接成功: {}", clientId);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
String clientId = getClientId(ctx);
getChannelManager().removeChannel(clientId);
log.info("❌ VM客户端断开连接: {}", clientId);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
String clientId = getClientId(ctx);
log.info("📥 收到VM数据 [{}]: {}", clientId, msg);
try {
// 直接把收到的消息交给匹配器(不解析,不匹配)
String cleanMsg = msg.trim();
getRequestMatcher().handleResponse(cleanMsg);
} catch (Exception e) {
log.error("处理消息失败: {}", e.getMessage());
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("连接异常: {}", cause.getMessage());
ctx.close();
}
}

View File

@@ -0,0 +1,47 @@
package top.wms.admin.controller.tcp.manager;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class ChannelManager {
// 存储所有VM客户端的连接
private final ConcurrentHashMap<String, Channel> channels = new ConcurrentHashMap<>();
// 添加连接
public void addChannel(String clientId, Channel channel) {
channels.put(clientId, channel);
log.info("VM客户端 [{}] 已连接,当前在线客户端数: {}", clientId, channels.size());
}
// 移除连接
public void removeChannel(String clientId) {
channels.remove(clientId);
log.info("VM客户端 [{}] 已断开,当前在线客户端数: {}", clientId, channels.size());
}
// 获取第一个连接的VM如果有多个VM可以根据需要修改
public Channel getFirstChannel() {
return channels.values().stream().findFirst().orElse(null);
}
// 根据ID获取连接
public Channel getChannel(String clientId) {
return channels.get(clientId);
}
// 检查是否有VM连接
public boolean hasConnection() {
return !channels.isEmpty();
}
// 获取所有连接
public ConcurrentHashMap<String, Channel> getAllChannels() {
return channels;
}
}

View File

@@ -0,0 +1,21 @@
package top.wms.admin.controller.tcp.model;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
// 指令请求
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommandRequest {
private String processId; // 要执行的流程ID如 "PROCESS_1"
private String param; // 参数,如 "10.5"
private Integer priority; // 优先级(可选)
// 构建完整的指令字符串
public String buildCommand() {
// 格式:*PROCESS_1,10.5#
return String.format("*%s,%s#", processId, param);
}
}

View File

@@ -0,0 +1,23 @@
package top.wms.admin.controller.tcp.model;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
// VM返回的结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class VMResult {
private String processId; // 流程ID
private String status; // OK 或 NG
private double value; // 测量值
private long timestamp; // 时间戳
public VMResult(String processId, String status, double value) {
this.processId = processId;
this.status = status;
this.value = value;
this.timestamp = System.currentTimeMillis();
}
}

View File

@@ -0,0 +1,72 @@
package top.wms.admin.controller.tcp.service;
import io.netty.channel.Channel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import top.wms.admin.controller.tcp.manager.ChannelManager;
import top.wms.admin.controller.tcp.model.VMResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Service
public class CommandService {
// 统计
private final AtomicInteger successCount = new AtomicInteger(0);
private final AtomicInteger failCount = new AtomicInteger(0);
// 存储最近100条结果
private final ConcurrentLinkedQueue<VMResult> resultQueue = new ConcurrentLinkedQueue<>();
// 处理VM返回的结果
public void processResult(String processId, String status, double value) {
VMResult result = new VMResult(processId, status, value);
// 更新统计
if ("OK".equalsIgnoreCase(status)) {
successCount.incrementAndGet();
} else {
failCount.incrementAndGet();
}
// 存储结果
resultQueue.offer(result);
if (resultQueue.size() > 100) {
resultQueue.poll(); // 只保留最近100条
}
log.info("处理结果 - 流程: {}, 状态: {}, 数值: {}", processId, status, value);
// 这里可以添加你的业务逻辑,比如:
// 1. 存入数据库
// 2. 通过WebSocket推送给前端
// 3. 触发其他业务操作
}
// 获取统计信息
public String getStatistics() {
return String.format("成功: %d, 失败: %d, 总数: %d", successCount.get(), failCount.get(), successCount
.get() + failCount.get());
}
// 获取最新结果
public VMResult getLatestResult() {
return resultQueue.peek();
}
@Autowired
private ChannelManager channelManager;
@Scheduled(cron = "*/1 * * * * ?")
public void sendAndWait() {
// 1. 检查连接
log.info("查询时间========");
Channel channel = channelManager.getFirstChannel();
channel.writeAndFlush("001");
}
}

View File

@@ -0,0 +1,21 @@
package top.wms.admin.controller.tcp.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
}

View File

@@ -0,0 +1,165 @@
package top.wms.admin.controller.vm;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.dromara.x.file.storage.core.FileInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.core.validation.CheckUtils;
import top.wms.admin.controller.tcp.config.SimpleRequestMatcher;
import top.wms.admin.controller.tcp.manager.ChannelManager;
import top.wms.admin.system.service.FileService;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@RequestMapping("/vm")
public class VmCommandController {
@Autowired
private ChannelManager channelManager;
@Autowired
private SimpleRequestMatcher requestMatcher;
@Autowired
private FileService fileService;
@GetMapping("/send")
public String sendAndWait(@RequestParam String msg) {
// 1. 检查连接
Channel channel = channelManager.getFirstChannel();
if (channel == null) {
return "ERROR: VM未连接";
}
String sendMsg = msg;
channel.writeAndFlush(sendMsg);
log.info("发送指令: {}", sendMsg);
// 3. 等待响应
String response = requestMatcher.waitForResponse(20);
CheckUtils.throwIf("TIMEOUT".equals(response), "响应超时,请重试");
if (StrUtil.equals(response, "success") || StrUtil.equals(response, "failed")) {
return response;
}
if (StrUtil.equals(response, msg)) {
response = "success";
} else {
response = "failed";
}
// 4. 返回结果
return response; // 直接返回VM的响应
}
// 基础路径
private static final String BASE_PATH = "C:/Users/14725/Desktop/material";
// 固定照片名称
private static final String PHOTO_NAME = "001.bmp";
/**
* 获取最新的001.bmp照片
*
* @return 图片文件
*/
/*@GetMapping("/latest-photo")
public ResponseEntity<byte[]> getLatestPhoto() {
try {
// 获取当前日期
LocalDate now = LocalDate.now();
String yearMonth = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
String day = now.format(DateTimeFormatter.ofPattern("dd"));
// 构建完整的文件路径
// 格式: C:/Users/14725/Desktop/material/202603/20260312/001.bmp
String filePath = String.format("%s/%s/%s%s/%s", BASE_PATH, yearMonth, yearMonth, day, PHOTO_NAME);
// 读取图片文件
Path imagePath = Paths.get(filePath);
// 检查文件是否存在
if (!Files.exists(imagePath)) {
return ResponseEntity.notFound().build();
}
// 读取文件字节
byte[] imageBytes = Files.readAllBytes(imagePath);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("image/bmp"));
headers.setContentLength(imageBytes.length);
// 返回图片
return new ResponseEntity<>(imageBytes, headers, HttpStatus.OK);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}*/
@GetMapping("/latest-photo")
public String getLatestPhoto() {
try {
// 获取当前日期
LocalDate now = LocalDate.now();
String yearMonth = now.format(DateTimeFormatter.ofPattern("yyyyMM"));
String day = now.format(DateTimeFormatter.ofPattern("dd"));
// 构建完整的本地文件路径
// 格式: C:/Users/14725/Desktop/material/202603/20260312/001.bmp
String filePath = String.format("%s/%s/%s%s/%s", BASE_PATH, yearMonth, yearMonth, day, PHOTO_NAME);
// 读取图片文件
Path imagePath = Paths.get(filePath);
// 检查文件是否存在
if (!Files.exists(imagePath)) {
return null; // 或者抛出异常,根据业务需求决定
}
// 将文件转换为MultipartFile
File file = imagePath.toFile();
FileInputStream input = new FileInputStream(file);
MultipartFile multipartFile = new MockMultipartFile(
file.getName(), // 文件名
file.getName(), // 原始文件名
"image/bmp", // 内容类型
input // 文件输入流
);
// 构建MinIO存储路径
String photoStoragePath = "catch" + DateUtil.today() + "/";
// 使用现有的fileService上传到MinIO
FileInfo fileInfo = fileService.upload(multipartFile, photoStoragePath, null, true, true);
// 检查上传结果
CheckUtils.throwIfNull(fileInfo, "照片上传失败");
// 关闭输入流
input.close();
return fileInfo.getUrl();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("处理图片失败: " + e.getMessage());
}
}
}

View File

@@ -1,6 +1,8 @@
package top.wms.admin.controller.weighManage;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import org.springframework.validation.annotation.Validated;
import top.continew.starter.extension.crud.enums.Api;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.log.annotation.Log;
import top.continew.starter.web.model.R;
import top.wms.admin.common.controller.BaseController;
import top.wms.admin.weighManage.model.query.WorkOrderQuery;
import top.wms.admin.weighManage.model.req.WorkOrderReq;
@@ -28,14 +31,46 @@ import java.util.List;
@Tag(name = "任务工单信息管理 API")
@RestController
@RequiredArgsConstructor
@CrudRequestMapping(value = "/weighManage/workOrder", api = {Api.PAGE, Api.ADD, Api.DELETE, Api.EXPORT})
@CrudRequestMapping(value = "/weighManage/workOrder", api = {Api.PAGE, Api.DELETE, Api.EXPORT})
public class WorkOrderController extends BaseController<WorkOrderService, WorkOrderResp, WorkOrderResp, WorkOrderQuery, WorkOrderReq> {
@Log(ignore = true)
@SaCheckPermission("workOrder:record:detail")
@GetMapping(value = "/info/{id}")
public List<WorkOrderInfoResp> getWorkOrderInfos(@PathVariable("id") Long id) {
return baseService.getWorkOrderInfos(id);
}
@Log(ignore = true)
@SaCheckPermission("workOrder:record:detail")
@GetMapping(value = "/{id}")
public List<WorkOrderInfoResp> getDetail(@PathVariable("id") Long id) {
public WorkOrderResp getDetail(@PathVariable("id") Long id) {
return baseService.getDetail(id);
}
@SaCheckPermission("workOrder:record:add")
@PostMapping
public WorkOrderResp addWorKerOrder(@Validated @RequestBody WorkOrderReq req) {
return baseService.addWorKerOrder(req);
}
@PostMapping("/validateWeighing")
public R validateWeighing(@RequestBody WorkOrderReq req) {
if (null == req.getMaterialId()) {
return R.fail("400", "物料主键id不能为空");
}
if (StrUtil.isBlank(req.getInputQuantity())) {
return R.fail("400", "输入数量不能为空");
}
if (StrUtil.isBlank(req.getAhDeviceWeight())) {
return R.fail("400", "电子称重量不能为空");
}
int validateResult = baseService.validateWeighing(req);
if (validateResult != 200) {
return R.fail("500", "重量不匹配!");
}
return R.ok();
}
}

View File

@@ -0,0 +1,326 @@
package top.wms.admin.controller.weighManage.ah;
import com.fazecast.jSerialComm.SerialPort;
import com.fazecast.jSerialComm.SerialPortDataListener;
import com.fazecast.jSerialComm.SerialPortEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.nio.charset.Charset;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 安衡电子秤连接类
*/
@Component
@Slf4j
public class AHDZCConnect {
private static final String PORT_NAME = "COM12";
private static final int BAUD_RATE = 9600;
private static final int DATA_BITS = 8;
private static final int STOP_BITS = 1;
private static final int TIMEOUT = 2000;
private static final Charset SCALE_CHARSET = Charset.forName("GBK");
private static final long HEALTH_CHECK_INTERVAL = 5000;
private static final long DATA_TIMEOUT = 10000;
private static final int MAX_RECONNECT_DELAY = 30000;
private static final int INITIAL_RECONNECT_DELAY = 1000;
private static final int BUFFER_SIZE = 256;
private volatile SerialPort serialPort;
private final AtomicBoolean isConnecting = new AtomicBoolean(false);
private final AtomicBoolean isRunning = new AtomicBoolean(true);
private final AtomicLong lastDataTime = new AtomicLong(System.currentTimeMillis());
private final AtomicInteger reconnectAttempts = new AtomicInteger(0);
private final AtomicInteger currentReconnectDelay = new AtomicInteger(INITIAL_RECONNECT_DELAY);
private final byte[] readBuffer = new byte[BUFFER_SIZE];
private ScheduledExecutorService executorService;
@PostConstruct
public void init() {
// 项目启动时初始化并启动服务
ScaleService();
start();
}
@PreDestroy
public void destroy() {
// 项目关闭时停止服务
stop();
}
public void ScaleService() {
executorService = Executors.newScheduledThreadPool(2, r -> {
Thread t = new Thread(r, "ScaleService-Worker");
t.setDaemon(true);
return t;
});
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "Shutdown-Hook"));
}
public void start() {
log.info("========================================");
log.info("启动电子秤连接线程,串口: " + PORT_NAME + " | 波特率: " + BAUD_RATE);
log.info("========================================");
startHealthCheck();
startConnectionMonitor();
if (connect()) {
log.info("电子秤连接成功!");
} else {
log.info("电子秤连接失败,将自动尝试重连...");
}
}
private boolean connect() {
if (!isRunning.get()) {
return false;
}
if (!isConnecting.compareAndSet(false, true)) {
log.error("[连接] 已有连接任务正在执行,跳过");
return false;
}
try {
closeSerialPort();
SerialPort newPort = SerialPort.getCommPort(PORT_NAME);
newPort.setBaudRate(BAUD_RATE);
newPort.setNumDataBits(DATA_BITS);
newPort.setNumStopBits(STOP_BITS);
newPort.setParity(SerialPort.NO_PARITY);
newPort.setComPortTimeouts(TIMEOUT, TIMEOUT, TIMEOUT);
if (!newPort.openPort()) {
int attempts = reconnectAttempts.incrementAndGet();
int delay = calculateReconnectDelay();
log.info("[连接失败] 无法打开串口 {} (第{}次尝试,下次重试间隔: {}ms)", PORT_NAME, attempts, delay);
return false;
}
serialPort = newPort;
reconnectAttempts.set(0);
currentReconnectDelay.set(INITIAL_RECONNECT_DELAY);
lastDataTime.set(System.currentTimeMillis());
log.info("[连接成功] 串口 {} 已打开", PORT_NAME);
startDataListener();
return true;
} catch (Exception e) {
log.error("[连接异常] {}", e.getMessage());
return false;
} finally {
isConnecting.set(false);
}
}
private int calculateReconnectDelay() {
int delay = currentReconnectDelay.get();
int newDelay = Math.min(delay * 2, MAX_RECONNECT_DELAY);
currentReconnectDelay.set(newDelay);
return newDelay;
}
private void closeSerialPort() {
SerialPort oldPort = serialPort;
serialPort = null;
if (oldPort != null) {
try {
if (oldPort.isOpen()) {
oldPort.closePort();
}
} catch (Exception e) {
log.error("[关闭串口] 异常: {}", e.getMessage());
}
}
}
private void startDataListener() {
SerialPort port = serialPort;
if (port == null || !port.isOpen()) {
return;
}
port.addDataListener(new SerialPortDataListener() {
@Override
public int getListeningEvents() {
return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
}
@Override
public void serialEvent(SerialPortEvent event) {
if (event.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) {
return;
}
try {
SerialPort currentPort = serialPort;
if (currentPort == null) {
return;
}
int bytesAvailable = currentPort.bytesAvailable();
if (bytesAvailable <= 0) {
return;
}
int bytesToRead = Math.min(bytesAvailable, BUFFER_SIZE);
int numRead = currentPort.readBytes(readBuffer, bytesToRead);
if (numRead > 0) {
lastDataTime.set(System.currentTimeMillis());
String data = new String(readBuffer, 0, numRead, SCALE_CHARSET);
// 解析数据,提取数字和重量单位
String weightStr = data.trim();
// 使用正则表达式提取数字(包括小数点)
Pattern pattern = java.util.regex.Pattern.compile("[-+]?\\d*\\.?\\d+");
Matcher matcher = pattern.matcher(weightStr);
if (matcher.find()) {
try {
String weightWithUnit = matcher.group().trim();
log.info("[解析后] 重量: {}", weightWithUnit);
// 发送提取的数字和重量单位给前端
ScaleWebSocketHandler.sendMessage(weightWithUnit);
} catch (NumberFormatException e) {
log.error("[解析异常] 无法解析重量数据: {}", e.getMessage());
// 发送原始数据作为备选
ScaleWebSocketHandler.sendMessage("电子秤异常: " + weightStr);
}
} else {
log.warn("[解析警告] 未找到重量数据: {}", weightStr);
// 发送原始数据作为备选
ScaleWebSocketHandler.sendMessage("电子秤异常: " + weightStr);
}
}
} catch (Exception e) {
log.error("[读取异常] {}", e.getMessage());
triggerReconnect();
}
}
});
}
private void startHealthCheck() {
log.info("[健康检查] 启动,间隔 {} 秒", HEALTH_CHECK_INTERVAL / 1000);
executorService.scheduleAtFixedRate(() -> {
if (!isRunning.get()) {
return;
}
try {
SerialPort port = serialPort;
if (port == null) {
log.error("[健康检查] 串口对象为空");
if (reconnectAttempts.get() < 10) {
connect();
}
return;
}
boolean isOpen = port.isOpen();
// log.error("[健康检查] 串口状态: {}", isOpen ? "已打开" : "已关闭");
if (!isOpen) {
log.info("[健康检查] 检测到串口关闭,尝试重连...");
connect();
}
} catch (Exception e) {
log.error("[健康检查异常] {}", e.getMessage());
}
}, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_INTERVAL, TimeUnit.MILLISECONDS);
}
private void startConnectionMonitor() {
log.info("[连接监控] 启动,间隔 3 秒");
executorService.scheduleAtFixedRate(() -> {
if (!isRunning.get()) {
return;
}
try {
long currentTime = System.currentTimeMillis();
long timeSinceLastData = currentTime - lastDataTime.get();
if (timeSinceLastData > DATA_TIMEOUT) {
SerialPort port = serialPort;
if (port != null && port.isOpen()) {
try {
int bytesAvailable = port.bytesAvailable();
if (bytesAvailable < 0) {
log.info("[连接监控] 检测到连接已断开,尝试重连...");
triggerReconnect();
}
} catch (Exception e) {
log.error("[连接监控] 检测到连接异常: {}", e.getMessage());
triggerReconnect();
}
}
}
} catch (Exception e) {
log.error("[连接监控异常] {}", e.getMessage());
}
}, 3000, 3000, TimeUnit.MILLISECONDS);
}
private void triggerReconnect() {
if (isConnecting.compareAndSet(false, true)) {
isConnecting.set(false);
connect();
}
}
public void stop() {
if (!isRunning.compareAndSet(true, false)) {
return;
}
log.info("========================================");
log.info("停止电子秤线程...");
log.info("========================================");
try {
if (executorService != null && !executorService.isShutdown()) {
executorService.shutdown();
if (!executorService.awaitTermination(3, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
}
} catch (InterruptedException e) {
if (executorService != null) {
executorService.shutdownNow();
}
Thread.currentThread().interrupt();
}
closeSerialPort();
log.info("线程已完全停止");
}
}

View File

@@ -0,0 +1,43 @@
package top.wms.admin.controller.weighManage.ah;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
@Slf4j
public class ScaleWebSocketHandler extends TextWebSocketHandler {
// 线程安全的连接集合
private static final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
log.info("客户端连接成功,当前连接数: {}", sessions.size());
}
@Override
public void afterConnectionClosed(WebSocketSession session,
org.springframework.web.socket.CloseStatus status) throws Exception {
sessions.remove(session);
log.info("客户端断开连接,当前连接数: {}", sessions.size());
}
/**
* 发送数据给所有连接的客户端
*/
public static void sendMessage(String message) {
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(message));
} catch (IOException e) {
log.error("发送消息失败", e);
}
}
}
}
}

View File

@@ -14,7 +14,7 @@ spring.datasource:
# 请务必提前创建好名为 wms_admin 的数据库,如果使用其他数据库名请注意同步修改 DB_NAME 配置
url: jdbc:p6spy:mysql://127.0.0.1:3306/wms?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: root
password: test123$
# PostgreSQL 配置
# url: jdbc:p6spy:mysql://192.168.2.30:${DB_PORT:3306}/continew?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&autoReconnect=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
# username: ${DB_USER:root}
@@ -48,12 +48,12 @@ spring.data:
## Redis 配置(单机模式)
redis:
# 地址
host: ${REDIS_HOST:192.168.2.30}
# host: ${REDIS_HOST:127.0.0.1}
# host: ${REDIS_HOST:192.168.2.30}
host: ${REDIS_HOST:127.0.0.1}
# 端口(默认 6379
port: ${REDIS_PORT:6379}
# 密码(未设置密码时请注释掉)
password: ${REDIS_PWD:redis2025}
# password: ${REDIS_PWD:redis2025}
# 数据库索引
database: ${REDIS_DB:1}
# 连接超时时间

View File

@@ -225,6 +225,7 @@ auth:
- /auth/user/info
- /auth/logout
- /system/user/password
- /ws/scale
--- ### 服务器配置
server: