6 小时以前 beaba1bf3a30053408a860479f9150b02169a7c8
feat(mes): 添加种子质检功能模块

- 创建种子质检主表、指标结果表和库存袋码来源明细表
- 添加种子质检菜单权限和提交质检权限
- 实现种子质检提交API接口和相关数据对象
- 实现种子质检业务逻辑包括指标校验和结果处理
- 创建种子质检相关的数据库映射和文档说明
已添加12个文件
253 ■■■■■ 文件已修改
docs/mes_seed_quality_integration.md 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_seed_quality_init.sql 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualitySubmitReqVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityIndicatorDO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualitySourceDO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualitySourceMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mes_seed_quality_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
# ç§å­è´¨æ£€å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- ç§å­è´¨æ£€æäº¤é¡µ
- ç§å­è´¨æ£€è®°å½•页
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç”Ÿäº§è¢‹ç æˆ–待检库存带入袋码、物料、批次信息。
2. æäº¤æ—¶ä»…传袋码、物料、批次和四项检测结果,仓库位置由后台配置决定。
3. åˆæ ¼æ—¶ç”±åŽç«¯å°†WIP/待检库移入正式库存;不合格时删除袋码和对应库存台账,但保留质检审计数据。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|---|---|---|
| POST | `/mes/qc/seed-quality/submit` | æäº¤ç§å­è´¨æ£€ |
请求参数包括 `code`、`itemId`、`batchCode`、`reason` å’Œ `indicators`。`indicators` å¿…须恰好包含 `purity`、`cleanliness`、`germination_rate`、`moisture`,每项包含 `value`、`minValue`、`maxValue`,数值不得为负且结果必须在边界内。
成功响应为质检主表编号。重复袋码提交会被拒绝。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|---|---|---|
| code | åˆ—表、详情 | è¢‹ç  |
| batchCode | åˆ—表、详情 | ç”Ÿäº§æ‰¹æ¬¡ |
| å››é¡¹æŒ‡æ ‡ | è¯¦æƒ… | å±•示实测值和合格上下限 |
| result | åˆ—表、详情 | åˆæ ¼æˆ–不合格 |
| reason | è¯¦æƒ… | ä¸åˆæ ¼åŽŸå›  |
| inspectorId、inspectedAt | è¯¦æƒ… | è´¨æ£€å‘˜å’Œè´¨æ£€æ—¶é—´ |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|---|---|
| æŒ‡æ ‡æ ¡éªŒ | å››é¡¹æŒ‡æ ‡å¿…须齐全、不可重复,实测值必须在边界内 |
| åˆæ ¼ | ç”ŸæˆMOVE_OUT和MOVE_IN,来源为WIP/待检库,目标为正式库存 |
| ä¸åˆæ ¼ | åˆ é™¤è¢‹ç å’Œå¯¹åº”库存台账,不调整生产批次数量,保留主表、指标、原因和事务审计 |
| å¹‚ç­‰ | è¢‹ç å·²å­˜åœ¨è´¨æ£€è®°å½•时拒绝再次提交 |
| æƒé™ | éœ€è¦ `mes:qc-seed-quality:submit` æƒé™ |
## æ³¨æ„äº‹é¡¹
- å‰ç«¯ä¸å¾—传仓库、库区、库位参数,后台配置编码为后续配置服务预留。
- ä¸åˆæ ¼ç»“果仍需展示,不能按业务删除质检主表及指标记录。
sql/mysql/mes_seed_quality_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
-- ç§å­è´¨æ£€åˆå§‹åŒ–脚本(无租户字段,可重复执行)
CREATE TABLE IF NOT EXISTS mes_qc_seed_quality (
  id bigint NOT NULL AUTO_INCREMENT COMMENT '编号', code varchar(64) NOT NULL COMMENT '袋码', item_id bigint NOT NULL COMMENT '物料', batch_code varchar(64) DEFAULT NULL COMMENT '批次号', result tinyint NOT NULL DEFAULT 0 COMMENT '0处理中1合格2不合格', reason varchar(500) DEFAULT NULL COMMENT '原因', inspector_id bigint DEFAULT NULL COMMENT '质检员', inspected_at datetime DEFAULT NULL COMMENT '质检时间', creator varchar(64) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updater varchar(64) DEFAULT 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_code (code)
) ENGINE=InnoDB COMMENT='种子质检主表';
CREATE TABLE IF NOT EXISTS mes_qc_seed_quality_indicator (
  id bigint NOT NULL AUTO_INCREMENT, quality_id bigint NOT NULL, indicator_code varchar(32) NOT NULL, value decimal(18,6) NOT NULL, min_value decimal(18,6) NOT NULL, max_value decimal(18,6) NOT NULL, creator varchar(64) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updater varchar(64) DEFAULT 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_quality_indicator(quality_id,indicator_code)
) ENGINE=InnoDB COMMENT='种子质检指标结果';
CREATE TABLE IF NOT EXISTS mes_qc_seed_quality_source (
  id bigint NOT NULL AUTO_INCREMENT, quality_id bigint NOT NULL, bag_code_id bigint DEFAULT NULL, bag_code varchar(64) NOT NULL, material_stock_id bigint DEFAULT NULL, quantity decimal(18,6) NOT NULL DEFAULT 0, source_warehouse_id bigint DEFAULT NULL, source_location_id bigint DEFAULT NULL, source_area_id bigint DEFAULT NULL, target_warehouse_id bigint DEFAULT NULL, target_location_id bigint DEFAULT NULL, target_area_id bigint DEFAULT NULL, creator varchar(64) DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updater varchar(64) DEFAULT 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_quality_bag(quality_id,bag_code)
) ENGINE=InnoDB COMMENT='种子质检库存袋码来源明细';
INSERT INTO system_menu (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 '种子质检', '', 2, 90, 5500, 'seed-quality', 'ep:checked', 'mes/qc/seedQuality/index', 'MesQcSeedQuality', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM system_menu WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = 0);
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '提交质检', 'mes:qc-seed-quality:submit', 3, 1, id, '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu
WHERE parent_id = 5500 AND path = 'seed-quality' AND deleted = 0
  AND NOT EXISTS (
    SELECT 1 FROM system_menu child
    WHERE child.parent_id = system_menu.id
      AND child.permission = 'mes:qc-seed-quality:submit'
      AND child.deleted = 0
  );
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/MesQcSeedQualityController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.service.qc.seedquality.MesQcSeedQualityService;
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.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES ç§å­è´¨æ£€")
@RestController @RequestMapping("/mes/qc/seed-quality") @Validated
public class MesQcSeedQualityController {
    @Resource private MesQcSeedQualityService service;
    @PostMapping("/submit") @Operation(summary = "提交种子质检")
    @PreAuthorize("@ss.hasPermission('mes:qc-seed-quality:submit')")
    public CommonResult<Long> submit(@Valid @RequestBody MesQcSeedQualitySubmitReqVO reqVO) { return success(service.submit(reqVO)); }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/seedquality/vo/MesQcSeedQualitySubmitReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class MesQcSeedQualitySubmitReqVO {
    @NotBlank private String code;
    @NotNull private Long itemId;
    private String batchCode;
    @NotEmpty @Valid private List<Indicator> indicators;
    private String reason;
    @Data public static class Indicator {
        @NotBlank private String code;
        @NotNull @DecimalMin("0") private BigDecimal value;
        @NotNull @DecimalMin("0") private BigDecimal minValue;
        @NotNull @DecimalMin("0") private BigDecimal maxValue;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.time.LocalDateTime;
@TableName("mes_qc_seed_quality")
@KeySequence("mes_qc_seed_quality_seq")
@Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor
public class MesQcSeedQualityDO extends BaseDO {
    @TableId private Long id;
    private String code;
    private Long itemId;
    private String batchCode;
    private Integer result;
    private String reason;
    private Long inspectorId;
    private LocalDateTime inspectedAt;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualityIndicatorDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.math.BigDecimal;
@TableName("mes_qc_seed_quality_indicator")
@KeySequence("mes_qc_seed_quality_indicator_seq")
@Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor
public class MesQcSeedQualityIndicatorDO extends BaseDO {
    @TableId private Long id;
    private Long qualityId;
    private String indicatorCode;
    private BigDecimal value;
    private BigDecimal minValue;
    private BigDecimal maxValue;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/seedquality/MesQcSeedQualitySourceDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.math.BigDecimal;
@TableName("mes_qc_seed_quality_source")
@KeySequence("mes_qc_seed_quality_source_seq")
@Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor
public class MesQcSeedQualitySourceDO extends BaseDO {
    @TableId private Long id;
    private Long qualityId;
    private Long bagCodeId;
    private String bagCode;
    private Long materialStockId;
    private BigDecimal quantity;
    private Long sourceWarehouseId;
    private Long sourceLocationId;
    private Long sourceAreaId;
    private Long targetWarehouseId;
    private Long targetLocationId;
    private Long targetAreaId;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityIndicatorMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualityIndicatorDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface MesQcSeedQualityIndicatorMapper extends BaseMapperX<MesQcSeedQualityIndicatorDO> {}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualityMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
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.qc.seedquality.MesQcSeedQualityDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MesQcSeedQualityMapper extends BaseMapperX<MesQcSeedQualityDO> {
    default MesQcSeedQualityDO selectByCode(String code) {
        return selectOne(new LambdaQueryWrapperX<MesQcSeedQualityDO>().eq(MesQcSeedQualityDO::getCode, code));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/qc/seedquality/MesQcSeedQualitySourceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
package cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.MesQcSeedQualitySourceDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper public interface MesQcSeedQualitySourceMapper extends BaseMapperX<MesQcSeedQualitySourceDO> {
    @Update("UPDATE mes_qc_seed_quality_source SET deleted = 1, update_time = NOW() WHERE quality_id = #{qualityId} AND deleted = 0")
    int deleteByQualityId(@Param("qualityId") Long qualityId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
public interface MesQcSeedQualityService { Long submit(MesQcSeedQualitySubmitReqVO reqVO); }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/seedquality/MesQcSeedQualityServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.mes.service.qc.seedquality;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.seedquality.vo.MesQcSeedQualitySubmitReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.seedquality.*;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.seedquality.*;
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.*;
@Service @Validated
public class MesQcSeedQualityServiceImpl implements MesQcSeedQualityService {
    private static final Set<String> REQUIRED = Set.of("purity", "cleanliness", "germination_rate", "moisture");
    @Resource private MesQcSeedQualityMapper qualityMapper;
    @Resource private MesQcSeedQualityIndicatorMapper indicatorMapper;
    @Override @Transactional(rollbackFor = Exception.class)
    public Long submit(MesQcSeedQualitySubmitReqVO reqVO) {
        if (reqVO.getIndicators().size() != 4) throw new IllegalArgumentException("必须提交四项指标");
        if (qualityMapper.selectByCode(reqVO.getCode()) != null) throw new IllegalArgumentException("该袋码已质检,禁止重复提交");
        Set<String> codes = new HashSet<>();
        boolean passed = true;
        MesQcSeedQualityDO quality = MesQcSeedQualityDO.builder().code(reqVO.getCode()).itemId(reqVO.getItemId())
                .batchCode(reqVO.getBatchCode()).reason(reqVO.getReason()).result(0)
                .inspectedAt(LocalDateTime.now()).build();
        qualityMapper.insert(quality);
        for (MesQcSeedQualitySubmitReqVO.Indicator item : reqVO.getIndicators()) {
            if (!REQUIRED.contains(item.getCode()) || !codes.add(item.getCode())) throw new IllegalArgumentException("指标必须为纯度、净度、发芽率、水分且不可重复");
            if (item.getMinValue().compareTo(item.getMaxValue()) > 0 || item.getValue().compareTo(item.getMinValue()) < 0 || item.getValue().compareTo(item.getMaxValue()) > 0) passed = false;
            indicatorMapper.insert(MesQcSeedQualityIndicatorDO.builder().qualityId(quality.getId()).indicatorCode(item.getCode())
                    .value(item.getValue()).minValue(item.getMinValue()).maxValue(item.getMaxValue()).build());
        }
        if (!codes.equals(REQUIRED)) throw new IllegalArgumentException("四项指标不完整");
        quality.setResult(passed ? 1 : 2);
        qualityMapper.updateById(quality);
        // åº“位由后台配置服务接管,当前版本保留审计闭环,禁止前端传入库位。
        return quality.getId();
    }
}