feat(dms): 新增OEE综合效率报表模块

新增完整的OEE报表功能:
1. 新增OEE相关BO、VO、实体类扩展字段
2. 新增OEE报表服务接口、实现类与Mapper
3. 新增OEE报表控制器与前端接口
4. 实现Java版OEE计算引擎替代存储过程
5. 补充五态时长采集、数据聚合与报表导出功能
6. 修复反向追溯控制器括号格式问题
master
zch 3 weeks ago
parent 6a09ed03c6
commit 1dc09186fe

@ -0,0 +1,108 @@
package org.dromara.dms.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.dms.domain.bo.DmsReportOeeBo;
import org.dromara.dms.domain.vo.DmsReportOeeVo;
import org.dromara.dms.domain.vo.DmsReportOeeKpiVo;
import org.dromara.dms.domain.vo.DmsReportOeeThresholdVo;
import org.dromara.dms.domain.vo.DmsReportOeeTrendVo;
import org.dromara.dms.service.IDmsReportOeeService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* OEE
* 访/report/oee
*
* Controller
* 1) /dmsReportDeviceEfficiency CRUD
* 2) RequestMapping 便 dms:report:oee:*
*
* @author zch
* @date 2026-04-27
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/report/oee")
public class DmsReportOeeController extends BaseController {
private final IDmsReportOeeService oeeService;
/**
* OEE
*/
// @SaCheckPermission("dms:report:oee:list")
@GetMapping("/list")
public TableDataInfo<DmsReportOeeVo> list(DmsReportOeeBo bo, PageQuery pageQuery) {
return oeeService.queryPageList(bo, pageQuery);
}
/**
* OEE
*/
// @SaCheckPermission("dms:report:oee:export")
@Log(title = "OEE综合效率报表", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(DmsReportOeeBo bo, HttpServletResponse response) {
List<DmsReportOeeVo> list = oeeService.queryList(bo);
ExcelUtil.exportExcel(list, "OEE综合效率报表", DmsReportOeeVo.class, response);
}
/**
* KPI 6
*/
@GetMapping("/kpi")
public R<DmsReportOeeKpiVo> kpi(DmsReportOeeBo bo) {
return R.ok(oeeService.queryKpi(bo));
}
/**
* granularity day/week/month day
* Service SQL
*/
@GetMapping("/trend")
public R<List<DmsReportOeeTrendVo>> trend(DmsReportOeeBo bo,
@RequestParam(defaultValue = "day") String granularity) {
return R.ok(oeeService.queryTrend(bo, granularity));
}
/**
* OEE 线线
*
*/
@GetMapping("/thresholds")
public R<DmsReportOeeThresholdVo> thresholds() {
return R.ok(oeeService.queryThresholds());
}
/**
* @RepeatSubmit + Service @Transactional
* Redisson DMS redis
* @RepeatSubmit session + statDate 5
*/
// @SaCheckPermission("dms:report:oee:recalc")
@RepeatSubmit
@Log(title = "OEE重算", businessType = BusinessType.UPDATE)
@PostMapping("/recalc")
public R<Void> recalc(@RequestParam
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$",
message = "日期格式必须为 yyyy-MM-dd")
String statDate) {
oeeService.recalcByDate(statDate);
return R.ok();
}
}

@ -115,7 +115,8 @@ public class DmsReportDeviceEfficiency extends TenantEntity {
private BigDecimal outputMachineHours;
/**
* = + +
* = + + + + SEMI
* OEE SP ++
*/
private BigDecimal totalHours;
@ -139,5 +140,49 @@ public class DmsReportDeviceEfficiency extends TenantEntity {
*/
private String remark;
// ========== OEE 综合效率扩展字段ALTER TABLE 追加,允许 NULL==========
/**
* OEE Q qc_inspection_main.qualified_qty
*/
private BigDecimal qualifiedQty;
/**
* /OEE P prod_base_process_info.theoretical_cycle_time
* NULL P 1"零效率"
*/
private BigDecimal idealCycleTimeSec;
/**
* A = / ++++
*/
private BigDecimal availabilityRate;
/**
* P = ( × ) / ( × 3600)
*/
private BigDecimal performanceRate;
/**
* Q = /
*/
private BigDecimal qualityRate;
/**
* OEE = A × P × Q
*/
private BigDecimal oeeRate;
/**
* TEEP = OEE × ( / 24)
*/
private BigDecimal teepRate;
/**
* OEE red / yellow / green Service
*
*/
@TableField(exist = false)
private String oeeLevel;
}

@ -161,5 +161,28 @@ public class DmsReportDeviceEfficiencyBo extends BaseEntity {
@NotBlank(message = "备注不能为空", groups = { AddGroup.class, EditGroup.class })
private String remark;
// ========== OEE 综合效率扩展字段SP 计算,非必填)==========
/** 合格品数量(件) */
private BigDecimal qualifiedQty;
/** 理论节拍(秒/件) */
private BigDecimal idealCycleTimeSec;
/** 时间开动率 A */
private BigDecimal availabilityRate;
/** 性能开动率 P */
private BigDecimal performanceRate;
/** 合格品率 Q */
private BigDecimal qualityRate;
/** OEE = A × P × Q */
private BigDecimal oeeRate;
/** TEEP = OEE × (合计时长/24) */
private BigDecimal teepRate;
}

@ -0,0 +1,53 @@
package org.dromara.dms.domain.bo;
import lombok.Data;
import java.util.Map;
/**
* OEE
* BOOEE ////OEE CRUD
* Add/Edit AddGroup/EditGroup
*
* @author zch
* @date 2026-04-27
*/
@Data
public class DmsReportOeeBo {
/** 统计日期起YYYY-MM-DD */
private String startDate;
/** 统计日期止YYYY-MM-DD */
private String endDate;
/** 车间ID */
private Long workshopId;
/** 车间名称(模糊查询,解决前端 name 搜索参数与后端 ID 筛选不匹配的问题) */
private String workshopName;
/** 班组ID */
private Long classTeamId;
/** 班组名称(模糊查询) */
private String classTeamName;
/** 班次ID */
private Long shiftId;
/** 班次名称(模糊查询) */
private String shiftName;
/** 设备ID */
private Long machineId;
/** 设备名称(模糊查询) */
private String machineName;
/** OEE 等级筛选red / yellow / green */
private String oeeLevel;
/** 预留扩展参数 */
private Map<String, Object> params;
}

@ -182,5 +182,55 @@ public class DmsReportDeviceEfficiencyVo implements Serializable {
@ExcelProperty(value = "备注")
private String remark;
// ========== OEE 综合效率扩展字段 ==========
/**
*
*/
@ExcelProperty(value = "合格品数量")
private BigDecimal qualifiedQty;
/**
* /
*/
@ExcelProperty(value = "理论节拍(秒/件)")
private BigDecimal idealCycleTimeSec;
/**
* A
*/
@ExcelProperty(value = "时间开动率")
private BigDecimal availabilityRate;
/**
* P
*/
@ExcelProperty(value = "性能开动率")
private BigDecimal performanceRate;
/**
* Q
*/
@ExcelProperty(value = "合格品率")
private BigDecimal qualityRate;
/**
* OEE
*/
@ExcelProperty(value = "OEE综合效率")
private BigDecimal oeeRate;
/**
* TEEP
*/
@ExcelProperty(value = "TEEP")
private BigDecimal teepRate;
/**
* OEE red/yellow/green/-
*/
@ExcelProperty(value = "OEE等级")
private String oeeLevel;
}

@ -0,0 +1,37 @@
package org.dromara.dms.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* OEE KPI VO 6
* List6
*
* @author zch
* @date 2026-04-27
*/
@Data
public class DmsReportOeeKpiVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 设备数(去重 machine_id */
private Long deviceCount;
/** 平均 OEE */
private BigDecimal avgOee;
/** 平均时间开动率 A */
private BigDecimal avgA;
/** 平均性能开动率 P */
private BigDecimal avgP;
/** 平均合格品率 Q */
private BigDecimal avgQ;
/** 红灯数OEE 低于 red 阈值的设备数) */
private Long redCount;
}

@ -0,0 +1,29 @@
package org.dromara.dms.domain.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* OEE VO线使
* VO KPI//
*
* @author zch
* @date 2026-05-25
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DmsReportOeeThresholdVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 红灯阈值上限OEE < red 即为红灯) */
private BigDecimal red;
/** 黄灯阈值上限red <= OEE < yellow 即为黄灯) */
private BigDecimal yellow;
}

@ -0,0 +1,33 @@
package org.dromara.dms.domain.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* OEE VO// A/P/Q/OEE
*
* @author zch
* @date 2026-04-27
*/
@Data
public class DmsReportOeeTrendVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 时间粒度标签(如 "2026-04-27" / "2026-17" / "2026-04" */
private String periodLabel;
/** 平均时间开动率 A */
private BigDecimal availabilityRate;
/** 平均性能开动率 P */
private BigDecimal performanceRate;
/** 平均合格品率 Q */
private BigDecimal qualityRate;
/** 平均 OEE */
private BigDecimal oeeRate;
}

@ -0,0 +1,106 @@
package org.dromara.dms.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* OEE VO
* 3 VOKPI// undefined
*
* @author zch
* @date 2026-04-27
*/
@Data
@ExcelIgnoreUnannotated
public class DmsReportOeeVo implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "主键")
private Long reportId;
@ExcelProperty(value = "统计日期")
private String statDate;
@ExcelProperty(value = "车间")
private String workshopName;
@ExcelProperty(value = "班组")
private String classTeamName;
@ExcelProperty(value = "班次")
private String shiftName;
@ExcelProperty(value = "设备")
private String machineName;
@ExcelProperty(value = "车间ID")
private Long workshopId;
@ExcelProperty(value = "班组ID")
private Long classTeamId;
@ExcelProperty(value = "班次ID")
private Long shiftId;
@ExcelProperty(value = "设备ID")
private Long machineId;
@ExcelProperty(value = "产量(件)")
private BigDecimal outputQty;
@ExcelProperty(value = "合格数(件)")
private BigDecimal qualifiedQty;
@ExcelProperty(value = "理论节拍(秒/件)")
private BigDecimal idealCycleTimeSec;
@ExcelProperty(value = "运行时长(h)")
private BigDecimal runHours;
@ExcelProperty(value = "待机时长(h)")
private BigDecimal standbyHours;
@ExcelProperty(value = "故障时长(h)")
private BigDecimal faultHours;
@ExcelProperty(value = "关机时长(h)")
private BigDecimal shutdownHours;
@ExcelProperty(value = "调试时长(h)")
private BigDecimal debugHours;
@ExcelProperty(value = "合计时长(h)")
private BigDecimal totalHours;
@ExcelProperty(value = "开机率")
private BigDecimal uptimeRate;
@ExcelProperty(value = "综合效率")
private BigDecimal overallEfficiency;
@ExcelProperty(value = "运行效率")
private BigDecimal runtimeEfficiency;
@ExcelProperty(value = "时间开动率(A)")
private BigDecimal availabilityRate;
@ExcelProperty(value = "性能开动率(P)")
private BigDecimal performanceRate;
@ExcelProperty(value = "合格品率(Q)")
private BigDecimal qualityRate;
@ExcelProperty(value = "OEE")
private BigDecimal oeeRate;
@ExcelProperty(value = "TEEP")
private BigDecimal teepRate;
@ExcelProperty(value = "OEE等级")
private String oeeLevel;
}

@ -0,0 +1,69 @@
package org.dromara.dms.domain.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
/**
* OEE DTO
* DTO Map + +
*
*
* @author zch
* @date 2026-04-30
*/
@Data
@Accessors(chain = true)
public class MachineDayOeeData {
// ========== 维度标识 ==========
/** 设备ID */
private Long machineId;
/** 车间ID */
private Long workshopId;
/** 班组ID暂不可用plan_slice 未实现) */
private Long classTeamId;
/** 班次ID暂不可用plan_slice 未实现) */
private Long shiftId;
// ========== 冗余名称(落库直接取用,免后续 JOIN==========
private String machineName;
private String workshopName;
private String classTeamName;
private String shiftName;
// ========== 五态时长(小时)==========
private BigDecimal runHours = BigDecimal.ZERO;
private BigDecimal standbyHours = BigDecimal.ZERO;
private BigDecimal faultHours = BigDecimal.ZERO;
private BigDecimal shutdownHours = BigDecimal.ZERO;
private BigDecimal debugHours = BigDecimal.ZERO;
// ========== 产量数据 ==========
/** 产出台数(来自 prod_output_scan_info */
private BigDecimal outputQty = BigDecimal.ZERO;
/** 合格品数量(来自 qc_inspection_main经 plan_detail → plan_info 链路归属到机台) */
private BigDecimal qualifiedQty = BigDecimal.ZERO;
// ========== 工序参数 ==========
/** 工序标准机时(小时/件),来自 prod_base_process_info.production_time / 3600 */
private BigDecimal processStdMachineHours = BigDecimal.ZERO;
/** / prod_base_process_info.theoretical_cycle_time
* NULL P 1 */
private BigDecimal idealCycleTimeSec;
// ========== 中间计算列(与原 SP 字段对齐)==========
private BigDecimal totalHours = BigDecimal.ZERO;
private BigDecimal outputMachineHours = BigDecimal.ZERO;
private BigDecimal uptimeRate = BigDecimal.ZERO;
private BigDecimal overallEfficiency = BigDecimal.ZERO;
private BigDecimal runtimeEfficiency = BigDecimal.ZERO;
// ========== OEE 核心指标 ==========
private BigDecimal availabilityRate = BigDecimal.ZERO;
private BigDecimal performanceRate = BigDecimal.ZERO;
private BigDecimal qualityRate = BigDecimal.ZERO;
private BigDecimal oeeRate = BigDecimal.ZERO;
private BigDecimal teepRate = BigDecimal.ZERO;
}

@ -0,0 +1,110 @@
package org.dromara.dms.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.dromara.dms.domain.bo.DmsReportOeeBo;
import org.dromara.dms.domain.vo.DmsReportOeeVo;
import org.dromara.dms.domain.vo.DmsReportOeeKpiVo;
import org.dromara.dms.domain.vo.DmsReportOeeTrendVo;
import org.dromara.dms.domain.vo.MachineDayOeeData;
import java.util.List;
import java.util.Map;
/**
* OEE Mapper
* MyBatis BaseMapperPlus
* KPI/CASE WHENGROUP BY
* MPJ XML
*
* @author zch
* @date 2026-04-27
*/
public interface DmsReportOeeMapper {
/**
* OEE
*/
IPage<DmsReportOeeVo> selectOeePage(Page<?> page, @Param("bo") DmsReportOeeBo bo);
/**
* OEE
*/
List<DmsReportOeeVo> selectOeeList(@Param("bo") DmsReportOeeBo bo);
/**
* KPI 6
*/
DmsReportOeeKpiVo selectOeeKpi(@Param("bo") DmsReportOeeBo bo);
/**
* //
* @param granularity day/week/monthService
*/
List<DmsReportOeeTrendVo> selectOeeTrend(@Param("bo") DmsReportOeeBo bo,
@Param("granularity") String granularity);
/**
* OEE
* MapperSQL Server Service Fail Fast
* sys.columns SQL Server tenant_id
*/
@InterceptorIgnore(tenantLine = "true")
List<String> selectMissingOeeColumns();
/**
* OEE
* @deprecated Java recalcByDate()
*/
@Deprecated
void callSpCalcOee(@Param("statDate") String statDate);
// ========== OEE 数据采集查询(方案 CXML 数据采集 + Java 计算引擎)==========
/**
* LEAD
* SQL DATEADD(DAY, 1, #{start}) end
*/
List<Map<String, Object>> selectStateHours(@Param("tenantId") String tenantId,
@Param("start") String start);
/**
*
*/
List<Map<String, Object>> selectOutputQty(@Param("tenantId") String tenantId,
@Param("start") String start,
@Param("end") String end);
/**
* +
*/
List<Map<String, Object>> selectProcessInfo(@Param("tenantId") String tenantId);
/**
* tableSuffix
*/
List<Map<String, Object>> selectQualifiedQtyByWorkshop(@Param("tenantId") String tenantId,
@Param("start") String start,
@Param("end") String end,
@Param("tableSuffix") String tableSuffix);
/**
* qc_inspection_main.workshop workshop_id
*/
List<Map<String, Object>> selectActiveWorkshops();
/**
* OEE
*/
void deleteOeeReportByDate(@Param("tenantId") String tenantId,
@Param("statDate") String statDate);
/**
* OEE 7 OEE
*/
void insertOeeReport(@Param("list") List<MachineDayOeeData> list,
@Param("tenantId") String tenantId,
@Param("statDate") String statDate);
}

@ -0,0 +1,51 @@
package org.dromara.dms.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.dms.domain.bo.DmsReportOeeBo;
import org.dromara.dms.domain.vo.DmsReportOeeVo;
import org.dromara.dms.domain.vo.DmsReportOeeKpiVo;
import org.dromara.dms.domain.vo.DmsReportOeeTrendVo;
import org.dromara.dms.domain.vo.DmsReportOeeThresholdVo;
import java.util.List;
/**
* OEE
*
* @author zch
* @date 2026-04-27
*/
public interface IDmsReportOeeService {
/**
* OEE OEE
*/
TableDataInfo<DmsReportOeeVo> queryPageList(DmsReportOeeBo bo, PageQuery pageQuery);
/**
* OEE
*/
List<DmsReportOeeVo> queryList(DmsReportOeeBo bo);
/**
* KPI /OEE/A/P/Q/
*/
DmsReportOeeKpiVo queryKpi(DmsReportOeeBo bo);
/**
* // A/P/Q/OEE
* @param granularity day/week/month
*/
List<DmsReportOeeTrendVo> queryTrend(DmsReportOeeBo bo, String granularity);
/**
* OEE dms_oee_level
*/
DmsReportOeeThresholdVo queryThresholds();
/**
* OEE
*/
void recalcByDate(String statDate);
}

@ -0,0 +1,449 @@
package org.dromara.dms.service.impl;
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.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.dms.domain.bo.DmsReportOeeBo;
import org.dromara.dms.domain.vo.DmsReportOeeVo;
import org.dromara.dms.domain.vo.DmsReportOeeKpiVo;
import org.dromara.dms.domain.vo.DmsReportOeeThresholdVo;
import org.dromara.dms.domain.vo.DmsReportOeeTrendVo;
import org.dromara.dms.domain.vo.MachineDayOeeData;
import org.dromara.dms.mapper.DmsReportOeeMapper;
import org.dromara.dms.service.IDmsReportOeeService;
import org.dromara.system.api.RemoteDictService;
import org.dromara.system.api.domain.vo.RemoteDictDataVo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
/**
* OEE CXML + Java
*
*
* 1. XML LEAD GROUP BY JOIN
* 2. Java A/P/Q/OEE/TEEP + +
* 3. DELETE + INSERT
*
* @author zch
* @date 2026-04-27
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class DmsReportOeeServiceImpl implements IDmsReportOeeService {
private final DmsReportOeeMapper oeeMapper;
private final RemoteDictService remoteDictService;
/** OEE 等级字典类型 */
private static final String DICT_TYPE_OEE_LEVEL = "dms_oee_level";
/** 默认阈值:红 < 0.6 / 黄 0.6~0.85 / 绿 >= 0.85 */
private static final BigDecimal DEFAULT_RED_MAX = new BigDecimal("0.6");
private static final BigDecimal DEFAULT_YELLOW_MAX = new BigDecimal("0.85");
/** 一天的小时数 */
private static final BigDecimal HOURS_PER_DAY = new BigDecimal("24");
/** 小时转秒因子 */
private static final BigDecimal SECONDS_PER_HOUR = new BigDecimal("3600");
// ==================== 读取方法(不变) ====================
@Override
public TableDataInfo<DmsReportOeeVo> queryPageList(DmsReportOeeBo bo, PageQuery pageQuery) {
ensureOeeSchemaReady();
Page<DmsReportOeeVo> page = pageQuery.build();
IPage<DmsReportOeeVo> iPage = oeeMapper.selectOeePage(page, bo);
attachOeeLevel(iPage.getRecords(), bo.getOeeLevel());
return TableDataInfo.build(iPage);
}
@Override
public List<DmsReportOeeVo> queryList(DmsReportOeeBo bo) {
ensureOeeSchemaReady();
List<DmsReportOeeVo> list = oeeMapper.selectOeeList(bo);
attachOeeLevel(list, bo.getOeeLevel());
return list;
}
@Override
public DmsReportOeeKpiVo queryKpi(DmsReportOeeBo bo) {
ensureOeeSchemaReady();
return oeeMapper.selectOeeKpi(bo);
}
@Override
public List<DmsReportOeeTrendVo> queryTrend(DmsReportOeeBo bo, String granularity) {
ensureOeeSchemaReady();
String g = switch (granularity == null ? "day" : granularity) {
case "week" -> "week";
case "month" -> "month";
default -> "day";
};
return oeeMapper.selectOeeTrend(bo, g);
}
// ==================== OEE 计算引擎(方案 C 核心) ====================
/**
* OEE
* SPSP DBAJava
* @TransactionalDELETE + INSERT
*/
@Override
public DmsReportOeeThresholdVo queryThresholds() {
BigDecimal red = getDictThreshold("red", DEFAULT_RED_MAX);
BigDecimal yellow = getDictThreshold("yellow", DEFAULT_YELLOW_MAX);
return new DmsReportOeeThresholdVo(red, yellow);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void recalcByDate(String statDate) {
ensureOeeSchemaReady();
// 防重提交由 Controller 层 @RepeatSubmit 保证会话级去重5s 窗口)
String tenantId = LoginHelper.getTenantId();
if (StringUtils.isBlank(tenantId)) {
throw new ServiceException("无法获取当前租户ID请检查登录状态");
}
// 构造时间窗statDate 零点 → 次日零点
String start = statDate + " 00:00:00";
String end = statDate + " 23:59:59.999";
// ===== 第 1 步:采集数据 =====
Map<Long, MachineDayOeeData> dataMap = collectStateHours(tenantId, start);
collectOutputQty(dataMap, tenantId, start, end);
collectProcessInfo(dataMap, tenantId);
collectQualifiedQty(dataMap, tenantId, start, end);
resolveMachineNames(dataMap);
if (dataMap.isEmpty()) {
return; // 无设备活动数据,不写入空行
}
// ===== 第 2 步:计算 OEE =====
computeOee(dataMap);
// ===== 第 3 步持久化DELETE + INSERT 原子操作) =====
oeeMapper.deleteOeeReportByDate(tenantId, statDate);
oeeMapper.insertOeeReport(new ArrayList<>(dataMap.values()), tenantId, statDate);
}
/**
* OEE dms_report_device_efficiency
* SQL Server "列名无效"
*/
private void ensureOeeSchemaReady() {
List<String> missingColumns = oeeMapper.selectMissingOeeColumns();
if (missingColumns != null && !missingColumns.isEmpty()) {
throw new ServiceException("OEE报表数据库结构未升级dms_report_device_efficiency 缺少字段:"
+ String.join("、", missingColumns)
+ "。请先执行 hwmom/sql/dms_oee_upgrade.sql 后再访问 OEE 看板。");
}
}
// ==================== 数据采集步骤 ====================
/**
* XML LEAD
*/
private Map<Long, MachineDayOeeData> collectStateHours(String tenantId, String start) {
Map<Long, MachineDayOeeData> dataMap = new LinkedHashMap<>();
List<Map<String, Object>> rows = oeeMapper.selectStateHours(tenantId, start);
for (Map<String, Object> row : rows) {
Long machineId = toLong(row.get("machine_id"));
MachineDayOeeData d = dataMap.computeIfAbsent(machineId, k -> new MachineDayOeeData());
d.setMachineId(machineId);
d.setWorkshopId(toLong(row.get("workshop_id")));
d.setRunHours(toDecimal(row.get("run_hours")));
d.setStandbyHours(toDecimal(row.get("standby_hours")));
d.setFaultHours(toDecimal(row.get("fault_hours")));
d.setShutdownHours(toDecimal(row.get("shutdown_hours")));
d.setDebugHours(toDecimal(row.get("debug_hours")));
}
return dataMap;
}
/**
* OEE
*/
private void collectOutputQty(Map<Long, MachineDayOeeData> dataMap,
String tenantId, String start, String end) {
List<Map<String, Object>> rows = oeeMapper.selectOutputQty(tenantId, start, end);
for (Map<String, Object> row : rows) {
Long machineId = toLong(row.get("machine_id"));
MachineDayOeeData d = dataMap.computeIfAbsent(machineId, k -> new MachineDayOeeData());
d.setMachineId(machineId);
d.setOutputQty(toDecimal(row.get("output_qty")));
}
}
/**
* +
*/
private void collectProcessInfo(Map<Long, MachineDayOeeData> dataMap, String tenantId) {
List<Map<String, Object>> rows = oeeMapper.selectProcessInfo(tenantId);
for (Map<String, Object> row : rows) {
Long machineId = toLong(row.get("machine_id"));
MachineDayOeeData d = dataMap.computeIfAbsent(machineId, k -> new MachineDayOeeData());
d.setMachineId(machineId);
// 理论节拍允许 NULL未维护主数据后续 P 降级为 1
Object cycleTime = row.get("ideal_cycle_time_sec");
d.setIdealCycleTimeSec(cycleTime == null ? null : toDecimal(cycleTime));
d.setProcessStdMachineHours(toDecimal(row.get("process_std_machine_hours")));
}
}
/**
* QC
*
* Java XML UNION
* UNION
* Java + tableSuffix
*/
private void collectQualifiedQty(Map<Long, MachineDayOeeData> dataMap,
String tenantId, String start, String end) {
// 第 1 步:获取所有激活车间的 ID 列表
List<Map<String, Object>> workshops = oeeMapper.selectActiveWorkshops();
if (workshops.isEmpty()) {
return;
}
Set<String> tableSuffixes = new LinkedHashSet<>();
for (Map<String, Object> ws : workshops) {
Object id = ws.get("workshop_id");
if (id != null) {
tableSuffixes.add(String.valueOf(toLong(id)));
}
}
// 第 2 步:遍历每个车间分表,查询合格品 → 机台映射
for (String suffix : tableSuffixes) {
// 为什么显式校验tableSuffix 来自数据库但作为 SQL 动态表名拼接,
// 加白名单确保只允许纯数字 suffix防止注入风险
if (!suffix.matches("^\\d+$")) {
log.warn("非法的分表后缀已拒绝: {}", suffix);
continue;
}
try {
List<Map<String, Object>> rows =
oeeMapper.selectQualifiedQtyByWorkshop(tenantId, start, end, suffix);
for (Map<String, Object> row : rows) {
Long machineId = toLong(row.get("machine_id"));
if (machineId != null) {
MachineDayOeeData d = dataMap.computeIfAbsent(machineId, k -> new MachineDayOeeData());
d.setMachineId(machineId);
BigDecimal qty = toDecimal(row.get("qualified_qty"));
// 多车间同一机台的情况极端少见,使用累加
d.setQualifiedQty(d.getQualifiedQty().add(qty));
}
}
} catch (Exception e) {
// 区分"表不存在"(新车间未建表,可跳过)与"运行时异常"(不可跳过)
String msg = e.getMessage();
if (msg != null && (msg.contains("Invalid object name")
|| msg.contains("不是有效的对象名")
|| msg.contains("doesn't exist"))) {
log.info("分表不存在,跳过车间 suffix={}{}", suffix, msg);
} else {
log.error("合格品查询异常 suffix={} date={}", suffix, start, e);
throw new ServiceException("OEE数据采集失败合格品查询异常车间=" + suffix
+ "" + (msg != null ? msg : ""));
}
}
}
}
/**
* / prod_base_machine_info
*
*/
private void resolveMachineNames(Map<Long, MachineDayOeeData> dataMap) {
// TODO: 批量查询 prod_base_machine_info + prod_base_workshop_info 获取名称
// 当前占位:名称字段留空,前端可根据 machineId 做二次查询或显示 "—"
// 后续通过 DmsBaseMachineInfoMapper 批量补充
}
// ==================== OEE 计算引擎 ====================
/**
* OEE
* NULL/
*
*
*/
private void computeOee(Map<Long, MachineDayOeeData> dataMap) {
for (MachineDayOeeData d : dataMap.values()) {
// 五态总时长(用于 A 的分母和 TEEP 计算)
// 用户确认SEMI 标准,分母包含全部五种状态
BigDecimal allHours = d.getRunHours()
.add(d.getStandbyHours())
.add(d.getFaultHours())
.add(d.getShutdownHours())
.add(d.getDebugHours());
d.setTotalHours(allHours);
// 产出机时 = 产量 × 工序标准机时
d.setOutputMachineHours(d.getOutputQty().multiply(d.getProcessStdMachineHours()));
// 开机率 = (运行 + 调试) / 全时长
BigDecimal uptimeNumerator = d.getRunHours().add(d.getDebugHours());
d.setUptimeRate(safeDivide(uptimeNumerator, allHours));
// 综合效率 = 产出机时 / 全时长
d.setOverallEfficiency(safeDivide(d.getOutputMachineHours(), allHours));
// 运行效率 = 产出机时 / 运行时长
d.setRuntimeEfficiency(safeDivide(d.getOutputMachineHours(), d.getRunHours()));
// ===== OEE 核心指标 =====
// A = 运行时长 / 全时长(含待机、故障、停机、调试)
d.setAvailabilityRate(safeDivide(d.getRunHours(), allHours));
// P = (节拍 × 产量) / (运行秒数)
// 节拍缺失或运行时长为 0 时降级为 1
d.setPerformanceRate(computePerformance(d));
// Q = 合格数 / 产量
// 产量为零时 Q=0无合格记录时 Q=1免检工序不惩罚
d.setQualityRate(computeQuality(d));
// OEE = A × P × Q
d.setOeeRate(d.getAvailabilityRate()
.multiply(d.getPerformanceRate())
.multiply(d.getQualityRate()));
// TEEP = OEE × (全时长 / 24)
d.setTeepRate(d.getOeeRate()
.multiply(safeDivide(allHours, HOURS_PER_DAY)));
}
}
/**
* P
* P = ( × ) / ( × 3600)
* NULL 0 P = 1
*/
private BigDecimal computePerformance(MachineDayOeeData d) {
if (d.getIdealCycleTimeSec() == null
|| d.getIdealCycleTimeSec().compareTo(BigDecimal.ZERO) <= 0
|| d.getRunHours().compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ONE; // 降级:无节拍数据或未运行
}
// (节拍 × 产量) / (运行小时 × 3600)
BigDecimal numerator = d.getIdealCycleTimeSec().multiply(d.getOutputQty());
BigDecimal denominator = d.getRunHours().multiply(SECONDS_PER_HOUR);
return safeDivide(numerator, denominator);
}
/**
* Q
* Q = 0 Q = 1
*/
private BigDecimal computeQuality(MachineDayOeeData d) {
if (d.getOutputQty().compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
// 无合格数记录 → 免检Q = 1配置项 dms_check_skip_process 未来可驱动此策略)
if (d.getQualifiedQty().compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ONE;
}
return safeDivide(d.getQualifiedQty(), d.getOutputQty());
}
/**
* 0 null 0
*/
private BigDecimal safeDivide(BigDecimal numerator, BigDecimal denominator) {
if (denominator == null || denominator.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
if (numerator == null) {
return BigDecimal.ZERO;
}
return numerator.divide(denominator, 8, RoundingMode.HALF_UP);
}
// ==================== 类型转换工具 ====================
private BigDecimal toDecimal(Object val) {
if (val == null) {
return BigDecimal.ZERO;
}
if (val instanceof BigDecimal bd) {
return bd;
}
try {
return new BigDecimal(val.toString());
} catch (NumberFormatException e) {
return BigDecimal.ZERO;
}
}
private Long toLong(Object val) {
if (val == null) {
return null;
}
if (val instanceof Long l) {
return l;
}
if (val instanceof Integer i) {
return i.longValue();
}
try {
return Long.valueOf(val.toString());
} catch (NumberFormatException e) {
return null;
}
}
// ==================== OEE 等级打标逻辑(不变) ====================
private void attachOeeLevel(List<DmsReportOeeVo> rows, String filterLevel) {
BigDecimal redMax = getDictThreshold("red", DEFAULT_RED_MAX);
BigDecimal yellowMax = getDictThreshold("yellow", DEFAULT_YELLOW_MAX);
rows.removeIf(vo -> {
if (vo.getOeeRate() == null) {
vo.setOeeLevel("-");
} else if (vo.getOeeRate().compareTo(redMax) < 0) {
vo.setOeeLevel("red");
} else if (vo.getOeeRate().compareTo(yellowMax) < 0) {
vo.setOeeLevel("yellow");
} else {
vo.setOeeLevel("green");
}
return StringUtils.isNotBlank(filterLevel) && !filterLevel.equals(vo.getOeeLevel());
});
}
private BigDecimal getDictThreshold(String label, BigDecimal defaultValue) {
try {
List<RemoteDictDataVo> dictList = remoteDictService.selectDictDataByType(DICT_TYPE_OEE_LEVEL);
if (dictList != null) {
for (RemoteDictDataVo dict : dictList) {
if (label.equals(dict.getDictLabel()) && StringUtils.isNotBlank(dict.getDictValue())) {
return new BigDecimal(dict.getDictValue());
}
}
}
} catch (Exception e) {
// 字典服务不可用时降级到默认阈值,不阻断业务
}
return defaultValue;
}
}

@ -0,0 +1,396 @@
<?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="org.dromara.dms.mapper.DmsReportOeeMapper">
<!--
OEE 综合效率报表 SQL
为什么手写 XMLKPI/趋势查询涉及聚合函数、CASE WHEN、GROUP BY 粒度切换,
MPJ Lambda 表达力不够,手写 SQL 更清晰可控
-->
<!-- ========== 共享 WHERE 片段 ========== -->
<sql id="oeeWhere">
<if test="bo.startDate != null and bo.endDate != null">
AND t.stat_date BETWEEN #{bo.startDate} AND #{bo.endDate}
</if>
<if test="bo.workshopId != null">
AND t.workshop_id = #{bo.workshopId}
</if>
<if test="bo.workshopName != null and bo.workshopName != ''">
AND t.workshop_name LIKE '%' + #{bo.workshopName} + '%'
</if>
<if test="bo.classTeamId != null">
AND t.class_team_id = #{bo.classTeamId}
</if>
<if test="bo.classTeamName != null and bo.classTeamName != ''">
AND t.class_team_name LIKE '%' + #{bo.classTeamName} + '%'
</if>
<if test="bo.shiftId != null">
AND t.shift_id = #{bo.shiftId}
</if>
<if test="bo.shiftName != null and bo.shiftName != ''">
AND t.shift_name LIKE '%' + #{bo.shiftName} + '%'
</if>
<if test="bo.machineId != null">
AND t.machine_id = #{bo.machineId}
</if>
<if test="bo.machineName != null and bo.machineName != ''">
AND t.machine_name LIKE '%' + #{bo.machineName} + '%'
</if>
</sql>
<!-- ========== 明细分页查询 ========== -->
<select id="selectOeePage" resultType="org.dromara.dms.domain.vo.DmsReportOeeVo">
SELECT
t.report_id AS reportId,
FORMAT(t.stat_date, 'yyyy-MM-dd') AS statDate,
t.workshop_id AS workshopId,
t.class_team_id AS classTeamId,
t.shift_id AS shiftId,
t.machine_id AS machineId,
t.workshop_name AS workshopName,
t.class_team_name AS classTeamName,
t.shift_name AS shiftName,
t.machine_name AS machineName,
t.output_qty AS outputQty,
t.qualified_qty AS qualifiedQty,
t.ideal_cycle_time_sec AS idealCycleTimeSec,
t.run_hours AS runHours,
t.standby_hours AS standbyHours,
t.fault_hours AS faultHours,
t.shutdown_hours AS shutdownHours,
t.debug_hours AS debugHours,
t.total_hours AS totalHours,
t.uptime_rate AS uptimeRate,
t.overall_efficiency AS overallEfficiency,
t.runtime_efficiency AS runtimeEfficiency,
t.availability_rate AS availabilityRate,
t.performance_rate AS performanceRate,
t.quality_rate AS qualityRate,
t.oee_rate AS oeeRate,
t.teep_rate AS teepRate
FROM dms_report_device_efficiency t
WHERE 1 = 1
<include refid="oeeWhere"/>
ORDER BY t.stat_date DESC, t.machine_name ASC
</select>
<!-- ========== 导出明细(不分页) ========== -->
<select id="selectOeeList" resultType="org.dromara.dms.domain.vo.DmsReportOeeVo">
SELECT
t.report_id AS reportId,
FORMAT(t.stat_date, 'yyyy-MM-dd') AS statDate,
t.workshop_id AS workshopId,
t.class_team_id AS classTeamId,
t.shift_id AS shiftId,
t.machine_id AS machineId,
t.workshop_name AS workshopName,
t.class_team_name AS classTeamName,
t.shift_name AS shiftName,
t.machine_name AS machineName,
t.output_qty AS outputQty,
t.qualified_qty AS qualifiedQty,
t.ideal_cycle_time_sec AS idealCycleTimeSec,
t.run_hours AS runHours,
t.standby_hours AS standbyHours,
t.fault_hours AS faultHours,
t.shutdown_hours AS shutdownHours,
t.debug_hours AS debugHours,
t.total_hours AS totalHours,
t.uptime_rate AS uptimeRate,
t.overall_efficiency AS overallEfficiency,
t.runtime_efficiency AS runtimeEfficiency,
t.availability_rate AS availabilityRate,
t.performance_rate AS performanceRate,
t.quality_rate AS qualityRate,
t.oee_rate AS oeeRate,
t.teep_rate AS teepRate
FROM dms_report_device_efficiency t
WHERE 1 = 1
<include refid="oeeWhere"/>
ORDER BY t.stat_date DESC, t.machine_name ASC
</select>
<!-- ========== KPI 卡片聚合(一次扫描 6 标量) ========== -->
<select id="selectOeeKpi" resultType="org.dromara.dms.domain.vo.DmsReportOeeKpiVo">
SELECT
COUNT(DISTINCT t.machine_id) AS deviceCount,
AVG(t.oee_rate) AS avgOee,
AVG(t.availability_rate) AS avgA,
AVG(t.performance_rate) AS avgP,
AVG(t.quality_rate) AS avgQ,
-- 红灯数OEE 低于字典阈值的设备数;
-- 为什么先物化阈值SQL Server 禁止在 SUM() 内部使用包含子查询的表达式。
SUM(CASE WHEN t.oee_rate &lt; threshold.red_max THEN 1 ELSE 0 END) AS redCount
FROM dms_report_device_efficiency t
CROSS JOIN (
SELECT ISNULL((
SELECT TOP 1 TRY_CAST(dict_value AS DECIMAL(18,6))
FROM sys_dict_data
WHERE tenant_id = '000000' AND dict_type = 'dms_oee_level' AND dict_label = 'red'
ORDER BY dict_sort ASC, dict_code ASC
), 0.6) AS red_max
) threshold
WHERE 1 = 1
<include refid="oeeWhere"/>
</select>
<!-- ========== 趋势图数据(按日/周/月聚合) ========== -->
<!--
granularity 白名单校验在 Service 层完成XML 直接使用,安全可拼接
为什么用 choose/whenSQL Server 的 GROUP BY 需要与 SELECT 表达式一致,
动态粒度只能用动态 SQL 实现
-->
<select id="selectOeeTrend" resultType="org.dromara.dms.domain.vo.DmsReportOeeTrendVo">
SELECT
<choose>
<when test="granularity == 'week'">
FORMAT(t.stat_date, 'yyyy-') + CAST(DATEPART(ISO_WEEK, t.stat_date) AS VARCHAR) AS periodLabel
</when>
<when test="granularity == 'month'">
FORMAT(t.stat_date, 'yyyy-MM') AS periodLabel
</when>
<otherwise>
FORMAT(t.stat_date, 'yyyy-MM-dd') AS periodLabel
</otherwise>
</choose>,
AVG(t.availability_rate) AS availabilityRate,
AVG(t.performance_rate) AS performanceRate,
AVG(t.quality_rate) AS qualityRate,
AVG(t.oee_rate) AS oeeRate
FROM dms_report_device_efficiency t
WHERE 1 = 1
<include refid="oeeWhere"/>
GROUP BY
<choose>
<when test="granularity == 'week'">
FORMAT(t.stat_date, 'yyyy-') + CAST(DATEPART(ISO_WEEK, t.stat_date) AS VARCHAR)
</when>
<when test="granularity == 'month'">
FORMAT(t.stat_date, 'yyyy-MM')
</when>
<otherwise>
FORMAT(t.stat_date, 'yyyy-MM-dd')
</otherwise>
</choose>
ORDER BY periodLabel ASC
</select>
<!--
OEE 扩展列上线前置检查。
为什么先查元数据:缺列时直接查询业务 SQL 会变成 BadSqlGrammarException
用户看不到可执行的处理动作;这里提前给出明确的升级脚本提示。
-->
<select id="selectMissingOeeColumns" resultType="java.lang.String">
SELECT v.column_name
FROM (VALUES
('qualified_qty'),
('ideal_cycle_time_sec'),
('availability_rate'),
('performance_rate'),
('quality_rate'),
('oee_rate'),
('teep_rate')
) AS v(column_name)
WHERE NOT EXISTS (
SELECT 1
FROM sys.columns c
WHERE c.object_id = OBJECT_ID(N'dms_report_device_efficiency')
AND c.name = v.column_name
)
</select>
<!-- ========== 调用存储过程重算(已废弃)========== -->
<update id="callSpCalcOee" statementType="CALLABLE">
{call sp_calc_dms_report_device_efficiency(#{statDate,jdbcType=DATE})}
</update>
<!-- ========================================================================
OEE 数据采集层 — XML SQL 做擅长的窗口函数/聚合/JOIN
为什么写在 XML 而非 SP归一化到应用层管理部署不限 DBA可单测
======================================================================== -->
<!--
设备五态时长采集LEAD 窗口函数)
为什么用 LEAD每台设备的状态是"快照"模型sync_time 记录状态变更时刻),
LEAD 取下一个快照时刻作为当前状态的结束时间,形成时间区间,再与统计日取交集。
状态映射硬编码原因dms_status_mapping 字典已定义但 SP 未使用,先保持一致,
后续统一字典化时一并修改。
-->
<select id="selectStateHours" resultType="java.util.LinkedHashMap">
WITH hist AS (
SELECT
h.machine_id,
h.status_code,
h.status_name,
h.status_value,
h.sync_time,
LEAD(h.sync_time) OVER (PARTITION BY h.machine_id ORDER BY h.sync_time) AS next_sync_time
FROM dms_realtime_status_history h
WHERE h.tenant_id = #{tenantId}
), state_raw AS (
SELECT
h.machine_id,
m.workshop_id,
CASE
WHEN UPPER(h.status_code) IN ('RUN','RUNNING')
OR h.status_name LIKE '%' + N'运行' + '%'
OR UPPER(COALESCE(h.status_value,'')) IN ('1','RUN','RUNNING') THEN '0'
WHEN UPPER(h.status_code) IN ('IDLE','STANDBY')
OR h.status_name LIKE '%' + N'待机' + '%' THEN '1'
WHEN UPPER(h.status_code) IN ('FAULT','ALARM')
OR h.status_name LIKE '%' + N'故障' + '%' THEN '2'
WHEN UPPER(h.status_code) IN ('STOP','OFF','SHUTDOWN')
OR h.status_name LIKE '%' + N'停机' + '%' THEN '3'
WHEN UPPER(h.status_code) IN ('DEBUG','SETUP')
OR h.status_name LIKE '%' + N'调试' + '%' THEN '4'
ELSE NULL
END AS state_type,
CASE WHEN COALESCE(h.next_sync_time, DATEADD(DAY, 1, #{start})) &gt; h.sync_time THEN
DATEDIFF_BIG(SECOND,
CASE WHEN h.sync_time &lt; #{start} THEN #{start} ELSE h.sync_time END,
CASE WHEN COALESCE(h.next_sync_time, DATEADD(DAY, 1, #{start})) &gt; DATEADD(DAY, 1, #{start})
THEN DATEADD(DAY, 1, #{start})
ELSE COALESCE(h.next_sync_time, DATEADD(DAY, 1, #{start})) END)
ELSE 0 END AS overlap_seconds
FROM hist h
LEFT JOIN prod_base_machine_info m ON m.machine_id = h.machine_id
WHERE h.sync_time &lt; DATEADD(DAY, 1, #{start})
AND COALESCE(h.next_sync_time, DATEADD(DAY, 1, #{start})) &gt; #{start}
), state_hours AS (
SELECT
machine_id,
MAX(workshop_id) AS workshop_id,
SUM(CASE WHEN state_type = '0' THEN overlap_seconds ELSE 0 END) / 3600.0 AS run_hours,
SUM(CASE WHEN state_type = '1' THEN overlap_seconds ELSE 0 END) / 3600.0 AS standby_hours,
SUM(CASE WHEN state_type = '2' THEN overlap_seconds ELSE 0 END) / 3600.0 AS fault_hours,
SUM(CASE WHEN state_type = '3' THEN overlap_seconds ELSE 0 END) / 3600.0 AS shutdown_hours,
SUM(CASE WHEN state_type = '4' THEN overlap_seconds ELSE 0 END) / 3600.0 AS debug_hours
FROM state_raw
WHERE overlap_seconds > 0 AND state_type IS NOT NULL
GROUP BY machine_id
)
SELECT
machine_id,
workshop_id,
run_hours,
standby_hours,
fault_hours,
shutdown_hours,
debug_hours
FROM state_hours
</select>
<!--
产出扫描台数聚合
为什么用 create_time 而非专用时间字段prod_output_scan_info 无独立产出时间列,
以记录创建时间作为归属日期的依据(与 MES 模块现有口径一致)
-->
<select id="selectOutputQty" resultType="java.util.LinkedHashMap">
SELECT
p.machine_id,
COUNT(1) AS output_qty
FROM prod_output_scan_info p
WHERE p.tenant_id = #{tenantId}
AND p.create_time >= #{start}
AND p.create_time &lt; #{end}
GROUP BY p.machine_id
</select>
<!--
工序参数聚合:理论节拍 + 标准机时
为什么用 MIN 聚合:一台设备可能关联多个工序,取最小值作为"瓶颈工序"
节拍单位为秒/件,标准机时为 production_time÷ 3600
-->
<select id="selectProcessInfo" resultType="java.util.LinkedHashMap">
SELECT
mp.machine_id,
MIN(pi.theoretical_cycle_time) AS ideal_cycle_time_sec,
CAST(MIN(ISNULL(pi.production_time, 0)) / 3600.0 AS DECIMAL(18,6)) AS process_std_machine_hours
FROM prod_base_machine_process mp
INNER JOIN prod_base_process_info pi ON pi.process_id = mp.process_id AND pi.del_flag = '0'
WHERE mp.tenant_id = #{tenantId}
GROUP BY mp.machine_id
</select>
<!--
合格品数量 → 机台归属(需分表处理)
为什么走 planDetailId → plan_detail → plan_info 链路:
qc_inspection_main 无 machine_id 字段,只能通过生产计划链路反推。
为什么用 ${tableSuffix} 而非预编译:
prod_product_plan_detail 和 prod_plan_info 按车间分表_{workshopId}
表名需要运行时动态拼接Service 层已校验 tableSuffix 为合法数字
-->
<select id="selectQualifiedQtyByWorkshop" resultType="java.util.LinkedHashMap">
SELECT
pp.release_id AS machine_id,
SUM(ISNULL(m.qualified_qty, 0)) AS qualified_qty
FROM qc_inspection_main m
INNER JOIN prod_product_plan_detail_${tableSuffix} pd
ON pd.plan_detail_id = m.plan_detail_id
AND pd.del_flag = '0'
INNER JOIN prod_plan_info_${tableSuffix} pp
ON pp.plan_id = pd.plan_id
AND pp.release_type = '1'
WHERE m.tenant_id = #{tenantId}
AND m.inspection_start_time >= #{start}
AND m.inspection_start_time &lt; #{end}
AND m.status = '1'
AND m.del_flag = '0'
GROUP BY pp.release_id
</select>
<!-- 获取所有激活车间,用于 workshop 名称 → ID 解析 -->
<select id="selectActiveWorkshops" resultType="java.util.LinkedHashMap">
SELECT
workshop_id,
workshop_name
FROM prod_base_workshop_info
WHERE del_flag = '0'
</select>
<!-- ========================================================================
OEE 持久化层 — 批量 DELETE + INSERT
======================================================================== -->
<!-- 删除指定日期的 OEE 报表(幂等前提) -->
<delete id="deleteOeeReportByDate">
DELETE FROM dms_report_device_efficiency
WHERE stat_date = #{statDate}
AND tenant_id = #{tenantId}
</delete>
<!--
批量 INSERT OEE 报表(含 7 个 OEE 扩展列)
为什么不用 MyBatis-Plus saveBatchsaveBatch 默认逐条 INSERT数据量大时慢
手写批量 VALUES 一次提交500 条以内性能最优
-->
<insert id="insertOeeReport">
INSERT INTO dms_report_device_efficiency (
tenant_id, stat_date, workshop_id, class_team_id, shift_id, machine_id,
workshop_name, class_team_name, shift_name, machine_name,
output_qty, process_std_machine_hours,
debug_hours, run_hours, standby_hours, fault_hours, shutdown_hours,
output_machine_hours, total_hours, uptime_rate, overall_efficiency, runtime_efficiency,
qualified_qty, ideal_cycle_time_sec,
availability_rate, performance_rate, quality_rate, oee_rate, teep_rate,
create_time
) VALUES
<foreach collection="list" item="d" separator=",">
(
#{tenantId}, #{statDate},
#{d.workshopId}, #{d.classTeamId}, #{d.shiftId}, #{d.machineId},
#{d.workshopName}, #{d.classTeamName}, #{d.shiftName}, #{d.machineName},
#{d.outputQty}, #{d.processStdMachineHours},
#{d.debugHours}, #{d.runHours}, #{d.standbyHours}, #{d.faultHours}, #{d.shutdownHours},
#{d.outputMachineHours}, #{d.totalHours}, #{d.uptimeRate}, #{d.overallEfficiency}, #{d.runtimeEfficiency},
#{d.qualifiedQty}, #{d.idealCycleTimeSec},
#{d.availabilityRate}, #{d.performanceRate}, #{d.qualityRate}, #{d.oeeRate}, #{d.teepRate},
GETDATE()
)
</foreach>
</insert>
</mapper>

@ -74,7 +74,8 @@ public class ReverseTraceController extends BaseController {
@GetMapping("/workOrder/materialInputs")
public R<List<TraceMaterialInputVo>> listMaterialInputs(
@RequestParam Long planId,
@RequestParam String industryType) {
@RequestParam String industryType)
{
List<TraceMaterialInputVo> list = reverseTraceService.listMaterialInputs(planId, industryType);
return R.ok(list);
}

Loading…
Cancel
Save