Compare commits

19 Commits

Author SHA1 Message Date
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
5726239eda 优化用户重置密码加密+物料照片批量导入 2026-03-05 14:39:20 +08:00
656267e6b7 Merge branch 'refs/heads/master' into master_lz
# Conflicts:
#	wms-module-system/src/main/java/top/wms/admin/material/service/MaterialInfoService.java
#	wms-module-system/src/main/java/top/wms/admin/material/service/impl/MaterialInfoServiceImpl.java
2026-03-05 14:27:48 +08:00
zc
381c1638e3 优化追溯 2026-03-04 18:05:00 +08:00
d95f527938 Merge branch 'master' into master_lz 2026-03-04 18:02:30 +08:00
97df87eb06 优化 2026-03-04 18:01:13 +08:00
zc
b98a0f06ba 优化称重 2026-03-03 17:58:57 +08:00
50 changed files with 3279 additions and 224 deletions

View File

@@ -156,4 +156,4 @@
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -19,4 +19,4 @@
<artifactId>wms-common</artifactId> <artifactId>wms-common</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -4,6 +4,8 @@ import top.continew.starter.data.mp.base.BaseMapper;
import top.wms.admin.material.model.entity.MaterialInfoDO; import top.wms.admin.material.model.entity.MaterialInfoDO;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
/** /**
* 物料信息 Mapper * 物料信息 Mapper
* *
@@ -12,5 +14,7 @@ import org.springframework.stereotype.Repository;
*/ */
@Repository @Repository
public interface MaterialInfoMapper extends BaseMapper<MaterialInfoDO> { public interface MaterialInfoMapper extends BaseMapper<MaterialInfoDO> {
public int updateByName(List<MaterialInfoDO> list);
public int updateByCode(List<MaterialInfoDO> list);
} }

View File

@@ -0,0 +1,45 @@
package top.wms.admin.material.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@Schema(description = "物料信息导入行数据")
public class MaterialImportRowReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 物料名称
*/
@Schema(description = "物料名称")
@NotBlank(message = "物料名称不能为空")
@Length(max = 255, message = "物料名称长度不能超过 {max} 个字符")
private String materialName;
/**
* 物料编码
*/
@Schema(description = "物料编码")
@NotBlank(message = "物料编码不能为空")
@Length(max = 255, message = "物料编码长度不能超过 {max} 个字符")
private String encoding;
/**
* 物料单位重量(g)
*/
@Schema(description = "物料单位重量(g)")
private BigDecimal unitWeight;
/*
* 物料规格
* */
@Schema(description = "物料规格")
private String materialSpec;
}

View File

@@ -0,0 +1,47 @@
package top.wms.admin.material.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import top.wms.admin.common.enums.DisEnableStatusEnum;
import top.wms.admin.system.enums.ImportPolicyEnum;
import java.io.Serial;
import java.io.Serializable;
@Data
@Schema(description = "物料导入参数")
public class MaterialInfoImportReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 导入会话KEY
*/
@Schema(description = "导入会话KEY", example = "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed")
@NotBlank(message = "导入已过期,请重新上传")
private String importKey;
/**
* 物料名称重复策略
*/
@Schema(description = "物料名称重复策略", example = "1")
@NotNull(message = "物料名称重复策略不能为空")
private ImportPolicyEnum duplicateName;
/**
* 重复编码策略
*/
@Schema(description = "重复编码策略", example = "1")
@NotNull(message = "重复编码策略不能为空")
private ImportPolicyEnum duplicateCode;
/**
* 默认状态
*/
@Schema(description = "默认状态", example = "1")
private DisEnableStatusEnum defaultStatus;
}

View File

@@ -0,0 +1,55 @@
package top.wms.admin.material.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
/**
* 物料导入解析结果
*
* @author kils
* @since 2024/6/18 14:37
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "用户导入解析结果")
public class MaterialImportParseResp implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 导入会话 Key
*/
@Schema(description = "导入会话Key", example = "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed")
private String importKey;
/**
* 总计行数
*/
@Schema(description = "总计行数", example = "100")
private Integer totalRows;
/**
* 有效行数
*/
@Schema(description = "有效行数", example = "100")
private Integer validRows;
/**
* 重复物料名行数
*/
@Schema(description = "重复物料名行数", example = "100")
private Integer duplicateNameRows;
/**
* 重复物料编码行数
*/
@Schema(description = "重复物料编码行数", example = "100")
private Integer duplicateCodeRows;
}

View File

@@ -0,0 +1,34 @@
package top.wms.admin.material.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Schema(description = "物料信息导入结果")
@AllArgsConstructor
@NoArgsConstructor
public class MaterialInfoImportResp implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总计行数
*/
@Schema(description = "总计行数", example = "100")
private Integer totalRows;
/**
* 新增行数
*/
@Schema(description = "新增行数", example = "100")
private Integer insertRows;
/**
* 修改行数
*/
@Schema(description = "修改行数", example = "100")
private Integer updateRows;
}

View File

@@ -49,7 +49,7 @@ public class MaterialInfoResp extends BaseDetailResp {
*/ */
@Schema(description = "物料规格") @Schema(description = "物料规格")
@ExcelProperty(value = "物料规格") @ExcelProperty(value = "物料规格")
private Double materialSpec; private String materialSpec;
/** /**
* 物料照片地址 * 物料照片地址

View File

@@ -1,11 +1,18 @@
package top.wms.admin.material.service; package top.wms.admin.material.service;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.extension.crud.service.BaseService; import top.continew.starter.extension.crud.service.BaseService;
import top.wms.admin.material.model.entity.MaterialInfoDO; import top.wms.admin.material.model.entity.MaterialInfoDO;
import top.wms.admin.material.model.query.MaterialInfoQuery; import top.wms.admin.material.model.query.MaterialInfoQuery;
import top.wms.admin.material.model.req.MaterialInfoImportReq;
import top.wms.admin.material.model.req.MaterialInfoReq; import top.wms.admin.material.model.req.MaterialInfoReq;
import top.wms.admin.material.model.resp.MaterialImportParseResp;
import top.wms.admin.material.model.resp.MaterialInfoImportResp;
import top.wms.admin.material.model.resp.MaterialInfoResp; import top.wms.admin.material.model.resp.MaterialInfoResp;
import java.io.IOException;
/** /**
* 物料信息业务接口 * 物料信息业务接口
* *
@@ -13,5 +20,45 @@ import top.wms.admin.material.model.resp.MaterialInfoResp;
* @since 2026/02/27 14:19 * @since 2026/02/27 14:19
*/ */
public interface MaterialInfoService extends BaseService<MaterialInfoResp, MaterialInfoResp, MaterialInfoQuery, MaterialInfoReq> { public interface MaterialInfoService extends BaseService<MaterialInfoResp, MaterialInfoResp, MaterialInfoQuery, MaterialInfoReq> {
public MaterialInfoDO getMaterialInfoByCode(String code);
/*
*
* 根据编码查询物料信息
* */
public MaterialInfoDO getMaterialInfoByCode(String code);
/**
* 下载导入模板
*
* @param response 响应对象
* @throws IOException /
*/
void downloadImportTemplate(HttpServletResponse response) throws IOException;
/**
* 解析导入数据
*
* @param file 导入文件
* @return 解析结果
*/
MaterialImportParseResp parseImport(MultipartFile file);
/**
* 导入数据
*
* @param req 导入信息
* @return 导入结果
*/
MaterialInfoImportResp importMaterial(MaterialInfoImportReq req);
/*
* 照片批量上传处理
* */
void uploadMaterialPhotos(MultipartFile file);
/*
* 称重时照片抓取
* */
String catchPhoto(MultipartFile file);
} }

View File

@@ -1,20 +1,63 @@
package top.wms.admin.material.service.impl; package top.wms.admin.material.service.impl;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.*;
import cn.hutool.extra.validation.ValidationUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import jakarta.servlet.http.HttpServletResponse;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.dreamlu.mica.core.result.R;
import org.dromara.x.file.storage.core.FileInfo;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.core.validation.CheckUtils;
import top.continew.starter.extension.crud.service.BaseServiceImpl; import top.continew.starter.extension.crud.service.BaseServiceImpl;
import top.continew.starter.web.util.FileUploadUtils;
import top.wms.admin.common.constant.CacheConstants;
import top.wms.admin.common.context.UserContextHolder;
import top.wms.admin.common.util.SecureUtils;
import top.wms.admin.material.mapper.MaterialInfoMapper; import top.wms.admin.material.mapper.MaterialInfoMapper;
import top.wms.admin.material.model.entity.MaterialInfoDO; import top.wms.admin.material.model.entity.MaterialInfoDO;
import top.wms.admin.material.model.query.MaterialInfoQuery; import top.wms.admin.material.model.query.MaterialInfoQuery;
import top.wms.admin.material.model.req.MaterialImportRowReq;
import top.wms.admin.material.model.req.MaterialInfoImportReq;
import top.wms.admin.material.model.req.MaterialInfoReq; import top.wms.admin.material.model.req.MaterialInfoReq;
import top.wms.admin.material.model.resp.MaterialImportParseResp;
import top.wms.admin.material.model.resp.MaterialInfoImportResp;
import top.wms.admin.material.model.resp.MaterialInfoResp; import top.wms.admin.material.model.resp.MaterialInfoResp;
import top.wms.admin.material.service.MaterialInfoService; import top.wms.admin.material.service.MaterialInfoService;
import top.wms.admin.system.service.FileService;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static top.wms.admin.system.enums.ImportPolicyEnum.*;
import static top.wms.admin.system.enums.ImportPolicyEnum.SKIP;
/** /**
* 物料信息业务实现 * 物料信息业务实现
@@ -24,14 +67,359 @@ import top.wms.admin.material.service.MaterialInfoService;
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper, MaterialInfoDO, MaterialInfoResp, MaterialInfoResp, MaterialInfoQuery, MaterialInfoReq> implements MaterialInfoService { public class MaterialInfoServiceImpl extends BaseServiceImpl<MaterialInfoMapper, MaterialInfoDO, MaterialInfoResp, MaterialInfoResp, MaterialInfoQuery, MaterialInfoReq> implements MaterialInfoService {
private static final Set<String> IMAGE_EXTENSIONS = Set.of("jpg", "jpeg", "png", "gif", "bmp");
private final FileService fileService;
@Override @Override
public MaterialInfoDO getMaterialInfoByCode(String code) { public MaterialInfoDO getMaterialInfoByCode(String code) {
if(StrUtil.isNotBlank(code)){ if (StrUtil.isNotBlank(code)) {
return baseMapper.lambdaQuery().eq(MaterialInfoDO::getEncoding, code).one(); return baseMapper.lambdaQuery().eq(MaterialInfoDO::getEncoding, code).one();
}else{ } else {
return null; return null;
} }
} }
@Override
public void downloadImportTemplate(HttpServletResponse response) throws IOException {
try {
FileUploadUtils.download(response, ResourceUtil
.getStream("templates/import/materialInfo.xlsx"), "物料信息导入模板.xlsx");
} catch (Exception e) {
log.error("下载用户导入模板失败:{}", e);
response.setCharacterEncoding(CharsetUtil.UTF_8);
response.setContentType(ContentType.JSON.toString());
response.getWriter().write(JSONUtil.toJsonStr(R.fail("下载用户导入模板失败")));
}
}
@Override
public MaterialImportParseResp parseImport(MultipartFile file) {
MaterialImportParseResp materialImportResp = new MaterialImportParseResp();
List<MaterialImportRowReq> importRowList;
// 读取表格数据
try {
importRowList = EasyExcel.read(file.getInputStream())
.head(MaterialImportRowReq.class)
.sheet()
.headRowNumber(1)
.doReadSync();
} catch (Exception e) {
log.error("物料信息导入数据文件解析异常:{}", e);
throw new BusinessException("数据文件解析异常");
}
// 总计行数
materialImportResp.setTotalRows(importRowList.size());
CheckUtils.throwIfEmpty(importRowList, "数据文件格式错误");
// 有效行数:过滤无效(同名物料)数据
List<MaterialImportRowReq> validRowList = this.filterImportData(importRowList);
materialImportResp.setValidRows(validRowList.size());
CheckUtils.throwIfEmpty(validRowList, "数据文件格式错误");
// 检测表格内数据是否合法
Set<String> seenCode = new HashSet<>();
boolean hasDuplicateEncoding = validRowList.stream()
.map(MaterialImportRowReq::getEncoding)
.anyMatch(encoding -> encoding != null && !seenCode.add(encoding));
CheckUtils.throwIf(hasDuplicateEncoding, "存在重复物料编码,请检测数据");
// 查询重复用户
materialImportResp
.setDuplicateNameRows(countExistByField(validRowList, MaterialImportRowReq::getMaterialName, MaterialInfoDO::getMaterialName, false));
// 查询重复邮箱
materialImportResp
.setDuplicateCodeRows(countExistByField(validRowList, MaterialImportRowReq::getEncoding, MaterialInfoDO::getEncoding, false));
// 设置导入会话并缓存数据有效期10分钟
String importKey = UUID.fastUUID().toString(true);
RedisUtils.set(CacheConstants.DATA_IMPORT_KEY + importKey, JSONUtil.toJsonStr(validRowList), Duration
.ofMinutes(10));
materialImportResp.setImportKey(importKey);
return materialImportResp;
}
@Override
@Transactional(rollbackFor = Exception.class)
public MaterialInfoImportResp importMaterial(MaterialInfoImportReq req) {
// 校验导入会话是否过期
List<MaterialImportRowReq> importMaterialList;
try {
String data = RedisUtils.get(CacheConstants.DATA_IMPORT_KEY + req.getImportKey());
importMaterialList = JSONUtil.toList(data, MaterialImportRowReq.class);
CheckUtils.throwIf(CollUtil.isEmpty(importMaterialList), "导入已过期,请重新上传");
} catch (Exception e) {
log.error("导入异常:", e);
throw new BusinessException("导入已过期,请重新上传");
}
// 已存在数据查询
List<String> existName = listExistByField(importMaterialList, MaterialImportRowReq::getMaterialName, MaterialInfoDO::getMaterialName);
List<String> existCode = listExistByField(importMaterialList, MaterialImportRowReq::getEncoding, MaterialInfoDO::getEncoding);
CheckUtils.throwIf(isExitImportMaterial(req, importMaterialList, existName, existCode), "数据不符合导入策略,已退出导入");
// 批量操作数据库集合
List<MaterialInfoDO> insertList = new ArrayList<>();
List<MaterialInfoDO> updateByNameList = new ArrayList<>();
List<MaterialInfoDO> updateByCodeList = new ArrayList<>();
// ID生成器
// IdGenerator idGenerator = DefaultIdGeneratorProvider.INSTANCE.getShare();
for (MaterialImportRowReq row : importMaterialList) {
if (isSkipMaterialImport(req, row, existName, existCode)) {
// 按规则跳过该行
continue;
}
MaterialInfoDO materialDO = BeanUtil.toBeanIgnoreError(row, MaterialInfoDO.class);
materialDO.setUnitWeight(NumberUtil.isValidNumber(row.getUnitWeight()) ? row.getUnitWeight() : null);
materialDO.setMaterialSpec(StrUtil.isNotBlank(row.getMaterialSpec()) ? row.getMaterialSpec() : null);
// 修改 or 新增
if (UPDATE.validate(req.getDuplicateName(), row.getMaterialName(), existName)) {
materialDO.setMaterialName(row.getMaterialName());
materialDO.setUpdateTime(LocalDateTime.now());
materialDO.setUpdateUser(UserContextHolder.getUserId());
updateByNameList.add(materialDO);
} else if (UPDATE.validate(req.getDuplicateCode(), row.getEncoding(), existCode)) {
materialDO.setEncoding(row.getEncoding());
materialDO.setUpdateTime(LocalDateTime.now());
materialDO.setUpdateUser(UserContextHolder.getUserId());
updateByCodeList.add(materialDO);
} else {
// materialDO.setId(idGenerator.generate());
insertList.add(materialDO);
}
}
doImportMaterial(insertList, updateByNameList, updateByCodeList);
RedisUtils.delete(CacheConstants.DATA_IMPORT_KEY + req.getImportKey());
int insertCount = insertList.size();
int updateByNameCount = updateByNameList.size();
int updateByCodeCount = updateByCodeList.size();
int totalUpdateCount = updateByNameCount + updateByCodeCount;
int totalHandleCount = insertCount + totalUpdateCount;
return new MaterialInfoImportResp(totalHandleCount, // 总处理数
insertCount, // 新增数
totalUpdateCount // 更新总数
);
}
/**
* 按指定数据集获取数据库已存在的数量
*
* @param materialRowList 导入的数据源
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @return 存在的数量
*/
private int countExistByField(List<MaterialImportRowReq> materialRowList,
Function<MaterialImportRowReq, String> rowField,
SFunction<MaterialInfoDO, ?> dbField,
boolean fieldEncrypt) {
List<String> fieldValues = materialRowList.stream().map(rowField).filter(Objects::nonNull).toList();
if (fieldValues.isEmpty()) {
return 0;
}
return (int)this.count(Wrappers.<MaterialInfoDO>lambdaQuery()
.in(dbField, fieldEncrypt ? SecureUtils.encryptFieldByAes(fieldValues) : fieldValues));
}
/**
* 过滤无效的导入用户数据(批量导入不严格校验数据)
*
* @param importRowList 导入数据
*/
private List<MaterialImportRowReq> filterImportData(List<MaterialImportRowReq> importRowList) {
// 校验过滤
List<MaterialImportRowReq> list = importRowList.stream()
.filter(row -> ValidationUtil.validate(row).isEmpty())
.toList();
// 物料名去重
return list.stream()
.collect(Collectors.toMap(MaterialImportRowReq::getMaterialName, row -> row, (existing,
replacement) -> existing))
.values()
.stream()
.toList();
}
/**
* 按指定数据集获取数据库已存在内容
*
* @param materialRowList 导入的数据源
* @param rowField 导入数据的字段
* @param dbField 对比数据库的字段
* @return 存在的内容
*/
private List<String> listExistByField(List<MaterialImportRowReq> materialRowList,
Function<MaterialImportRowReq, String> rowField,
SFunction<MaterialInfoDO, String> dbField) {
List<String> fieldValues = materialRowList.stream().map(rowField).filter(Objects::nonNull).toList();
if (fieldValues.isEmpty()) {
return Collections.emptyList();
}
List<MaterialInfoDO> materialDOList = baseMapper.selectList(Wrappers.<MaterialInfoDO>lambdaQuery()
.in(dbField, fieldValues)
.select(dbField));
return materialDOList.stream().map(dbField).filter(Objects::nonNull).toList();
}
/**
* 判断是否退出导入
*
* @param req 导入参数
* @param list 导入数据
* @param existName 导入数据中已存在的物料名
* @param existCode 导入数据中已存在的物料编号
* @return 是否退出
*/
private boolean isExitImportMaterial(MaterialInfoImportReq req,
List<MaterialImportRowReq> list,
List<String> existName,
List<String> existCode) {
return list.stream()
.anyMatch(row -> EXIT.validate(req.getDuplicateName(), row.getMaterialName(), existName) || EXIT
.validate(req.getDuplicateCode(), row.getEncoding(), existCode));
}
/**
* 判断是否跳过导入
*
* @param req 导入参数
* @param row 导入数据
* @param existName 导入数据中已存在的物料名称
* @param existCode 导入数据中已存在的物料编号
* @return 是否跳过
*/
private boolean isSkipMaterialImport(MaterialInfoImportReq req,
MaterialImportRowReq row,
List<String> existName,
List<String> existCode) {
return SKIP.validate(req.getDuplicateName(), row.getMaterialName(), existName) || SKIP.validate(req
.getDuplicateCode(), row.getEncoding(), existCode);
}
/**
* 导入用户
*
* @param insertList 新增用户
* @param updateByNameList 修改用户
* @param updateByCodeList 用户角色关联
*/
private void doImportMaterial(List<MaterialInfoDO> insertList,
List<MaterialInfoDO> updateByNameList,
List<MaterialInfoDO> updateByCodeList) {
if (CollUtil.isNotEmpty(insertList)) {
baseMapper.insertBatch(insertList);
}
if (CollUtil.isNotEmpty(updateByNameList)) {
baseMapper.updateByName(updateByNameList);
}
if (CollUtil.isNotEmpty(updateByCodeList)) {
baseMapper.updateByCode(updateByCodeList);
}
}
@Override
public void uploadMaterialPhotos(MultipartFile zipFile) {
// 1. 初始化物料编码-照片地址Map
Map<String, String> codeUrlMap = new HashMap<>();
// 物料照片存储路径(自定义,比如按日期分目录)
String photoStoragePath = "/" + DateUtil.today() + "/";
try (ZipInputStream zipInputStream = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
// 跳过目录、非图片文件
if (zipEntry.isDirectory() || !isImageFile(zipEntry.getName())) {
zipInputStream.closeEntry();
continue;
}
// 2. 提取物料编码(照片名 = 物料编码,去掉后缀)
String fileName = zipEntry.getName();
log.info("正在处理的照片:" + fileName);
//去除windows或linux环境下 可能存在的多层级目录
if (fileName.contains("/")) {
fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
}
if (fileName.contains("\\")) {
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
}
String materialCode = fileName.substring(0, fileName.lastIndexOf("."));
// 3. 读取ZIP中的图片为BufferedImage
BufferedImage image = ImageIO.read(zipInputStream);
if (ObjectUtil.isEmpty(image)) {
log.warn("无法读取图片: {}", fileName);
zipInputStream.closeEntry();
continue;
}
// 4. 将BufferedImage转为字节流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String imageExt = getImageExtension(fileName);
ImageIO.write(image, imageExt, baos);
byte[] imageBytes = baos.toByteArray();
// 5. 转换为MultipartFile使用MockMultipartFile
MultipartFile singleImageFile = new MockMultipartFile("file", fileName, "image/" + (imageExt
.equals("jpg") ? "jpeg" : imageExt), imageBytes);
// 6. 调用upload方法上传图片
FileInfo fileInfo = fileService.upload(singleImageFile, photoStoragePath, null, true, true);
// 7. 将物料编码和图片URL存入Map
codeUrlMap.put(materialCode, fileInfo.getUrl());
zipInputStream.closeEntry();
}
setPhotosByCode(codeUrlMap);
} catch (Exception e) {
throw new BusinessException("照片批量导入失败:" + e.getMessage());
}
}
private boolean isImageFile(String fileName) {
if (fileName == null || !fileName.contains("."))
return false;
String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
return IMAGE_EXTENSIONS.contains(ext);
}
private String getImageExtension(String fileName) {
String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
return "jpeg".equals(ext) ? "jpg" : ext;
}
private void setPhotosByCode(Map<String, String> codeUrlMap) {
CheckUtils.throwIfEmpty(codeUrlMap, "照片为空,请重新上传");
List<MaterialInfoDO> existList = baseMapper.selectList(Wrappers.<MaterialInfoDO>lambdaQuery()
.in(MaterialInfoDO::getEncoding, codeUrlMap.keySet()));
if (existList.isEmpty()) {
log.warn("未找到任何匹配的物料编码");
return;
}
List<MaterialInfoDO> updateList = existList.stream().map(exist -> {
MaterialInfoDO updateDO = new MaterialInfoDO();
updateDO.setId(exist.getId());
updateDO.setPhotoUrl(codeUrlMap.get(exist.getEncoding()));
return updateDO;
}).collect(Collectors.toList());
if (!updateList.isEmpty()) {
baseMapper.updateBatchById(updateList);
log.info("成功更新 {} 个物料的照片", updateList.size());
}
//记录未找到的物料编码(可选,方便排查问题)
Set<String> existCodes = existList.stream().map(MaterialInfoDO::getEncoding).collect(Collectors.toSet());
codeUrlMap.keySet()
.stream()
.filter(code -> !existCodes.contains(code))
.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

@@ -1,18 +0,0 @@
package top.wms.admin.system.mapper;
import org.springframework.stereotype.Repository;
import top.continew.starter.data.mp.base.BaseMapper;
import top.wms.admin.system.model.entity.RuleRelationDO;
/**
* 通行规则-设备关联 Mapper
*
* @author zc
* @since 2025/12/22 17:16
*/
@Repository
public interface RuleRelationMapper extends BaseMapper<RuleRelationDO> {
int deleteByRuleId(RuleRelationDO ruleRelation);
}

View File

@@ -1,37 +0,0 @@
package top.wms.admin.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 通行规则-设备关联实体
*
* @author zc
* @since 2025/12/22 17:16
*/
@Data
@TableName("equipment_rule_relation")
public class RuleRelationDO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 平台侧规则ID关联规则表主键
*/
private Long ruleId;
/**
* 设备ID关联设备表主键
*/
private Long equipmentId;
/**
* 设备侧的规则ID设备本地存储的规则标识
*/
private String equipmentRuleId;
}

View File

@@ -1,52 +0,0 @@
package top.wms.admin.system.model.req;
import jakarta.validation.constraints.*;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import org.hibernate.validator.constraints.Length;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 创建或修改通行规则-设备关联参数
*
* @author zc
* @since 2025/12/22 17:16
*/
@Data
@Schema(description = "创建或修改通行规则-设备关联参数")
public class RuleRelationReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 平台侧规则ID关联规则表主键
*/
@Schema(description = "平台侧规则ID关联规则表主键")
@NotNull(message = "平台侧规则ID关联规则表主键不能为空")
private Long ruleId;
/**
* 设备ID关联设备表主键
*/
@Schema(description = "设备ID关联设备表主键")
@NotNull(message = "设备ID关联设备表主键不能为空")
private Long equipmentId;
/**
* 设备侧的规则ID设备本地存储的规则标识
*/
@Schema(description = "设备侧的规则ID设备本地存储的规则标识")
@NotBlank(message = "设备侧的规则ID设备本地存储的规则标识不能为空")
@Length(max = 64, message = "设备侧的规则ID设备本地存储的规则标识长度不能超过 {max} 个字符")
private String equipmentRuleId;
@Schema(description = "创建时间")
private LocalDateTime createTime;
}

View File

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

View File

@@ -1,23 +0,0 @@
package top.wms.admin.system.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import top.continew.starter.extension.crud.service.BaseServiceImpl;
import top.wms.admin.system.mapper.PeopleEquipmentMapper;
import top.wms.admin.system.model.entity.PeopleEquipmentDO;
import top.wms.admin.system.model.query.PeopleEquipmentQuery;
import top.wms.admin.system.model.req.PeopleEquipmentReq;
import top.wms.admin.system.model.resp.PeopleEquipmentResp;
import top.wms.admin.system.service.PeopleEquipmentService;
/**
* 人员设备下发信息业务实现
*
* @author zc
* @since 2025/03/27 14:59
*/
@Service
@RequiredArgsConstructor
public class PeopleEquipmentServiceImpl extends BaseServiceImpl<PeopleEquipmentMapper, PeopleEquipmentDO, PeopleEquipmentResp, PeopleEquipmentResp, PeopleEquipmentQuery, PeopleEquipmentReq> implements PeopleEquipmentService {}

View File

@@ -36,6 +36,7 @@ import net.dreamlu.mica.core.result.R;
import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.FileStorageService; import org.dromara.x.file.storage.core.FileStorageService;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -131,6 +132,8 @@ public class UserServiceImpl extends BaseServiceImpl<UserMapper, UserDO, UserRes
CheckUtils.throwIf(StrUtil.isNotBlank(email) && this.isEmailExists(email, null), errorMsgTemplate, email); CheckUtils.throwIf(StrUtil.isNotBlank(email) && this.isEmailExists(email, null), errorMsgTemplate, email);
String phone = req.getPhone(); String phone = req.getPhone();
CheckUtils.throwIf(StrUtil.isNotBlank(phone) && this.isPhoneExists(phone, null), errorMsgTemplate, phone); CheckUtils.throwIf(StrUtil.isNotBlank(phone) && this.isPhoneExists(phone, null), errorMsgTemplate, phone);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
req.setPassword("{bcrypt}" + encoder.encode(req.getPassword()));
} }
@Override @Override

View File

@@ -0,0 +1,16 @@
package top.wms.admin.weighManage.mapper;
import top.continew.starter.data.mp.base.BaseMapper;
import org.springframework.stereotype.Repository;
import top.wms.admin.weighManage.model.entity.WorkOrderInfoDO;
/**
* 任务工单详情 Mapper
*
* @author zc
* @since 2026/03/04 14:16
*/
@Repository
public interface WorkOrderInfoMapper extends BaseMapper<WorkOrderInfoDO> {
}

View File

@@ -0,0 +1,43 @@
package top.wms.admin.weighManage.mapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import top.continew.starter.data.mp.base.BaseMapper;
import top.wms.admin.weighManage.model.entity.WorkOrderDO;
import org.springframework.stereotype.Repository;
import top.wms.admin.weighManage.model.resp.WorkOrderInfoResp;
import top.wms.admin.weighManage.model.resp.WorkOrderResp;
import java.util.List;
/**
* 任务工单信息 Mapper
*
* @author zc
* @since 2026/03/03 17:09
*/
@Repository
public interface WorkOrderMapper extends BaseMapper<WorkOrderDO> {
/**
* 获取任务工单分页列表
*
* @param objectPage 分页参数
* @param queryWrapper 查询参数
* @return 任务工单分页列表
*/
IPage<WorkOrderResp> selectWorkOrderPage(@Param("page") Page<Object> objectPage,
@Param(Constants.WRAPPER) QueryWrapper<WorkOrderDO> queryWrapper);
/**
* 获取任务工单详情信息
*
* @param id 任务工单主键id
* @return 任务工单详情信息
*/
List<WorkOrderInfoResp> getDetail(Long id);
}

View File

@@ -0,0 +1,50 @@
package top.wms.admin.weighManage.model.entity;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import top.wms.admin.common.model.entity.BaseDO;
import java.io.Serial;
import java.math.BigDecimal;
/**
* 任务工单信息实体
*
* @author zc
* @since 2026/03/03 17:09
*/
@Data
@TableName("sys_work_order")
public class WorkOrderDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 标题
*/
private String title;
/**
* 任务工单号
*/
private String orderNo;
/**
* 物料主键id
*/
private Long materialId;
/**
* 总重量
*/
private BigDecimal totalWeight;
/**
* 总数量
*/
private Integer totalCount;
}

View File

@@ -0,0 +1,59 @@
package top.wms.admin.weighManage.model.entity;
import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableName;
import top.wms.admin.common.model.entity.BaseDO;
import java.io.Serial;
import java.math.BigDecimal;
/**
* 任务工单详情实体
*
* @author zc
* @since 2026/03/04 14:16
*/
@Data
@TableName("sys_work_order_info")
public class WorkOrderInfoDO extends BaseDO {
@Serial
private static final long serialVersionUID = 1L;
/**
* 工单主键id
*/
private Long workOrderId;
/**
* 物料主键id
*/
private Long materialId;
/**
* 称重次数
*/
private int weightTime;
/**
* 抓拍的图片
*/
private String imgUrl;
/**
* 物料数量
*/
private BigDecimal quantity;
/**
* 称重重量
*/
private BigDecimal weight;
/**
* 计算重量
*/
private BigDecimal calculatedWeight;
}

View File

@@ -0,0 +1,66 @@
package top.wms.admin.weighManage.model.query;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.time.*;
import java.util.Date;
/**
* 任务工单信息查询条件
*
* @author zc
* @since 2026/03/03 17:09
*/
@Data
@Schema(description = "任务工单信息查询条件")
public class WorkOrderQuery implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务工单号
*/
@Schema(description = "任务工单号")
private String orderNo;
/**
* 物料名称
*/
@Schema(description = "物料名称")
private String materialName;
/**
* 物料编码
*/
@Schema(description = "物料编码")
private String encoding;
/**
* 创建人
*/
@Schema(description = "创建人")
private String userName;
/**
* 卡号
*/
@Schema(description = "卡号")
private String carNo;
/**
* 创建开始时间
*/
@Schema(description = "创建开始时间")
private Date startTime;
/**
* 创建结束时间
*/
@Schema(description = "创建结束时间")
private Date endTime;
}

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

@@ -0,0 +1,59 @@
package top.wms.admin.weighManage.model.req;
import jakarta.validation.constraints.*;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 创建或修改任务工单信息参数
*
* @author zc
* @since 2026/03/03 17:09
*/
@Data
@Schema(description = "创建或修改任务工单信息参数")
public class WorkOrderReq implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 物料主键id
*/
@Schema(description = "物料主键id")
@NotNull(message = "物料主键id不能为空")
private Long materialId;
/**
* 物料名称
*/
@Schema(description = "物料名称")
@NotBlank(message = "物料名称不能为空")
private String materialName;
/**
* 称重列表
*/
@Schema(description = "称重列表")
@NotEmpty(message = "称重列表不能为空")
private List<WorkOrderInfoReq> workOrderInfos;
/**
* 手动填写的物料数量
*/
private String inputQuantity;
/**
* 电子秤重量
*/
private String ahDeviceWeight;
}

View File

@@ -1,14 +0,0 @@
package top.wms.admin.weighManage.model.resp;
import lombok.Data;
@Data
public class MaterialResp {
private Long id;
private String materialCode;
private String materialName;
private String materialSpec;
private String weight;
private String imageUrl;
}

View File

@@ -0,0 +1,72 @@
package top.wms.admin.weighManage.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 任务工单详情信息
*
* @author zc
* @since 2026/03/04 14:21
*/
@Data
@Schema(description = "任务工单详情信息")
public class WorkOrderInfoResp {
/**
* 主键id
*/
@Schema(description = "主键id")
private Long id;
/**
* 工单主键id
*/
@Schema(description = "工单主键id")
private Long workOrderId;
/**
* 物料主键id
*/
@Schema(description = "物料主键id")
private Long materialId;
/**
* 物料主键id
*/
@Schema(description = "物料主键id")
private String materialName;
/**
* 称重次数
*/
@Schema(description = "称重次数")
private int weightTime;
/**
* 物料数量
*/
@Schema(description = "物料数量")
private BigDecimal quantity;
/**
* 称重重量
*/
@Schema(description = "称重重量")
private BigDecimal weight;
/**
* 计算重量
*/
@Schema(description = "计算重量")
private BigDecimal calculatedWeight;
/**
* 抓拍的图片
*/
@Schema(description = "抓拍的图片")
private String imgUrl;
}

View File

@@ -0,0 +1,105 @@
package top.wms.admin.weighManage.model.resp;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import top.wms.admin.common.model.resp.BaseDetailResp;
import java.io.Serial;
import java.time.*;
import java.math.BigDecimal;
import java.util.List;
/**
* 任务工单信息信息
*
* @author zc
* @since 2026/03/03 17:09
*/
@Data
@Schema(description = "任务工单信息信息")
public class WorkOrderResp extends BaseDetailResp {
@Serial
private static final long serialVersionUID = 1L;
/**
* 卡号
*/
@Schema(description = "卡号")
private String cardNo;
/**
* 工单编号
*/
@Schema(description = "工单编号")
private String orderNo;
/**
* 标题
*/
@Schema(description = "标题")
private String title;
/**
* 物料主键id
*/
@Schema(description = "物料主键id")
private Long materialId;
/**
* 物料名称
*/
@Schema(description = "物料名称")
private String materialName;
/**
* 物料编码
*/
@Schema(description = "物料编码")
private String encoding;
/**
* 物料单位重量
*/
@Schema(description = "物料单位重量")
private BigDecimal unitWeight;
/**
* 物料规格
*/
@Schema(description = "物料规格")
private String materialSpec;
/**
* 物料图片
*/
@Schema(description = "物料图片")
private String photoUrl;
/**
* 计算总重量(标重)
*/
@Schema(description = "计算总重量(标重)")
private BigDecimal totalCalculatedWeight;
/**
* 实际总重量
*/
@Schema(description = "实际总重量")
private BigDecimal totalWeight;
/**
* 总数量
*/
@Schema(description = "总数量")
private int totalCount;
/**
* 任务工单详情信息列表
*/
@Schema(description = "任务工单详情信息列表")
private List<WorkOrderInfoResp> workOrderInfos;
}

View File

@@ -0,0 +1,49 @@
package top.wms.admin.weighManage.service;
import top.continew.starter.extension.crud.service.BaseService;
import top.wms.admin.weighManage.model.query.WorkOrderQuery;
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 java.util.List;
/**
* 任务工单信息业务接口
*
* @author zc
* @since 2026/03/03 17:09
*/
public interface WorkOrderService extends BaseService<WorkOrderResp, WorkOrderResp, WorkOrderQuery, WorkOrderReq> {
/**
* 获取任务工单详情信息
*
* @param id 任务工单主键id
* @return 任务工单详情信息
*/
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

@@ -0,0 +1,169 @@
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.IdUtil;
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;
/**
* 任务工单信息业务实现
*
* @author zc
* @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());
this.sort(queryWrapper, pageQuery);
IPage<WorkOrderResp> page = baseMapper.selectWorkOrderPage(new Page<>(pageQuery.getPage(), pageQuery
.getSize()), queryWrapper);
return PageResp.build(page);
}
@Override
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

@@ -1,4 +1,52 @@
<?xml version="1.0" encoding="UTF-8" ?> <?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" > <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="top.wms.admin.material.mapper.MaterialInfoMapper"> <mapper namespace="top.wms.admin.material.mapper.MaterialInfoMapper">
<!-- 按物料名称批量更新 -->
<update id="updateByName">
<foreach collection="list" item="item" separator=";">
UPDATE sys_material_info
SET
<if test="item.encoding != null and item.encoding != ''" >
encoding = #{item.encoding},
</if>
<if test="item.unitWeight != null">
unit_weight = #{item.unitWeight},
</if>
<if test="item.materialSpec != null and item.materialSpec != ''">
material_spec = #{item.materialSpec},
</if>
<if test="item.updateTime != null">
update_time = NOW(),
</if>
<if test="item.updateUser != null and item.updateUser != ''">
update_user = #{item.updateUser}
</if>
WHERE material_name = #{item.materialName}
</foreach>
</update>
<update id="updateByCode">
<foreach collection="list" item="item" separator=";">
UPDATE sys_material_info
SET
<if test="item.materialName != null and item.materialName != ''">
material_name = #{item.materialName},
</if>
<if test="item.unitWeight != null">
unit_weight = #{item.unitWeight},
</if>
<if test="item.materialSpec != null and item.materialSpec != ''">
material_spec = #{item.materialSpec},
</if>
<if test="item.updateTime != null">
update_time = NOW(),
</if>
<if test="item.updateUser != null and item.updateUser != ''">
update_user = #{item.updateUser}
</if>
WHERE encoding = #{item.encoding}
</foreach>
</update>
</mapper> </mapper>

View File

@@ -1,21 +0,0 @@
<?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="top.wms.admin.system.mapper.RuleRelationMapper">
<delete id="deleteByRuleId" parameterType="top.wms.admin.system.model.entity.RuleRelationDO">
DELETE FROM equipment_rule_relation
WHERE 1=1
<!-- 判空规则:字段不为 null 且 不为空字符串(兼容字符串/数字类型) -->
<if test="ruleId != null and ruleId != ''">
AND rule_id = #{ruleId}
</if>
<if test="equipmentId != null and equipmentId != ''">
AND equipment_id = #{equipmentId}
</if>
<if test="equipmentRuleId != null and equipmentRuleId != ''">
AND equipment_rule_id = #{equipmentRuleId}
</if>
</delete>
</mapper>

View File

@@ -0,0 +1,29 @@
<?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="top.wms.admin.weighManage.mapper.WorkOrderMapper">
<select id="selectWorkOrderPage" resultType="top.wms.admin.weighManage.model.resp.WorkOrderResp">
select
w.*,
m.material_name,
m.encoding,
m.photo_url,
m.unit_weight,
u.card_no,
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
${ew.customSqlSegment}
</select>
<select id="getDetail" resultType="top.wms.admin.weighManage.model.resp.WorkOrderInfoResp">
select
wi.*,
m.material_name
from sys_work_order_info wi
left join sys_material_info m on wi.material_id = m.id
where wi.work_order_id = #{id}
</select>
</mapper>

View File

@@ -53,6 +53,16 @@
<artifactId>aws-java-sdk-s3</artifactId> <artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.780</version> <version>1.12.780</version>
</dependency> </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>
</dependencies> </dependencies>
<build> <build>
<finalName>${project.parent.name}</finalName> <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> <artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.780</version> <version>1.12.780</version>
</dependency> </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> </dependencies>
<build> <build>

View File

@@ -0,0 +1,25 @@
package top.wms.admin.config.webSocket;
import org.springframework.beans.factory.annotation.Autowired;
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.hkMVS.CameraWebSocketHandler;
import top.wms.admin.controller.weighManage.ah.ScaleWebSocketHandler;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private CameraWebSocketHandler cameraWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 注册WebSocket端点允许所有跨域请求
registry.addHandler(new ScaleWebSocketHandler(), "/ws/scale").setAllowedOrigins("*");
// 注册海康相机1 WebSocket端点
registry.addHandler(cameraWebSocketHandler, "/ws/camera").setAllowedOrigins("*");
}
}

View File

@@ -1,37 +0,0 @@
package top.wms.admin.controller.WeighManage;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import top.wms.admin.weighManage.model.resp.MaterialResp;
/**
* 部门管理管理 API
*
* @author zc
* @since 2025/03/19 17:46
*/
@Tag(name = "部门管理管理 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/weighManage/material")
public class WeighController {
/**
* 获取材料详细信息
*/
@SaCheckPermission("Weigh:material:detail")
@GetMapping(value = "/detail")
public MaterialResp getInfo() {
MaterialResp materialResp = new MaterialResp();
materialResp.setId(1L);
materialResp.setMaterialCode("123");
materialResp.setMaterialName("测试材料");
materialResp.setMaterialSpec("测试规格");
materialResp.setWeight("100");
materialResp.setImageUrl("http://example.com/image.jpg");
return materialResp;
}
}

View File

@@ -0,0 +1,487 @@
package top.wms.admin.controller.hkMVS;
import MvCameraControlWrapper.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.WebSocketSession;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static MvCameraControlWrapper.MvCameraControlDefines.*;
@Slf4j
@Service
public class CameraService {
// 相机句柄
private Handle hCamera = null;
// 设备信息列表
private ArrayList<MV_CC_DEVICE_INFO> stDeviceList = null;
// WebSocket会话映射用于存储活跃的WebSocket连接
private final Map<String, WebSocketSession> webSocketSessions = new ConcurrentHashMap<>();
// 相机是否正在运行
private boolean isRunning = false;
// 帧率控制相关
private long lastFrameTime = 0;
private final int targetFps = 15; // 目标帧率
private final long frameInterval = 1000 / targetFps; // 帧间隔(毫秒)
// 最后一次会话移除时间,用于判断是否需要保留相机资源
private long lastSessionRemoveTime = 0;
// 相机资源保留时间(毫秒)
private final long cameraResourceRetentionTime = 5000; // 5秒
/**
* 初始化相机
* @return 初始化结果
*/
public synchronized boolean initializeCamera() {
if (hCamera != null) {
log.info("Camera already initialized");
return true;
}
int nRet = MV_OK;
try {
// 初始化SDK
log.info("Initializing SDK...");
nRet = MvCameraControl.MV_CC_Initialize();
if (MV_OK != nRet) {
log.error("Initialize SDK fail! nRet {}", Integer.toHexString(nRet));
return false;
}
// 枚举设备
log.info("Enumerating devices...");
try {
stDeviceList = MvCameraControl.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_GIGE_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE);
} catch (CameraControlException e) {
log.error("Enumerate devices failed!", e);
MvCameraControl.MV_CC_Finalize();
return false;
}
if (stDeviceList == null || stDeviceList.size() <= 0) {
log.error("No devices found!");
MvCameraControl.MV_CC_Finalize();
return false;
}
// 打印设备信息
log.info("Found {} device(s):", stDeviceList.size());
int i = 0;
for (MV_CC_DEVICE_INFO stDeviceInfo : stDeviceList) {
if (null == stDeviceInfo) {
continue;
}
}
// 默认选择第一个设备
log.info("Selecting first device...");
try {
hCamera = MvCameraControl.MV_CC_CreateHandle(stDeviceList.get(0));
} catch (CameraControlException e) {
log.error("Create handle failed!", e);
MvCameraControl.MV_CC_Finalize();
return false;
}
// 打开设备
log.info("Opening device...");
nRet = MvCameraControl.MV_CC_OpenDevice(hCamera);
if (MV_OK != nRet) {
log.error("Connect to camera failed, errcode: {}", Integer.toHexString(nRet));
MvCameraControl.MV_CC_DestroyHandle(hCamera);
hCamera = null;
MvCameraControl.MV_CC_Finalize();
return false;
}
// 关闭触发模式
log.info("Turning off trigger mode...");
nRet = MvCameraControl.MV_CC_SetEnumValueByString(hCamera, "TriggerMode", "Off");
if (MV_OK != nRet) {
log.error("SetTriggerMode failed, errcode: {}", Integer.toHexString(nRet));
closeCamera();
return false;
}
// 设置Bayer转换质量用于彩色相机
log.info("Setting Bayer convert quality...");
nRet = MvCameraControl.MV_CC_SetBayerCvtQuality(hCamera, 1);
if (MV_OK != nRet) {
log.error("Set Bayer convert quality fail! nRet {}", Integer.toHexString(nRet));
closeCamera();
return false;
}
log.info("Camera initialized successfully");
return true;
} catch (Exception e) {
log.error("Initialize camera failed", e);
closeCamera();
return false;
}
}
/**
* 开始视频流
* @return 开始结果
*/
public synchronized boolean startStream() {
if (hCamera == null) {
log.error("Camera not initialized");
return false;
}
if (isRunning) {
log.info("Stream already running");
return true;
}
try {
// 注册图像回调
log.info("Registering image callback...");
int nRet = MvCameraControl.MV_CC_RegisterImageCallBack(hCamera, new CameraImageCallBack() {
@Override
public int OnImageCallBack(byte[] bytes, MV_FRAME_OUT_INFO mv_frame_out_info) {
processImage(bytes, mv_frame_out_info);
// processBlackWhiteImage(bytes, mv_frame_out_info);
return 0;
}
});
if (MV_OK != nRet) {
log.error("Register image callback failed, errcode: {}", Integer.toHexString(nRet));
return false;
}
// 开始采集
log.info("Starting grabbing...");
nRet = MvCameraControl.MV_CC_StartGrabbing(hCamera);
if (MV_OK != nRet) {
log.error("StartGrabbing failed, errcode: {}", Integer.toHexString(nRet));
return false;
}
isRunning = true;
log.info("Stream started successfully");
return true;
} catch (Exception e) {
log.error("Start stream failed", e);
return false;
}
}
/**
* 停止视频流
*/
public synchronized void stopStream() {
if (hCamera == null || !isRunning) {
return;
}
try {
// 停止采集
log.info("Stopping grabbing...");
int nRet = MvCameraControl.MV_CC_StopGrabbing(hCamera);
if (MV_OK != nRet) {
log.error("StopGrabbing failed, errcode: {}", Integer.toHexString(nRet));
}
// close device
nRet = MvCameraControl.MV_CC_CloseDevice(hCamera);
if (MV_OK != nRet) {
log.error("CloseDevice failed, errcode: {}", Integer.toHexString(nRet));
}
if (null != hCamera) {
// Destroy handle
nRet = MvCameraControl.MV_CC_DestroyHandle(hCamera);
if (MV_OK != nRet) {
log.error("DestroyHandle failed, errcode: {}", Integer.toHexString(nRet));
}
hCamera = null;
}
MvCameraControl.MV_CC_Finalize();
isRunning = false;
log.info("Stream stopped");
} catch (Exception e) {
log.error("Stop stream failed", e);
}
}
/**
* 关闭相机
*/
public synchronized void closeCamera() {
if (hCamera != null) {
try {
// 停止采集
log.info("Stopping grabbing...");
MvCameraControl.MV_CC_StopGrabbing(hCamera);
// 关闭设备
log.info("Closing device...");
MvCameraControl.MV_CC_CloseDevice(hCamera);
// 销毁句柄
log.info("Destroying handle...");
MvCameraControl.MV_CC_DestroyHandle(hCamera);
} catch (Exception e) {
log.error("Close camera failed", e);
} finally {
hCamera = null;
}
}
// 释放SDK
try {
log.info("Finalizing SDK...");
MvCameraControl.MV_CC_Finalize();
} catch (Exception e) {
log.error("Finalize SDK failed", e);
}
isRunning = false;
log.info("Camera closed");
}
/**
* 处理图像数据
*
* @param bytes 原始图像数据
* @param frameInfo 帧信息
*/
private void processImage(byte[] bytes, MV_FRAME_OUT_INFO frameInfo) {
if (bytes == null || frameInfo == null || webSocketSessions.isEmpty()) {
return;
}
try {
// 帧率控制
long currentTime = System.currentTimeMillis();
if (currentTime - lastFrameTime < frameInterval) {
return; // 跳过当前帧,控制帧率
}
lastFrameTime = currentTime;
int dataSizeForRGB = frameInfo.width * frameInfo.height * 3; // RGB每个像素3字节
byte[] pDataForRGB = new byte[dataSizeForRGB];
// 初始化像素转换参数
MV_CC_PIXEL_CONVERT_PARAM_EX stConvertParam = new MV_CC_PIXEL_CONVERT_PARAM_EX();
// 使用实际的width和height避免ExtendHeight为0的问题
stConvertParam.width = frameInfo.width;
stConvertParam.height = frameInfo.height;
stConvertParam.srcData = bytes;
stConvertParam.srcDataLen = frameInfo.frameLen;
stConvertParam.srcPixelType = frameInfo.pixelType;
stConvertParam.dstPixelType = MvGvspPixelType.PixelType_Gvsp_RGB8_Packed;
stConvertParam.dstBuffer = pDataForRGB;
stConvertParam.dstBufferSize = dataSizeForRGB;
// 尝试进行像素格式转换
int nRet = MvCameraControl.MV_CC_ConvertPixelTypeEx(hCamera, stConvertParam);
if (MV_OK != nRet) {
log.error("Convert PixelType fail, errCode: {}, srcPixelType: {}, width: {}, height: {}",
Integer.toHexString(nRet),
frameInfo.pixelType.getnValue(),
frameInfo.width,
frameInfo.height);
return;
}
// 将RGB数据转换为BufferedImage
BufferedImage image = new BufferedImage(frameInfo.width, frameInfo.height, BufferedImage.TYPE_3BYTE_BGR);
image.getRaster().setDataElements(0, 0, frameInfo.width, frameInfo.height, pDataForRGB);
// 压缩图像为JPEG格式
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "jpeg", baos);
byte[] compressedData = baos.toByteArray();
baos.close();
log.debug("Original size: {} bytes, Converted size: {} bytes, Compressed size: {} bytes, Compression ratio: {:.2f}%",
bytes.length, pDataForRGB.length, compressedData.length, (1 - (double) compressedData.length / bytes.length) * 100);
// 创建包含图像尺寸信息和压缩数据的消息
// 消息格式: [width(4 bytes)][height(4 bytes)][image data]
byte[] message = new byte[8 + compressedData.length];
// 写入宽度和高度(使用小端序)
message[0] = (byte) (frameInfo.width & 0xFF);
message[1] = (byte) ((frameInfo.width >> 8) & 0xFF);
message[2] = (byte) ((frameInfo.width >> 16) & 0xFF);
message[3] = (byte) ((frameInfo.width >> 24) & 0xFF);
message[4] = (byte) (frameInfo.height & 0xFF);
message[5] = (byte) ((frameInfo.height >> 8) & 0xFF);
message[6] = (byte) ((frameInfo.height >> 16) & 0xFF);
message[7] = (byte) ((frameInfo.height >> 24) & 0xFF);
// 写入压缩后的图像数据
System.arraycopy(compressedData, 0, message, 8, compressedData.length);
// 发送图像数据到所有连接的WebSocket客户端
for (WebSocketSession session : webSocketSessions.values()) {
if (session.isOpen()) {
try {
session.sendMessage(new BinaryMessage(message));
} catch (Exception e) {
log.error("Send message to WebSocket failed", e);
}
}
}
} catch (Exception e) {
log.error("Process image failed", e);
}
}
/**
* 处理黑白相机图像数据
* @param bytes 原始图像数据
* @param frameInfo 帧信息
*/
private void processBlackWhiteImage(byte[] bytes, MV_FRAME_OUT_INFO frameInfo) {
if (bytes == null || frameInfo == null || webSocketSessions.isEmpty()) {
return;
}
try {
// 帧率控制
long currentTime = System.currentTimeMillis();
if (currentTime - lastFrameTime < frameInterval) {
return; // 跳过当前帧,控制帧率
}
lastFrameTime = currentTime;
// 对于黑白相机MV-CE050-30GM数据是单通道灰度格式
// 将原始灰度数据转换为BufferedImage
BufferedImage image = new BufferedImage(frameInfo.width, frameInfo.height, BufferedImage.TYPE_BYTE_GRAY);
image.getRaster().setDataElements(0, 0, frameInfo.width, frameInfo.height, bytes);
// 压缩图像为JPEG格式
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "jpeg", baos);
byte[] compressedData = baos.toByteArray();
baos.close();
log.debug("Original size: {} bytes, Compressed size: {} bytes, Compression ratio: {:.2f}%",
bytes.length, compressedData.length, (1 - (double)compressedData.length / bytes.length) * 100);
// 创建包含图像尺寸信息和压缩数据的消息
// 消息格式: [width(4 bytes)][height(4 bytes)][image data]
byte[] message = new byte[8 + compressedData.length];
// 写入宽度和高度(使用小端序)
message[0] = (byte) (frameInfo.width & 0xFF);
message[1] = (byte) ((frameInfo.width >> 8) & 0xFF);
message[2] = (byte) ((frameInfo.width >> 16) & 0xFF);
message[3] = (byte) ((frameInfo.width >> 24) & 0xFF);
message[4] = (byte) (frameInfo.height & 0xFF);
message[5] = (byte) ((frameInfo.height >> 8) & 0xFF);
message[6] = (byte) ((frameInfo.height >> 16) & 0xFF);
message[7] = (byte) ((frameInfo.height >> 24) & 0xFF);
// 写入压缩后的图像数据
System.arraycopy(compressedData, 0, message, 8, compressedData.length);
// 发送图像数据到所有连接的WebSocket客户端
for (WebSocketSession session : webSocketSessions.values()) {
if (session.isOpen()) {
try {
session.sendMessage(new BinaryMessage(message));
} catch (Exception e) {
log.error("Send message to WebSocket failed", e);
}
}
}
} catch (Exception e) {
log.error("Process image failed", e);
}
}
/**
* 添加WebSocket会话
* @param session WebSocket会话
*/
public void addWebSocketSession(WebSocketSession session) {
if (session != null) {
webSocketSessions.put(session.getId(), session);
log.info("WebSocket session added: {}", session.getId());
// 确保相机已初始化
if (!initializeCamera()) {
log.error("Failed to initialize camera for new WebSocket session");
return;
}
// 确保视频流已开始
if (!isRunning) {
boolean started = startStream();
if (!started) {
log.error("Failed to start stream for new WebSocket session");
}
} else {
log.info("Stream already running, no need to start again");
}
}
}
/**
* 移除WebSocket会话
* @param session WebSocket会话
*/
public void removeWebSocketSession(WebSocketSession session) {
if (session != null) {
webSocketSessions.remove(session.getId());
log.info("WebSocket session removed: {}", session.getId());
// 如果没有活跃的WebSocket会话记录移除时间
if (webSocketSessions.isEmpty()) {
lastSessionRemoveTime = System.currentTimeMillis();
// 启动一个线程,延迟停止视频流,以便页面刷新时能够快速恢复
new Thread(() -> {
try {
Thread.sleep(cameraResourceRetentionTime);
// 再次检查是否仍然没有活跃会话
if (webSocketSessions.isEmpty()) {
stopStream();
log.info("No active sessions after retention period, stopped stream");
}
} catch (InterruptedException e) {
log.error("Interrupted while waiting to stop stream", e);
}
}).start();
}
}
}
/**
* 获取当前连接的WebSocket会话数量
* @return 会话数量
*/
public int getWebSocketSessionCount() {
return webSocketSessions.size();
}
/**
* 检查相机是否正在运行
* @return 是否正在运行
*/
public boolean isRunning() {
return isRunning;
}
}

View File

@@ -0,0 +1,64 @@
package top.wms.admin.controller.hkMVS;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.BinaryWebSocketHandler;
@Slf4j
@Component
public class CameraWebSocketHandler extends BinaryWebSocketHandler {
@Resource
private CameraService cameraService;
/**
* 连接建立时触发
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("WebSocket connection established: {}", session.getId());
// 添加WebSocket会话到CameraService
cameraService.addWebSocketSession(session);
}
/**
* 连接关闭时触发
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("WebSocket connection closed: {}, status: {}", session.getId(), status);
// 从CameraService移除WebSocket会话
cameraService.removeWebSocketSession(session);
}
/**
* 处理二进制消息
*/
@Override
protected void handleBinaryMessage(WebSocketSession session, org.springframework.web.socket.BinaryMessage message) throws Exception {
// 可以在这里处理前端发送的二进制消息(如果需要)
log.debug("Received binary message from WebSocket session: {}", session.getId());
}
/**
* 处理文本消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, org.springframework.web.socket.TextMessage message) {
// 可以在这里处理前端发送的文本消息(如果需要)
log.debug("Received text message from WebSocket session: {}, message: {}", session.getId(), message.getPayload());
}
/**
* 处理传输错误
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("WebSocket transport error: {}", exception.getMessage(), exception);
// 从CameraService移除WebSocket会话
cameraService.removeWebSocketSession(session);
}
}

View File

@@ -0,0 +1,370 @@
package top.wms.admin.controller.hkMVS; /***************************************************************************************************
* @file ConvertPixelType.java
* @breif Use functions provided in MvCameraControlWrapper.jar to convert pixel type。
* @author zhanglei72
* @date 2020/02/10
*
* @warning
* @version V1.0.0 2020/02/10 create this file.
* @since 2020/02/10
**************************************************************************************************/
import MvCameraControlWrapper.CameraControlException;
import MvCameraControlWrapper.MvCameraControl;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Scanner;
import static MvCameraControlWrapper.MvCameraControlDefines.*;
public class ConvertPixelType
{
public static Scanner scanner;
private static void printDeviceInfo(MV_CC_DEVICE_INFO stDeviceInfo)
{
if (null == stDeviceInfo) {
System.out.println("stDeviceInfo is null");
return;
}
if ((stDeviceInfo.transportLayerType == MV_GIGE_DEVICE) ||( stDeviceInfo.transportLayerType == MV_GENTL_GIGE_DEVICE))
{
System.out.println("\tCurrentIp: " + stDeviceInfo.gigEInfo.currentIp);
System.out.println("\tModel: " + stDeviceInfo.gigEInfo.modelName);
System.out.println("\tUserDefinedName: " + stDeviceInfo.gigEInfo.userDefinedName);
} else if (stDeviceInfo.transportLayerType == MV_USB_DEVICE) {
System.out.println("\tUserDefinedName: " + stDeviceInfo.usb3VInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.usb3VInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.usb3VInfo.deviceNumber);
} else if (stDeviceInfo.transportLayerType == MV_GENTL_CAMERALINK_DEVICE)
{
System.out.println("\tUserDefinedName: " + stDeviceInfo.cmlInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.cmlInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.cmlInfo.DeviceID);
}
else if (stDeviceInfo.transportLayerType == MV_GENTL_CXP_DEVICE)
{
System.out.println("\tUserDefinedName: " + stDeviceInfo.cxpInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.cxpInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.cxpInfo.DeviceID);
}
else if (stDeviceInfo.transportLayerType == MV_GENTL_XOF_DEVICE)
{
System.out.println("\tUserDefinedName: " + stDeviceInfo.xofInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.xofInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.xofInfo.DeviceID);
}else {
System.err.print("Device is not supported! \n");
}
System.out.println("\tAccessible: "
+ MvCameraControl.MV_CC_IsDeviceAccessible(stDeviceInfo, MV_ACCESS_Exclusive));
System.out.println("");
}
private static void printFrameInfo(MV_FRAME_OUT_INFO stFrameInfo)
{
if (null == stFrameInfo)
{
System.err.println("stFrameInfo is null");
return;
}
StringBuilder frameInfo = new StringBuilder("");
frameInfo.append(("\tFrameNum[" + stFrameInfo.frameNum + "]"));
frameInfo.append("\tWidth[" + stFrameInfo.width + "]");
frameInfo.append("\tHeight[" + stFrameInfo.height + "]");
frameInfo.append(String.format("\tPixelType[%#x]", stFrameInfo.pixelType.getnValue()));
System.out.println(frameInfo.toString());
}
public static void saveDataToFile(byte[] dataToSave, int dataSize, String fileName)
{
OutputStream os = null;
try
{
if((null == dataToSave)||(dataSize <= 0))
{
System.out.println("saveDataToFile param error.");
return;
}
// create diractory
File tempFile = new File("dat");
if (!tempFile.exists())
{
tempFile.mkdirs();
}
os = new FileOutputStream(tempFile.getPath() + File.separator + fileName);
if(null != os)
{
os.write(dataToSave, 0, dataSize);
System.out.println("ConvertPixelType succeed.");
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally
{
// close file stream
try
{
if(os != null)
{
os.close();
}
} catch (IOException e)
{
e.printStackTrace();
}
}
}
public static int chooseCamera(ArrayList<MV_CC_DEVICE_INFO> stDeviceList)
{
if (null == stDeviceList)
{
return -1;
}
// Choose a device to operate
int camIndex = -1;
while (true) {
try {
camIndex = 0;
if ((camIndex >= 0 && camIndex < stDeviceList.size()) || -1 == camIndex) {
break;
} else {
System.out.println("Input error: " + camIndex + " Over Range:( 0 - " + (stDeviceList.size() - 1) + " )");
}
} catch (Exception e) {
System.out.println("Input not number.");
camIndex = -1;
break;
}
}
if (-1 == camIndex) {
System.out.println("Input error.exit");
return camIndex;
}
if (0 <= camIndex && stDeviceList.size() > camIndex)
{
if ((MV_GIGE_DEVICE == stDeviceList.get(camIndex).transportLayerType)||(MV_GENTL_GIGE_DEVICE == stDeviceList.get(camIndex).transportLayerType))
{
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).gigEInfo.userDefinedName);
}
else if (MV_USB_DEVICE == stDeviceList.get(camIndex).transportLayerType)
{
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).usb3VInfo.userDefinedName);
}
else if (MV_GENTL_CAMERALINK_DEVICE == stDeviceList.get(camIndex).transportLayerType)
{
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).cmlInfo.DeviceID);
}
else if (MV_GENTL_CXP_DEVICE == stDeviceList.get(camIndex).transportLayerType)
{
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).cxpInfo.DeviceID);
}
else if (MV_GENTL_XOF_DEVICE == stDeviceList.get(camIndex).transportLayerType)
{
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).xofInfo.DeviceID);
}
else
{
System.out.println("Device is not supported.");
}
}
else
{
System.out.println("Invalid index " + camIndex);
camIndex = -1;
}
return camIndex;
}
public static void main(String[] args)
{
int nRet = MV_OK;
int camIndex = -1;
Handle hCamera = null;
scanner = new Scanner(System.in);
ArrayList<MV_CC_DEVICE_INFO> stDeviceList = null;
do
{
System.out.println("SDK Version " + MvCameraControl.MV_CC_GetSDKVersion());
// Initialize SDK
nRet = MvCameraControl.MV_CC_Initialize();
if (MV_OK != nRet)
{
System.err.printf("Initialize SDK fail! nRet [0x%x]\n\n",nRet);
break;
}
// Enumerate GigE and USB devices
try
{
stDeviceList = MvCameraControl.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_GIGE_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE);
if (0 >= stDeviceList.size())
{
System.out.println("No devices found!");
break;
}
int i = 0;
for (MV_CC_DEVICE_INFO stDeviceInfo : stDeviceList)
{
System.out.println("[camera " + (i++) + "]");
printDeviceInfo(stDeviceInfo);
}
}
catch (CameraControlException e)
{
System.err.println("Enumrate devices failed!" + e.toString());
e.printStackTrace();
break;
}
// choose camera
camIndex = chooseCamera(stDeviceList);
if (camIndex == -1)
{
break;
}
// Create device handle
try
{
hCamera = MvCameraControl.MV_CC_CreateHandle(stDeviceList.get(camIndex));
}
catch (CameraControlException e)
{
System.err.println("Create handle failed!" + e.toString());
e.printStackTrace();
hCamera = null;
break;
}
// Open selected device
nRet = MvCameraControl.MV_CC_OpenDevice(hCamera);
if (MV_OK != nRet)
{
System.err.printf("Connect to camera failed, errcode: [%#x]\n", nRet);
break;
}
// make sure that Trigger mode is off
nRet = MvCameraControl.MV_CC_SetEnumValueByString(hCamera, "TriggerMode", "Off");
if (MV_OK != nRet)
{
System.err.printf("SetTriggerMode failed, errcode: [%#x]\n", nRet);
break;
}
// Get payload size
MVCC_INTVALUE stParam = new MVCC_INTVALUE();
nRet = MvCameraControl.MV_CC_GetIntValue(hCamera, "PayloadSize", stParam);
if (MV_OK != nRet)
{
System.err.printf("Get PayloadSize fail, errcode: [%#x]\n", nRet);
break;
}
// Start grabbing images
nRet = MvCameraControl.MV_CC_StartGrabbing(hCamera);
if (MV_OK != nRet)
{
System.err.printf("Start Grabbing fail, errcode: [%#x]\n", nRet);
break;
}
// Get one frame
MV_FRAME_OUT_INFO stImageInfo = new MV_FRAME_OUT_INFO();
byte[] pData = new byte[(int)stParam.curValue];
nRet = MvCameraControl.MV_CC_GetOneFrameTimeout(hCamera, pData, stImageInfo, 1000);
if (MV_OK != nRet)
{
System.err.printf("GetOneFrameTimeout fail, errcode:[%#x]\n", nRet);
break;
}
System.out.println("GetOneFrame: ");
printFrameInfo(stImageInfo);
// set interpolation algorithm type, 0-Fast 1-Equilibrium 2-Optimal 3-Optimal plus
nRet = MvCameraControl.MV_CC_SetBayerCvtQuality(hCamera, 1);
if (MV_OK != nRet)
{
System.err.printf("set Bayer convert quality fail! nRet [0x%x]\n", nRet);
break;
}
System.out.println("临时日志: frameInfo.width = " + stImageInfo.width + ", frameInfo.height = " + stImageInfo.height + ", frameInfo.ExtendWidth = " + stImageInfo.ExtendWidth + ", frameInfo.ExtendHeight = " + stImageInfo.ExtendHeight + ", frameInfo.frameLen = " + stImageInfo.frameLen + ", frameInfo.pixelType = " + stImageInfo.pixelType);
int dataSizeForRGB = stImageInfo.width * stImageInfo.height * 3; // every RGB pixel takes 3 bytes
byte[] pDataForRGB = new byte[dataSizeForRGB];
// Convert pixel type to RGB8_Packed
MV_CC_PIXEL_CONVERT_PARAM_EX stConvertParam = new MV_CC_PIXEL_CONVERT_PARAM_EX();
stConvertParam.width = stImageInfo.ExtendWidth; // image width
stConvertParam.height = stImageInfo.ExtendHeight; // image height
stConvertParam.srcData = pData; // input buffer
stConvertParam.srcDataLen = stImageInfo.frameLen; // input buffer size
stConvertParam.srcPixelType = stImageInfo.pixelType; // input pixel format
stConvertParam.dstPixelType = MvGvspPixelType.PixelType_Gvsp_RGB8_Packed; // output pixel format
stConvertParam.dstBuffer = pDataForRGB; // output buffer
stConvertParam.dstBufferSize = dataSizeForRGB; // output buffer size
nRet = MvCameraControl.MV_CC_ConvertPixelTypeEx(hCamera, stConvertParam);
if (MV_OK != nRet)
{
System.err.printf("Convert PixelType fail, errcode: [%#x]\n", nRet);
break;
}
// Save buffer content to file
saveDataToFile(pDataForRGB, dataSizeForRGB, "AfterConvert_RGB.raw");
// Stop grabbing
nRet = MvCameraControl.MV_CC_StopGrabbing(hCamera);
if (MV_OK != nRet)
{
System.err.printf("StopGrabbing failed, errcode: [%#x]\n", nRet);
break;
}
// close device
nRet = MvCameraControl.MV_CC_CloseDevice(hCamera);
if (MV_OK != nRet)
{
System.err.printf("CloseDevice failed, errcode: [%#x]\n", nRet);
break;
}
} while (false);
if (null != hCamera)
{
// Destroy handle
nRet = MvCameraControl.MV_CC_DestroyHandle(hCamera);
if (MV_OK != nRet) {
System.err.printf("DestroyHandle failed, errcode: [%#x]\n", nRet);
}
}
MvCameraControl.MV_CC_Finalize();
scanner.close();
}
}

View File

@@ -0,0 +1,264 @@
package top.wms.admin.controller.hkMVS; /***************************************************************************************************
* @file Grab_Callback.java
* @breif Use functions provided in MvCameraControlWrapper.jar to grab images
* @author zhanglei72
* @date 2020/01/12
*
* @warning
* @version V1.0.0 2020/01/12 Create this file
* V1.0.1 2020/02/10 add parameter checking
* @since 2020/02/10
**************************************************************************************************/
import MvCameraControlWrapper.*;
import java.util.ArrayList;
import java.util.Scanner;
import static MvCameraControlWrapper.MvCameraControlDefines.*;
public class Grab_Callback {
public static Scanner scanner;
private static void printDeviceInfo(MV_CC_DEVICE_INFO stDeviceInfo) {
if (null == stDeviceInfo) {
System.out.println("stDeviceInfo is null");
return;
}
if ((stDeviceInfo.transportLayerType == MV_GIGE_DEVICE) || (stDeviceInfo.transportLayerType == MV_GENTL_GIGE_DEVICE)) {
System.out.println("\tCurrentIp: " + stDeviceInfo.gigEInfo.currentIp);
System.out.println("\tModel: " + stDeviceInfo.gigEInfo.modelName);
System.out.println("\tUserDefinedName: " + stDeviceInfo.gigEInfo.userDefinedName);
} else if (stDeviceInfo.transportLayerType == MV_USB_DEVICE) {
System.out.println("\tUserDefinedName: " + stDeviceInfo.usb3VInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.usb3VInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.usb3VInfo.deviceNumber);
} else if (stDeviceInfo.transportLayerType == MV_GENTL_CAMERALINK_DEVICE) {
System.out.println("\tUserDefinedName: " + stDeviceInfo.cmlInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.cmlInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.cmlInfo.DeviceID);
} else if (stDeviceInfo.transportLayerType == MV_GENTL_CXP_DEVICE) {
System.out.println("\tUserDefinedName: " + stDeviceInfo.cxpInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.cxpInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.cxpInfo.DeviceID);
} else if (stDeviceInfo.transportLayerType == MV_GENTL_XOF_DEVICE) {
System.out.println("\tUserDefinedName: " + stDeviceInfo.xofInfo.userDefinedName);
System.out.println("\tSerial Number: " + stDeviceInfo.xofInfo.serialNumber);
System.out.println("\tDevice Number: " + stDeviceInfo.xofInfo.DeviceID);
} else {
System.err.print("Device is not supported! \n");
}
System.out.println("\tAccessible: "
+ MvCameraControl.MV_CC_IsDeviceAccessible(stDeviceInfo, MV_ACCESS_Exclusive));
System.out.println("");
}
private static void printFrameInfo(MV_FRAME_OUT_INFO stFrameInfo) {
if (null == stFrameInfo) {
System.err.println("stFrameInfo is null");
return;
}
StringBuilder frameInfo = new StringBuilder("");
frameInfo.append(("\tFrameNum[" + stFrameInfo.frameNum + "]"));
frameInfo.append("\tWidth[" + stFrameInfo.width + "]");
frameInfo.append("\tHeight[" + stFrameInfo.height + "]");
frameInfo.append(String.format("\tPixelType[%#x]", stFrameInfo.pixelType.getnValue()));
System.out.println(frameInfo.toString());
}
public static int chooseCamera(ArrayList<MV_CC_DEVICE_INFO> stDeviceList) {
if (null == stDeviceList) {
return -1;
}
// Choose a device to operate
int camIndex = -1;
while (true) {
System.out.print("Please input camera index:");
if (scanner.hasNextInt()) {
try {
camIndex = scanner.nextInt();
if ((camIndex >= 0 && camIndex < stDeviceList.size()) || -1 == camIndex) {
break;
} else {
System.out.println("Input error: " + camIndex + " Over Range:( 0 - " + (stDeviceList.size() - 1) + " )");
}
} catch (NumberFormatException e) {
System.out.println("Input not number.");
camIndex = -1;
break;
}
} else {
camIndex = -1;
break;
}
}
if (-1 == camIndex) {
System.out.println("Input error.exit");
return camIndex;
}
if (0 <= camIndex && stDeviceList.size() > camIndex) {
if ((MV_GIGE_DEVICE == stDeviceList.get(camIndex).transportLayerType) || (MV_GENTL_GIGE_DEVICE == stDeviceList.get(camIndex).transportLayerType)) {
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).gigEInfo.userDefinedName);
} else if (MV_USB_DEVICE == stDeviceList.get(camIndex).transportLayerType) {
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).usb3VInfo.userDefinedName);
} else if (MV_GENTL_CAMERALINK_DEVICE == stDeviceList.get(camIndex).transportLayerType) {
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).cmlInfo.DeviceID);
} else if (MV_GENTL_CXP_DEVICE == stDeviceList.get(camIndex).transportLayerType) {
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).cxpInfo.DeviceID);
} else if (MV_GENTL_XOF_DEVICE == stDeviceList.get(camIndex).transportLayerType) {
System.out.println("Connect to camera[" + camIndex + "]: " + stDeviceList.get(camIndex).xofInfo.DeviceID);
} else {
System.out.println("Device is not supported.");
}
} else {
System.out.println("Invalid index " + camIndex);
camIndex = -1;
}
return camIndex;
}
public static void main(String[] args) {
int nRet = MV_OK;
int camIndex = -1;
scanner = new Scanner(System.in);
Handle hCamera = null;
ArrayList<MV_CC_DEVICE_INFO> stDeviceList = null;
do {
System.out.println("SDK Version " + MvCameraControl.MV_CC_GetSDKVersion());
// Initialize SDK
nRet = MvCameraControl.MV_CC_Initialize();
if (MV_OK != nRet) {
System.err.printf("Initialize SDK fail! nRet [0x%x]\n\n", nRet);
break;
}
// Enumerate devices
try {
stDeviceList = MvCameraControl.MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_GIGE_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE);
} catch (CameraControlException e) {
System.err.println("Enumrate devices failed!" + e.toString());
e.printStackTrace();
break;
}
if (0 >= stDeviceList.size()) {
System.out.println("No devices found!");
break;
}
int i = 0;
for (MV_CC_DEVICE_INFO stDeviceInfo : stDeviceList) {
if (null == stDeviceInfo) {
continue;
}
System.out.println("[camera " + (i++) + "]");
printDeviceInfo(stDeviceInfo);
}
// choose camera
camIndex = chooseCamera(stDeviceList);
if (-1 == camIndex) {
break;
}
// Create device handle
try {
hCamera = MvCameraControl.MV_CC_CreateHandle(stDeviceList.get(camIndex));
} catch (CameraControlException e) {
System.err.println("Create handle failed!" + e.toString());
e.printStackTrace();
hCamera = null;
break;
}
// Open selected device
nRet = MvCameraControl.MV_CC_OpenDevice(hCamera);
if (MV_OK != nRet) {
System.err.printf("Connect to camera failed, errcode: [%#x]\n", nRet);
break;
}
// Register image callback
nRet = MvCameraControl.MV_CC_RegisterImageCallBack(hCamera, new CameraImageCallBack() {
@Override
public int OnImageCallBack(byte[] bytes, MV_FRAME_OUT_INFO mv_frame_out_info) {
printFrameInfo(mv_frame_out_info);
return 0;
}
});
if (MV_OK != nRet) {
System.err.printf("register image callback failed, errcode: [%#x]\n", nRet);
break;
}
// Turn off trigger mode
nRet = MvCameraControl.MV_CC_SetEnumValueByString(hCamera, "TriggerMode", "Off");
if (MV_OK != nRet) {
System.err.printf("SetTriggerMode failed, errcode: [%#x]\n", nRet);
break;
}
nRet = MvCameraControl.MV_CC_StartGrabbing(hCamera);
if (MV_OK != nRet) {
System.err.printf("StartGrabbing failed, errcode: [%#x]\n", nRet);
break;
}
scanner.useDelimiter("");
System.out.println("Press Enter to exit.");
while (true) {
String input = scanner.nextLine();
if (scanner.hasNextLine()) {
break;
} else {
try {
Thread.sleep(1 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
// Stop grabbing
nRet = MvCameraControl.MV_CC_StopGrabbing(hCamera);
if (MV_OK != nRet) {
System.err.printf("StopGrabbing failed, errcode: [%#x]\n", nRet);
break;
}
// close device
nRet = MvCameraControl.MV_CC_CloseDevice(hCamera);
if (MV_OK != nRet) {
System.err.printf("CloseDevice failed, errcode: [%#x]\n", nRet);
break;
}
} while (false);
if (null != hCamera) {
// Destroy handle
nRet = MvCameraControl.MV_CC_DestroyHandle(hCamera);
if (MV_OK != nRet) {
System.err.printf("DestroyHandle failed, errcode: [%#x]\n", nRet);
}
}
MvCameraControl.MV_CC_Finalize();
scanner.close();
}
}

View File

@@ -1,5 +1,14 @@
package top.wms.admin.controller.meterial; package top.wms.admin.controller.meterial;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
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; import top.continew.starter.extension.crud.enums.Api;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -8,13 +17,19 @@ import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import top.continew.starter.extension.crud.annotation.CrudRequestMapping; import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
import top.continew.starter.web.model.R;
import top.wms.admin.common.controller.BaseController; import top.wms.admin.common.controller.BaseController;
import top.wms.admin.material.model.entity.MaterialInfoDO; import top.wms.admin.material.model.entity.MaterialInfoDO;
import top.wms.admin.material.model.query.MaterialInfoQuery; import top.wms.admin.material.model.query.MaterialInfoQuery;
import top.wms.admin.material.model.req.MaterialInfoImportReq;
import top.wms.admin.material.model.req.MaterialInfoReq; import top.wms.admin.material.model.req.MaterialInfoReq;
import top.wms.admin.material.model.resp.MaterialImportParseResp;
import top.wms.admin.material.model.resp.MaterialInfoImportResp;
import top.wms.admin.material.model.resp.MaterialInfoResp; import top.wms.admin.material.model.resp.MaterialInfoResp;
import top.wms.admin.material.service.MaterialInfoService; import top.wms.admin.material.service.MaterialInfoService;
import java.io.IOException;
/** /**
* 物料信息管理 API * 物料信息管理 API
* *
@@ -28,9 +43,49 @@ import top.wms.admin.material.service.MaterialInfoService;
Api.EXPORT}) Api.EXPORT})
public class MaterialInfoController extends BaseController<MaterialInfoService, MaterialInfoResp, MaterialInfoResp, MaterialInfoQuery, MaterialInfoReq> { public class MaterialInfoController extends BaseController<MaterialInfoService, MaterialInfoResp, MaterialInfoResp, MaterialInfoQuery, MaterialInfoReq> {
@GetMapping("/code/{code}") @GetMapping("/code/{code}")
public MaterialInfoDO getMaterialInfoByCode(@PathVariable String code) { public MaterialInfoDO getMaterialInfoByCode(@PathVariable String code) {
return baseService.getMaterialInfoByCode(code); MaterialInfoDO materialInfoDO = baseService.getMaterialInfoByCode(code);
CheckUtils.throwIfEmpty(materialInfoDO,"未查询到相关物料信息");
return materialInfoDO;
}
@Operation(summary = "下载导入模板", description = "下载导入模板")
@SaCheckPermission("admin:materialInfo:import")
@GetMapping(value = "/import/template", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void downloadImportTemplate(HttpServletResponse response) throws IOException {
baseService.downloadImportTemplate(response);
}
@Operation(summary = "解析导入数据", description = "解析导入数据")
@SaCheckPermission("admin:materialInfo:import")
@PostMapping("/import/parse")
public MaterialImportParseResp parseImport(@NotNull(message = "文件不能为空") MultipartFile file) {
ValidationUtils.throwIf(file::isEmpty, "文件不能为空");
return baseService.parseImport(file);
}
@Operation(summary = "导入数据", description = "导入数据")
@SaCheckPermission("admin:materialInfo:import")
@PostMapping(value = "/import")
public MaterialInfoImportResp importUser(@Validated @RequestBody MaterialInfoImportReq req) {
return baseService.importMaterial(req);
}
@Operation(summary = "照片批量导入", description = "照片批量导入")
@SaCheckPermission("admin:materialInfo:import")
@PostMapping("/import/uploadMaterialPhotos")
public R uploadMaterialPhotos(@RequestParam("zipFile") @NotNull(message = "文件不能为空") MultipartFile zipFile) {
ValidationUtils.throwIf(zipFile::isEmpty, "文件不能为空");
String originalFilename = zipFile.getOriginalFilename();
ValidationUtils.throwIf(() -> originalFilename == null || !originalFilename.endsWith(".zip"), "仅支持上传ZIP格式文件");
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

@@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -91,7 +92,8 @@ public class UserController extends BaseController<UserService, UserResp, UserDe
ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败"); ValidationUtils.throwIfNull(rawNewPassword, "新密码解密失败");
ValidationUtils.throwIf(!ReUtil ValidationUtils.throwIf(!ReUtil
.isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字"); .isMatch(RegexConstants.PASSWORD, rawNewPassword), "密码长度为 8-32 个字符,支持大小写字母、数字、特殊字符,至少包含字母和数字");
req.setNewPassword(rawNewPassword); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
req.setNewPassword("{bcrypt}" + encoder.encode(rawNewPassword));
baseService.resetPassword(req, id); baseService.resetPassword(req, id);
} }

View File

@@ -0,0 +1,76 @@
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;
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;
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.util.List;
/**
* 任务工单信息管理 API
*
* @author zc
* @since 2026/03/03 17:09
*/
@Tag(name = "任务工单信息管理 API")
@RestController
@RequiredArgsConstructor
@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 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,327 @@
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 = "COM5";
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() {
// 项目启动时初始化并启动服务
if (false) {
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

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