13 小时以前 7dc2054d4d93f17ce8595f1b0559c904bee2a38f
feat(mes): 新增生产线扫码入库功能

- 添加扫码入库菜单权限和数据库表结构
- 实现袋码扫码入库的完整业务流程
- 集成WIP虚拟仓库存和库存事务处理
- 提供扫码事件查询和工作台看板功能
- 实现幂等性检查和重复入库防护机制
- 完善错误处理和状态追踪功能
已添加12个文件
462 ■■■■■ 文件已修改
docs/mes_scan_event_integration.md 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mes_scan_event_inbound.sql 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_pro_scan_inbound_init.sql 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/MesProScanEventController.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventCreateReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventPageReqVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventRespVO.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/scan/MesProScanEventDO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/scan/MesProScanEventMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesProScanEventStatusEnum.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_scan_event_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
# ç”Ÿäº§çº¿æ‰«ç å…¥åº“ - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§çº¿æ‰«ç å…¥åº“自动工作台
- æ‰«ç äº‹ä»¶å®¡è®¡
- ç”Ÿäº§æ‰¹æ¬¡å’Œè¢‹ç è¯¦æƒ…
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. å·¥ä¸šç›¸æœºç”±è¾¹ç¼˜ç¨‹åºå®ŒæˆäºŒç»´ç è¯†åˆ«ï¼Œä¸Šä¼  `eventId`、设备编码、产线编码和袋码。
2. åŽç«¯æ ¹æ®è¢‹ç å¸¦å‡ºç”Ÿäº§æ‰¹æ¬¡å’Œäº§å“ç‰©æ–™ï¼Œå¹¶ä½¿ç”¨ WIP è™šæ‹Ÿä»“配置完成库存入账。
3. å…¥åº“成功后记录服务端入库时间,增加暂存库存,并累计批次入库袋数。
4. åŒä¸€ä¸ª `eventId` é‡è¯•时返回原事件;已入库袋码不会重复增加库存。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|---|---|---|
| POST | `/mes/pro/scan-in/inbound` | è¢‹ç æ‰«ç å…¥åº“,成功后增加 WIP è™šæ‹Ÿä»“库存并写库存事务 |
| GET | `/mes/pro/scan-in/page` | åˆ†é¡µæŸ¥è¯¢æ‰«ç äº‹ä»¶ |
| GET | `/mes/pro/scan-in/latest` | æŸ¥è¯¢è®¾å¤‡æˆ–全局最近扫码事件 |
| GET | `/mes/pro/scan-in/dashboard` | æŸ¥è¯¢å·¥ä½œå°æœ€è¿‘事件和累计事件数 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|---|---|---|---|
| eventId | String | æ˜¯ | è¾¹ç¼˜è®¾å¤‡ç”Ÿæˆçš„幂等事件号 |
| bagCode | String | æ˜¯ | å·¥ä¸šç›¸æœºè¯†åˆ«å‡ºçš„袋码 |
| quantity | Decimal | æ˜¯ | å…¥åº“数量,本期一袋通常为 1 |
| deviceCode | String | æ˜¯ | è®¾å¤‡ç¼–码,用于审计 |
| lineCode | String | å¦ | äº§çº¿ç¼–码,用于审计 |
| locationId | Long | æ˜¯ | WIP æš‚存库区编号 |
| areaId | Long | æ˜¯ | WIP æš‚存库位编号 |
| remark | String | å¦ | å¤‡æ³¨ |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|---|---|---|
| eventId | String | å¹‚等事件号 |
| bagCode | String | è¢‹ç  |
| batchCode | String | ç”Ÿäº§æ‰¹æ¬¡å· |
| quantity | Decimal | æœ¬æ¬¡å…¥åº“数量 |
| deviceCode | String | è®¾å¤‡ç¼–码 |
| eventTime | DateTime | æœåŠ¡ç«¯å…¥åº“æ—¶é—´ |
| transactionId | Long | åº“存事务编号 |
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|---|---|---|
| deviceCode | è®¾å¤‡çŠ¶æ€ã€æœ€è¿‘æ‰«ç åˆ—è¡¨ | æ˜¾ç¤ºå½“前设备或事件来源 |
| bagCode | æœ€è¿‘扫码列表 | æ˜¾ç¤ºæœ€è¿‘识别的袋码 |
| batchCode | æœ€è¿‘扫码列表、批次汇总 | å±•示袋码归属的生产批次 |
| eventTime | æœ€è¿‘扫码列表 | ä»¥æœåŠ¡ç«¯å…¥åº“æ—¶é—´ä¸ºå‡† |
| transactionId | æœ€è¿‘扫码列表 | ç”¨äºŽåº“存流水追溯 |
| quantity | åº“存摘要 | ç´¯è®¡æš‚存数量 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|---|---|
| é‡å¤äº‹ä»¶ | åŒä¸€ `eventId` è¿”回第一次处理结果,不重复增加库存 |
| å·²å…¥åº“袋码 | æ‹’绝重复入库,并保留业务错误信息 |
| WIP æš‚存库 | ä»“库由后端使用 WIP è™šæ‹Ÿä»“解析,前端不传仓库编号 |
| äº‹åŠ¡ä¸€è‡´æ€§ | äº‹ä»¶ã€è¢‹ç çŠ¶æ€ã€æ‰¹æ¬¡ç´¯è®¡ã€åº“å­˜å°è´¦å’Œåº“å­˜äº‹åŠ¡åœ¨åŒä¸€äº‹åŠ¡ä¸­æäº¤ |
| è®¾å¤‡æ—¶é—´ | è®¾å¤‡æ‰«ç æ—¶é—´ç”¨äºŽè¿½æº¯ï¼Œåº“存入库时间使用服务端时间 |
## å¤±è´¥çŠ¶æ€ä¸Žæ ‡å‡†é”™è¯¯ç 
扫码请求无论成功或失败都会保留扫码事件记录。状态取值:
| çŠ¶æ€ | è¯´æ˜Ž |
|---|---|
| SUCCESS | å·²å®Œæˆåº“存入库 |
| FAILED | æ ¡éªŒæˆ–库存处理失败 |
| é”™è¯¯ç  | è¯´æ˜Ž |
|---|---|
| MES_SCAN_BAG_NOT_EXISTS | è¢‹ç ä¸å­˜åœ¨ |
| MES_SCAN_BAG_ALREADY_INBOUND | è¢‹ç å·²å…¥åº“ |
| MES_SCAN_BATCH_NOT_EXISTS | ç”Ÿäº§æ‰¹æ¬¡ä¸å­˜åœ¨ |
| MES_SCAN_WIP_WAREHOUSE_NOT_EXISTS | WIP è™šæ‹Ÿä»“不存在 |
| MES_SCAN_TRANSACTION_FAILED | åº“存事务处理失败 |
| MES_SCAN_EVENT_ID_CONFLICT | äº‹ä»¶å·å·²å­˜åœ¨ä½†å†…容不一致 |
失败记录保留设备编码、袋码、接收时间、失败时间、错误码和错误原因;失败时不产生库存事务。
## æƒé™ç‚¹
| æƒé™ | ç”¨é€” |
|---|---|
| mes:pro-scan-in:query | æŸ¥è¯¢äº‹ä»¶å’Œæœ€è¿‘扫码 |
| mes:pro-scan-in:inbound | æ‰§è¡Œæ‰«ç å…¥åº“ |
| mes:pro-scan-in:dashboard | æŸ¥è¯¢å·¥ä½œå°çœ‹æ¿ |
- è¾¹ç¼˜ç¨‹åºä¸å¾—上传图片或视频,本接口只接收解码后的二维码结果。
- å‰ç«¯å·¥ä½œå°åº”在页面卸载时停止刷新任务。
- å…¥åº“失败时展示后端返回的业务错误,不自动调用通用库存调整接口重试。
- WIP åº“区和库位需提前配置;接口不接受客户端指定仓库编号。
sql/mes_scan_event_inbound.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
ALTER TABLE mes_pro_bag_code ADD COLUMN inbound_time datetime NULL COMMENT '入库时间';
ALTER TABLE mes_pro_bag_code ADD COLUMN inbound_event_id bigint NULL COMMENT '入库事件ID';
ALTER TABLE mes_pro_bag_code ADD COLUMN inbound_device_code varchar(64) NULL COMMENT '入库设备编码';
ALTER TABLE mes_pro_bag_code ADD COLUMN inbound_line_code varchar(64) NULL COMMENT '入库产线编码';
ALTER TABLE mes_pro_batch ADD COLUMN inbound_bag_quantity int NULL DEFAULT 0 COMMENT '累计入库袋数' AFTER pallet_quantity;
ALTER TABLE mes_pro_batch ADD COLUMN inbound_event_id bigint NULL COMMENT '入库事件ID';
ALTER TABLE mes_pro_batch ADD COLUMN inbound_device_code varchar(64) NULL COMMENT '入库设备编码';
ALTER TABLE mes_pro_batch ADD COLUMN inbound_line_code varchar(64) NULL COMMENT '入库产线编码';
CREATE TABLE mes_pro_scan_event (
  id bigint NOT NULL AUTO_INCREMENT,
  event_id varchar(64) NOT NULL,
  bag_code varchar(128) NOT NULL,
  bag_code_id bigint NULL,
  batch_id bigint NULL,
  batch_code varchar(128) NULL,
  item_id bigint NULL,
  quantity decimal(18,6) NULL,
  device_code varchar(64) NOT NULL,
  line_code varchar(64) NULL,
  warehouse_id bigint NULL,
  location_id bigint NULL,
  area_id bigint NULL,
  transaction_id bigint NULL,
  status varchar(16) NOT NULL DEFAULT 'FAILED',
  error_code varchar(64) NULL,
  error_message varchar(500) NULL,
  received_time datetime NOT NULL,
  failed_time datetime NULL,
  event_time datetime NULL,
  remark varchar(500) NULL,
  creator varchar(64) NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updater varchar(64) NULL, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  deleted bit NOT NULL DEFAULT 0,
  PRIMARY KEY (id), UNIQUE KEY uk_mes_scan_event_event_id (event_id), UNIQUE KEY uk_mes_scan_event_bag (bag_code_id), KEY idx_mes_scan_event_status (status), KEY idx_mes_scan_event_error_code (error_code), KEY idx_mes_scan_event_received_time (received_time)
) ENGINE=InnoDB COMMENT='MES袋码扫码入库事件';
sql/mysql/mes_pro_scan_inbound_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
-- ç”Ÿäº§çº¿æ‰«ç å…¥åº“菜单与按钮权限
-- èœå•挂载到一袋一码管理(602630),组件路径与前端动态路由一致。
UPDATE system_menu
SET parent_id = 602630, sort = 7, updater = '1', update_time = NOW()
WHERE id = 602650 AND deleted = 0;
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name,
                         status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 602650, '生产线扫码入库', '', 2, 7, 602630, 'scan-in', 'ep:camera', 'mes/pro/scan-in/index', 'MesProScanIn',
       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 602650 AND deleted = 0);
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name,
                         status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 602651, '扫码入库查询', 'mes:pro-scan-in:query', 3, 1, 602650, '', '', '', '',
       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 602651 AND deleted = 0);
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name,
                         status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 602652, '扫码入库', 'mes:pro-scan-in:inbound', 3, 2, 602650, '', '', '', '',
       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 602652 AND deleted = 0);
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name,
                         status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT 602653, '扫码入库看板', 'mes:pro-scan-in:dashboard', 3, 3, 602650, '', '', '', '',
       0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE id = 602653 AND deleted = 0);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/MesProScanEventController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.scan;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventCreateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventRespVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO;
import java.util.List;
import java.util.Map;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import cn.iocoder.yudao.module.mes.service.pro.scan.MesProScanEventService;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@Tag(name = "管理后台 - MES æ‰«ç å…¥åº“")
@RestController @RequestMapping("/mes/pro/scan-in")
public class MesProScanEventController {
    @Resource private MesProScanEventService service;
    @PostMapping("/inbound") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:inbound')") @Operation(summary = "袋码扫码入库")
    public CommonResult<MesProScanEventRespVO> inbound(@Valid @RequestBody MesProScanEventCreateReqVO req) {
        return success(BeanUtils.toBean(service.createInbound(req), MesProScanEventRespVO.class));
    }
    @GetMapping("/page") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:query')") @Operation(summary = "扫码事件分页")
    public CommonResult<PageResult<MesProScanEventRespVO>> page(@Valid MesProScanEventPageReqVO req) {
        PageResult<MesProScanEventDO> page = service.getPage(req);
        return success(new PageResult<>(BeanUtils.toBean(page.getList(), MesProScanEventRespVO.class), page.getTotal()));
    }
    @GetMapping("/latest") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:query')") @Operation(summary = "最新扫码事件")
    public CommonResult<MesProScanEventRespVO> latest(@RequestParam(required = false) String deviceCode) {
        return success(BeanUtils.toBean(service.getLatest(deviceCode), MesProScanEventRespVO.class));
    }
    @GetMapping("/dashboard") @PreAuthorize("@ss.hasPermission('mes:pro-scan-in:dashboard')") @Operation(summary = "扫码入库看板")
    public CommonResult<Map<String, Object>> dashboard() { return success(service.getDashboard()); }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventCreateReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class MesProScanEventCreateReqVO {
    @NotBlank private String eventId;
    @NotBlank private String bagCode;
    @NotNull @DecimalMin("0.000001") private BigDecimal quantity;
    @NotBlank private String deviceCode;
    private String lineCode;
    @NotNull private Long locationId;
    @NotNull private Long areaId;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class MesProScanEventPageReqVO extends PageParam {
    private String eventId;
    private String bagCode;
    private String batchCode;
    private String deviceCode;
    private String status;
    private String errorCode;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/scan/vo/MesProScanEventRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class MesProScanEventRespVO {
    private Long id;
    private String eventId;
    private String bagCode;
    private String batchCode;
    private BigDecimal quantity;
    private String deviceCode;
    private String lineCode;
    private Long warehouseId;
    private Long locationId;
    private Long areaId;
    private Long transactionId;
    private String status;
    private String errorCode;
    private String errorMessage;
    private LocalDateTime receivedTime;
    private LocalDateTime failedTime;
    private LocalDateTime eventTime;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/scan/MesProScanEventDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@TableName("mes_pro_scan_event")
@KeySequence("mes_pro_scan_event_seq")
@Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor
public class MesProScanEventDO extends BaseDO {
    @TableId private Long id;
    private String eventId;
    private String bagCode;
    private Long bagCodeId;
    private Long batchId;
    private String batchCode;
    private Long itemId;
    private BigDecimal quantity;
    private String deviceCode;
    private String lineCode;
    private Long warehouseId;
    private Long locationId;
    private Long areaId;
    private Long transactionId;
    private String status;
    private String errorCode;
    private String errorMessage;
    private LocalDateTime receivedTime;
    private LocalDateTime failedTime;
    private LocalDateTime eventTime;
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/scan/MesProScanEventMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.dal.mysql.pro.scan;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MesProScanEventMapper extends BaseMapperX<MesProScanEventDO> {
    default MesProScanEventDO selectByEventIdForUpdate(String eventId) {
        return selectOne(new LambdaQueryWrapperX<MesProScanEventDO>()
                .eq(MesProScanEventDO::getEventId, eventId).last("FOR UPDATE"));
    }
    default PageResult<MesProScanEventDO> selectPage(MesProScanEventPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesProScanEventDO>()
                .eqIfPresent(MesProScanEventDO::getEventId, reqVO.getEventId())
                .likeIfPresent(MesProScanEventDO::getBagCode, reqVO.getBagCode())
                .likeIfPresent(MesProScanEventDO::getBatchCode, reqVO.getBatchCode())
                .eqIfPresent(MesProScanEventDO::getDeviceCode, reqVO.getDeviceCode())
                .eqIfPresent(MesProScanEventDO::getStatus, reqVO.getStatus())
                .eqIfPresent(MesProScanEventDO::getErrorCode, reqVO.getErrorCode())
                .orderByDesc(MesProScanEventDO::getEventTime));
    }
    default MesProScanEventDO selectLatest(String deviceCode) {
        return selectOne(new LambdaQueryWrapperX<MesProScanEventDO>()
                .eqIfPresent(MesProScanEventDO::getDeviceCode, deviceCode)
                .orderByDesc(MesProScanEventDO::getEventTime).last("LIMIT 1"));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesProScanEventStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
package cn.iocoder.yudao.module.mes.enums;
public interface MesProScanEventStatusEnum {
    String SUCCESS = "SUCCESS";
    String FAILED = "FAILED";
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.mes.service.pro.scan;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventCreateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import jakarta.validation.Valid;
import java.util.Map;
public interface MesProScanEventService {
    MesProScanEventDO createInbound(@Valid MesProScanEventCreateReqVO reqVO);
    MesProScanEventDO getByEventId(String eventId);
    PageResult<MesProScanEventDO> getPage(MesProScanEventPageReqVO reqVO);
    MesProScanEventDO getLatest(String deviceCode);
    Map<String, Object> getDashboard();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/scan/MesProScanEventServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.mes.service.pro.scan;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventCreateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.scan.vo.MesProScanEventPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.scan.MesProScanEventDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.bagcode.MesProBagCodeMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.batch.MesProBatchMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.scan.MesProScanEventMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.MesProScanEventStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Service
@Validated
public class MesProScanEventServiceImpl implements MesProScanEventService {
    @Resource private MesProScanEventMapper eventMapper;
    @Resource private MesProBagCodeMapper bagCodeMapper;
    @Resource private MesProBatchMapper batchMapper;
    @Resource private MesWmTransactionService transactionService;
    @Resource private MesWmWarehouseService warehouseService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProScanEventDO createInbound(MesProScanEventCreateReqVO req) {
        MesProScanEventDO existing = eventMapper.selectByEventIdForUpdate(req.getEventId());
        if (existing != null) {
            if (!req.getBagCode().equals(existing.getBagCode()) || !req.getDeviceCode().equals(existing.getDeviceCode())) {
                return failed(existing, "MES_SCAN_EVENT_ID_CONFLICT", "事件号已存在但内容不一致");
            }
            return existing;
        }
        MesProScanEventDO event = new MesProScanEventDO();
        event.setEventId(req.getEventId()).setBagCode(req.getBagCode()).setQuantity(req.getQuantity())
                .setDeviceCode(req.getDeviceCode()).setLineCode(req.getLineCode()).setRemark(req.getRemark())
                .setStatus(MesProScanEventStatusEnum.FAILED).setReceivedTime(LocalDateTime.now());
        eventMapper.insert(event);
        MesProBagCodeDO bag = bagCodeMapper.selectByCodeForUpdate(req.getBagCode());
        if (bag == null) return failed(event, "MES_SCAN_BAG_NOT_EXISTS", "袋码不存在");
        event.setBagCodeId(bag.getId());
        if (bag.getInboundEventId() != null) return failed(event, "MES_SCAN_BAG_ALREADY_INBOUND", "袋码已入库");
        MesProBatchDO batch = batchMapper.selectByIdForUpdate(bag.getBatchId());
        if (batch == null) return failed(event, "MES_SCAN_BATCH_NOT_EXISTS", "生产批次不存在");
        event.setBatchId(batch.getId()).setBatchCode(batch.getCode()).setItemId(batch.getItemId());
        MesWmWarehouseDO warehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
        if (warehouse == null) return failed(event, "MES_SCAN_WIP_WAREHOUSE_NOT_EXISTS", "WIP虚拟仓不存在");
        event.setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId());
        try {
            Long txId = transactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                    .setType(MesWmTransactionTypeEnum.IN.getType()).setBizType(MesBizTypeConstants.WM_PRODUCT_PRODUCE)
                    .setBizId(batch.getId()).setBizCode(batch.getCode()).setBizLineId(bag.getId())
                    .setItemId(batch.getItemId()).setQuantity(req.getQuantity()).setBatchCode(batch.getCode())
                    .setWarehouseId(warehouse.getId()).setLocationId(req.getLocationId()).setAreaId(req.getAreaId())
                    .setReceiptTime(LocalDateTime.now()).setCheckFlag(true));
            event.setTransactionId(txId).setStatus(MesProScanEventStatusEnum.SUCCESS).setEventTime(LocalDateTime.now());
            eventMapper.updateById(event);
            bagCodeMapper.markInbound(bag.getId(), event.getId(), req.getDeviceCode(), req.getLineCode());
            batchMapper.markInbound(batch.getId(), event.getId(), req.getDeviceCode(), req.getLineCode());
            return event;
        } catch (Exception ex) {
            return failed(event, "MES_SCAN_TRANSACTION_FAILED", "库存事务处理失败");
        }
    }
    private MesProScanEventDO failed(MesProScanEventDO event, String code, String message) {
        event.setStatus(MesProScanEventStatusEnum.FAILED).setErrorCode(code).setErrorMessage(message)
                .setFailedTime(LocalDateTime.now()).setEventTime(LocalDateTime.now());
        eventMapper.updateById(event);
        return event;
    }
    @Override public MesProScanEventDO getByEventId(String eventId) { return eventMapper.selectOne(MesProScanEventDO::getEventId, eventId); }
    @Override public PageResult<MesProScanEventDO> getPage(MesProScanEventPageReqVO reqVO) { return eventMapper.selectPage(reqVO); }
    @Override public MesProScanEventDO getLatest(String deviceCode) { return eventMapper.selectLatest(deviceCode); }
    @Override public Map<String, Object> getDashboard() {
        Map<String, Object> result = new HashMap<>();
        result.put("latest", eventMapper.selectLatest(null));
        result.put("total", eventMapper.selectCount(null));
        result.put("success", eventMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProScanEventDO>().eq(MesProScanEventDO::getStatus, MesProScanEventStatusEnum.SUCCESS)));
        result.put("failed", eventMapper.selectCount(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProScanEventDO>().eq(MesProScanEventDO::getStatus, MesProScanEventStatusEnum.FAILED)));
        return result;
    }
}