feat(qms): 添加质检模块上位机接口和模板匹配策略,RemoteCodeRuleService添加selectCodeRuleCodeWithTenant

- 新增质检模块架构说明文档,包括质检模板匹配策略
- 完善检测类型字典和通用模板规则定义
- 扩展编码规则服务支持租户传递功能
- 实现上位机接口服务层和控制层
- 添加质检任务生成功能和工位名称转编码逻辑
- 实现8级降级匹配策略用于质检模板匹配
- 添加事务管理和异常处理机制
master
zangch@mesnac.com 3 days ago
parent 0103c1c901
commit 906bc8adc1

@ -14,4 +14,14 @@ public interface RemoteCodeRuleService {
*/
String selectCodeRuleCode(String codeRuleCode);
/**
* Dubbo
* <p>
*
*
* @param codeRuleCode
* @return currentCode
*/
String selectCodeRuleCodeWithTenant(String codeRuleCode);
}

@ -53,4 +53,60 @@ qc_inspection_main (检验单主表) → [产生不合格] → qc_unqualified_re
1. **模板驱动**: 检验单关联模板,模板包含多个检测项定义
2. **级联继承**: 检验结果从检测项定义继承标准值、上下限等规格参数
3. **主子分离**: 主表记录总体信息(单号、物料、数量),子表记录明细(每个检测项的检测结果)
4. **不合格评审**: 当检验结果不合格时,触发评审流程,生成评审主表及记录子表
4. **不合格评审**: 当检验结果不合格时,触发评审流程,生成评审主表及记录子表
---
## 质检模板匹配策略(核心业务规则)
**生成质检任务时按以下4级降级优先级自动匹配模板**
```
┌─────────────────────────────────────────────────────────────┐
│ 质检模板匹配策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第1级物料 + 工位 + 工序 + 检测类型(精确匹配) │
│ ↓ 未找到 │
│ 第2级物料 + 工位 + 检测类型 │
│ ↓ 未找到 │
│ 第3级物料 + 检测类型 │
│ ↓ 未找到 │
│ 第4级通用模板仅检测类型 + isDefault=1
│ ↓ 未找到 → 抛出异常:"无可用检测模板" │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 匹配条件说明
| 优先级 | 匹配条件 | 说明 | 业务场景 |
|-------|---------|------|---------|
| 1 | 物料 + 工位 + 工序 + 检测类型 | 最精确匹配 | 某物料在特定工位/工序有特殊质检要求 |
| 2 | 物料 + 工位 + 检测类型 | 特定工位(不限工序) | 某物料在特定工位的质检要求统一 |
| 3 | 物料 + 检测类型 | 特定物料(不限工位/工序) | 某物料的质检要求在任何工位/工序都一致 |
| 4 | 通用模板(仅检测类型) | 兜底方案 | 该检测类型的默认质检方案 |
### 检测类型字典
| 代码 | 检测类型 | 说明 |
|------|---------|------|
| 0 | 首检 | 生产开始前首批产品的检验 |
| 1 | 专检 | 专职检验员的检验 |
| 2 | 自检 | 操作工自己检验 |
| 3 | 互检 | 相互检验(上下工序互检) |
| 4 | 原材料检 | 原材料入库检验 |
| 5 | 抽检 | 抽样检验 |
| 6 | 成品检 | 成品检验 |
| 7 | 入库检 | 产品入库前的检验 |
### 通用模板规则
**定义**
- **通用模板**:不绑定具体物料、工序或工位,仅按检测类型适用的默认质检方案
- **标识字段**`is_default`0否/1是
**约束规则**
1. **唯一性**:每种检测类型只能有一个通用模板
2. **互斥性**:通用模板不能同时绑定物料、工序或工位
3. **校验方式**应用层校验Service层代码校验非数据库约束

@ -0,0 +1,60 @@
package org.dromara.qms.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.qms.domain.dto.QcInspectionMainTask;
import org.dromara.qms.service.IQcHMIService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
*
* <p>
* HMI
*
* @author zch
* @date 2026-01-14
*/
@Validated
@RequiredArgsConstructor
@Slf4j
@RestController
@RequestMapping("/hmi")
public class QcHMIController {
private final IQcHMIService qcHmiService;
/**
*
* <p>
* : POST /hmi/QcInspectionMainTask
* <p>
* :
* <pre>
* {
* "MaterialCode": "30109",
* "StationName": "内衬层#01",
* "InspectionQty": "12",
* "InspectionType": "7",
* "ProductionOrder": "20260112100834PL0005",
* "BatchNo": "123",
* "Barcode": "20260114NC001010006"
* }
* </pre>
*
* @param taskDto DTO
* @return
*/
// 上位机接口不登录,不使用@Log注解LogAspect会尝试获取用户信息导致异常
@PostMapping("/QcInspectionMainTask")
public R<String> generateInspectionTask(@Valid @RequestBody QcInspectionMainTask taskDto) {
log.info("收到上位机质检任务生成请求,物料编码: {}, 工位名称: {}, 检验类型: {}",
taskDto.getMaterialCode(), taskDto.getStationName(), taskDto.getInspectionType());
String inspectionNo = qcHmiService.generateInspectionTask(taskDto);
return R.ok("质检任务生成成功", inspectionNo);
}
}

@ -0,0 +1,61 @@
package org.dromara.qms.domain.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
*
* <p>
* 使 @JsonProperty JSON
*
* @author zch
* @date 2026-1-13
*/
@Data
public class QcInspectionMainTask {
/**
*
*/
@JsonProperty("MaterialCode")
private String materialCode;
/**
*
*/
@JsonProperty("StationName")
private String stationName;
/**
*
*/
@JsonProperty("InspectionQty")
private String inspectionQty;
/**
* 0 1 2 3 4 5 6 7
* 7
*/
@JsonProperty("InspectionType")
private String inspectionType;
/**
* ()
* prod plan info_2plan_code
*/
@JsonProperty("ProductionOrder")
private String productionOrder;
/**
*
*/
@JsonProperty("BatchNo")
private String batchNo;
/**
*
*/
@JsonProperty("Barcode")
private String barcode;
}

@ -0,0 +1,29 @@
package org.dromara.qms.service;
import org.dromara.qms.domain.dto.QcInspectionMainTask;
/**
* Service
*
* @author zch
* @date 2026-01-14
*/
public interface IQcHMIService {
/**
*
* <p>
*
* 1.
* 2.
* 3. 8
* 4.
* 5.
* 6.
* 7.
*
* @param taskDto DTO
* @return
*/
String generateInspectionTask(QcInspectionMainTask taskDto);
}

@ -0,0 +1,277 @@
package org.dromara.qms.service.impl;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import cn.dev33.satoken.stp.StpUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.rpc.RpcContext;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.api.RemoteCodeRuleService;
import org.dromara.qms.domain.bo.QcInspectionMainBo;
import org.dromara.qms.domain.bo.QcInspectionResultBo;
import org.dromara.qms.domain.bo.ProdBaseStationInfoBo;
import org.dromara.qms.domain.bo.ProdBaseProcessInfoBo;
import org.dromara.qms.domain.bo.QcTemplateItemBo;
import org.dromara.qms.domain.dto.QcInspectionMainTask;
import org.dromara.qms.domain.vo.ProdBaseStationInfoVo;
import org.dromara.qms.domain.vo.ProdBaseProcessInfoVo;
import org.dromara.qms.domain.vo.QcInspectionTemplateVo;
import org.dromara.qms.domain.vo.QcTemplateItemVo;
import org.dromara.qms.service.IQcHMIService;
import org.dromara.qms.service.IQcInspectionMainService;
import org.dromara.qms.service.IQcInspectionResultService;
import org.dromara.qms.service.IQcInspectionTemplateService;
import org.dromara.qms.service.IQcTemplateItemService;
import org.dromara.qms.service.IProdBaseStationInfoService;
import org.dromara.qms.service.IProdBaseProcessInfoService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Service
*
* @author zch
* @date 2026-01-14
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class QcHMIServiceImpl implements IQcHMIService {
private final IQcInspectionMainService qcInspectionMainService;
private final IQcInspectionResultService qcInspectionResultService;
private final IQcInspectionTemplateService qcInspectionTemplateService;
private final IQcTemplateItemService qcTemplateItemService;
private final IProdBaseStationInfoService prodBaseStationInfoService;
private final IProdBaseProcessInfoService prodBaseProcessInfoService;
/**
* Dubbo
*
*/
@DubboReference(timeout = 300000)
private final RemoteCodeRuleService remoteCodeRuleService;
/**
* ID
*/
private static final String HMI_DEFAULT_TENANT_ID = "000000";
/**
* ID
*/
private static final Long HMI_DEFAULT_CREATE_BY = 1L;
/**
*
* <p>
*
* 1.
* 2.
* 3. 8
* 4.
* 5.
* 6.
* 7.
*
* @param taskDto DTO
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String generateInspectionTask(QcInspectionMainTask taskDto) {
// 【重要】上位机不登录,模拟登录系统管理员,确保认证上下文和租户信息正确传递
StpUtil.login(HMI_DEFAULT_CREATE_BY, "login");
// 设置默认租户上下文
TenantHelper.setDynamic(HMI_DEFAULT_TENANT_ID);
// 【关键】通过Dubbo隐式参数传递租户ID确保远程服务能获取租户上下文
RpcContext.getContext().setAttachment("tenantId", HMI_DEFAULT_TENANT_ID);
try {
return doGenerateInspectionTask(taskDto);
} finally {
// 【清理】清理登录状态和租户上下文,避免线程池复用时的状态污染
try {
StpUtil.logout();
} catch (Exception ignored) {
}
try {
TenantHelper.clearDynamic();
} catch (Exception ignored) {
}
}
}
/**
*
*/
private String doGenerateInspectionTask(QcInspectionMainTask taskDto) {
// 1. 参数校验
if (StringUtils.isBlank(taskDto.getMaterialCode())) {
throw new ServiceException("物料编码不能为空");
}
if (StringUtils.isBlank(taskDto.getInspectionType())) {
throw new ServiceException("检验类型不能为空");
}
log.info("上位机调用生成质检任务,物料编码: {}, 工位名称: {}, 检验类型: {}",
taskDto.getMaterialCode(), taskDto.getStationName(), taskDto.getInspectionType());
// 2. 匹配质检模板调用已实现的8级降级匹配策略
// FIXME: QcInspectionMainTask.stationName 是工位名称,需要查询工位编码
// FIXME: 上位机不传工序信息,工序编码传 null
// TODO: 后期建议上位机直接传递编码,避免跨模块查询
String materialCode = taskDto.getMaterialCode();
String stationCode = getStationCodeByName(taskDto.getStationName()); // 根据工位名称查询工位编码
QcInspectionTemplateVo templateVo = qcInspectionTemplateService.getMatchedTemplate(
materialCode,
stationCode, // 工位编码(通过名称查询)
null, // 工序编码(上位机不传)
taskDto.getInspectionType()
);
if (templateVo == null) {
throw new ServiceException("未找到匹配的质检模板,物料编码: " + taskDto.getMaterialCode());
}
log.info("质检模板匹配成功模板ID: {}, 模板名称: {}", templateVo.getTemplateId(), templateVo.getTemplateName());
// 3. 生成质检单号(调用编码规则服务,使用支持租户传递的方法)
String inspectionNo = remoteCodeRuleService.selectCodeRuleCodeWithTenant("3");
if (StringUtils.isBlank(inspectionNo)) {
throw new ServiceException("获取质检单号失败");
}
// 4. 构建质检主表BO
QcInspectionMainBo mainBo = buildInspectionMainBo(taskDto, templateVo, inspectionNo);
// 5. 插入质检主表
Boolean insertResult = qcInspectionMainService.insertByBo(mainBo);
if (!insertResult) {
throw new ServiceException("质检主表保存失败");
}
log.info("质检主表创建成功,质检单号: {}, 质检ID: {}", inspectionNo, mainBo.getInspectionId());
// 6. 根据模板生成质检结果子表
generateInspectionResults(mainBo.getInspectionId(), templateVo.getTemplateId());
log.info("质检结果子表生成成功,共{}个检测项", getTemplateItemCount(templateVo.getTemplateId()));
// 7. 返回质检单号
return inspectionNo;
}
/**
* BO
*/
private QcInspectionMainBo buildInspectionMainBo(QcInspectionMainTask taskDto,
QcInspectionTemplateVo templateVo,
String inspectionNo) {
QcInspectionMainBo mainBo = new QcInspectionMainBo();
mainBo.setInspectionNo(inspectionNo);
mainBo.setTemplateId(templateVo.getTemplateId());
mainBo.setTemplateName(templateVo.getTemplateName());
mainBo.setInspectionType(templateVo.getTypeId());
mainBo.setMaterialCode(taskDto.getMaterialCode());
mainBo.setStationName(taskDto.getStationName());
mainBo.setInspectionQty(new BigDecimal(taskDto.getInspectionQty()));
mainBo.setProductionOrder(taskDto.getProductionOrder());
mainBo.setBatchNo(taskDto.getBatchNo());
mainBo.setBarcode(taskDto.getBarcode());
mainBo.setStatus("0"); // 未处理
mainBo.setResult("0"); // 待判定
// 上位机不登录,手动设置审计字段
mainBo.setCreateBy(HMI_DEFAULT_CREATE_BY);
mainBo.setCreateTime(new Date());
return mainBo;
}
/**
*
* <p>
*
*/
private void generateInspectionResults(Long inspectionId, Long templateId) {
QcTemplateItemBo itemBo = new QcTemplateItemBo();
itemBo.setTemplateId(templateId);
List<QcTemplateItemVo> itemList = qcTemplateItemService.queryList(itemBo);
for (QcTemplateItemVo item : itemList) {
QcInspectionResultBo resultBo = new QcInspectionResultBo();
resultBo.setInspectionId(inspectionId);
resultBo.setItemId(item.getItemId());
resultBo.setDetectResult("2"); // 未判定
resultBo.setItemCode(item.getItemCode());
resultBo.setItemName(item.getItemName());
resultBo.setInspectionPosition(item.getInspectionPosition());
resultBo.setCategoryName(item.getCategoryName());
resultBo.setDetectType(item.getDetectType());
resultBo.setControlType(item.getControlType());
resultBo.setStandardValue(item.getStandardValue());
resultBo.setUpperLimit(item.getUpperLimit());
resultBo.setLowerLimit(item.getLowerLimit());
resultBo.setSpecName(item.getSpecName());
resultBo.setSpecUpper(item.getSpecUpper());
resultBo.setSpecLower(item.getSpecLower());
resultBo.setDescription(item.getDescription());
resultBo.setTypeId(item.getInspectionType());
// 上位机不登录,手动设置审计字段
resultBo.setCreateBy(HMI_DEFAULT_CREATE_BY);
resultBo.setCreateTime(new Date());
qcInspectionResultService.insertByBo(resultBo);
}
}
/**
*
*/
private int getTemplateItemCount(Long templateId) {
QcTemplateItemBo itemBo = new QcTemplateItemBo();
itemBo.setTemplateId(templateId);
return qcTemplateItemService.queryList(itemBo).size();
}
/**
*
* FIXME: 访
* TODO:
*
* @param stationName QcInspectionMainTask.stationName
* @return null
*/
private String getStationCodeByName(String stationName) {
if (StringUtils.isBlank(stationName)) {
return null;
}
ProdBaseStationInfoBo bo = new ProdBaseStationInfoBo();
bo.setStationName(stationName);
List<ProdBaseStationInfoVo> list = prodBaseStationInfoService.queryList(bo);
return (list != null && !list.isEmpty()) ? list.get(0).getStationCode() : null;
}
/**
*
* FIXME: 访
* TODO:
*
* @param processName
* @return null
*/
private String getProcessCodeByName(String processName) {
if (StringUtils.isBlank(processName)) {
return null;
}
ProdBaseProcessInfoBo bo = new ProdBaseProcessInfoBo();
bo.setProcessName(processName);
List<ProdBaseProcessInfoVo> list = prodBaseProcessInfoService.queryList(bo);
return (list != null && !list.isEmpty()) ? list.get(0).getProcessCode() : null;
}
}

@ -2,6 +2,9 @@ package org.dromara.system.dubbo;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.dubbo.rpc.RpcContext;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.system.api.RemoteCodeRuleService;
import org.dromara.system.domain.bo.SysCodeRuleBo;
import org.dromara.system.service.ISysCodeRuleService;
@ -30,4 +33,31 @@ public class RemoteCodeRuleServiceImpl implements RemoteCodeRuleService {
bo.setCodeRuleCode(codeRuleCode);
return sysCodeRuleService.getRuleGenerateCode(bo);
}
/**
* Dubbo
* <p>
* DubboID
*
*
* @param codeRuleCode
* @return currentCode
*/
@Override
public String selectCodeRuleCodeWithTenant(String codeRuleCode) {
// 从Dubbo上下文获取租户ID并设置确保租户过滤生效
String tenantId = RpcContext.getContext().getAttachment("tenantId");
if (StringUtils.isNotBlank(tenantId)) {
TenantHelper.setDynamic(tenantId);
}
try {
SysCodeRuleBo bo = new SysCodeRuleBo();
bo.setCodeRuleCode(codeRuleCode);
return sysCodeRuleService.getRuleGenerateCode(bo);
} finally {
if (StringUtils.isNotBlank(tenantId)) {
TenantHelper.clearDynamic();
}
}
}
}

Loading…
Cancel
Save