底稿(不维护):本文是早期知识底稿,主线已改为 工业数字化实验室(Mini MES · 判断 · 实验)。请勿当作当前方案。
05-业务抽象设计(核心🔥)
前置知识: 04-工业可视化(SCADA)
🎯 学习目标
掌握工业数字化系统的核心业务建模能力:
- ✅ 设备模型抽象与层级关系
- ✅ 工单流程设计与状态机
- ✅ 生产节拍计算与瓶颈分析
- ✅ OEE(设备综合效率)深度解析
- ✅ 质量追溯体系设计
💡 本章是整个系列的灵魂,理解了业务抽象,才能设计出真正可用的系统。
🏭 业务场景梳理
在开始编码前,先明确我们要解决的业务问题:
典型中小企业制造场景
某机械加工厂有 3 条产线,每条产线包含:
- 5 台 CNC 机床
- 2 台工业机器人
- 1 台质检设备
管理层关心的问题:
1. 哪些设备在运行?哪些停机了?为什么停机?
2. 今天的产量达标了吗?哪条产线效率最高?
3. 设备 OEE 是多少?如何提升?
4. 出现不良品时,能追溯到哪个批次、哪台设备、哪个操作工吗?
5. 下个月的产能规划怎么做?我们的任务: 将这些业务问题转化为数据模型和算法。
📦 核心领域模型
1. 设备模型设计
1.1 设备层级关系
工厂 (Factory)
└─ 车间 (Workshop)
└─ 产线 (ProductionLine)
└─ 工位 (Workstation)
└─ 设备 (Device)
└─ 测点 (DataPoint)1.2 数据库表设计
sql
-- 工厂表
CREATE TABLE factory (
id VARCHAR(32) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) UNIQUE NOT NULL,
address VARCHAR(200),
manager VARCHAR(50),
phone VARCHAR(20),
status TINYINT DEFAULT 1, -- 1:启用 0:停用
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 车间表
CREATE TABLE workshop (
id VARCHAR(32) PRIMARY KEY,
factory_id VARCHAR(32) NOT NULL,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
manager VARCHAR(50),
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
FOREIGN KEY (factory_id) REFERENCES factory(id)
);
-- 产线表
CREATE TABLE production_line (
id VARCHAR(32) PRIMARY KEY,
workshop_id VARCHAR(32) NOT NULL,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
planned_capacity INT, -- 计划产能(件/小时)
cycle_time INT, -- 标准节拍(秒)
status TINYINT DEFAULT 1,
FOREIGN KEY (workshop_id) REFERENCES workshop(id)
);
-- 工位表
CREATE TABLE workstation (
id VARCHAR(32) PRIMARY KEY,
production_line_id VARCHAR(32) NOT NULL,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL,
sequence INT, -- 工序顺序
standard_time INT, -- 标准工时(秒)
FOREIGN KEY (production_line_id) REFERENCES production_line(id)
);
-- 设备表(核心)
CREATE TABLE device (
id VARCHAR(32) PRIMARY KEY,
workstation_id VARCHAR(32),
name VARCHAR(100) NOT NULL,
code VARCHAR(50) UNIQUE NOT NULL,
type VARCHAR(50), -- 设备类型:CNC、Robot、Sensor
brand VARCHAR(50), -- 品牌
model VARCHAR(50), -- 型号
purchase_date DATE, -- 采购日期
install_location VARCHAR(100), -- 安装位置
-- 通信配置
protocol_type VARCHAR(20), -- MODBUS_TCP, S7, OPC_UA, MQTT
ip_address VARCHAR(50),
port INT,
slave_id INT,
-- 状态管理
status TINYINT DEFAULT 1, -- 1:在线 0:离线 2:维护 3:报废
run_status TINYINT DEFAULT 0, -- 0:停机 1:运行 2:待机 3:告警
-- 责任人
operator_id VARCHAR(32), -- 当前操作工
maintainer_id VARCHAR(32), -- 维护负责人
-- 统计字段(冗余,提高查询性能)
total_running_hours DECIMAL(10, 2) DEFAULT 0, -- 累计运行时长
last_maintenance_date DATE, -- 上次维护日期
next_maintenance_date DATE, -- 下次维护日期
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (workstation_id) REFERENCES workstation(id)
);
-- 设备测点表
CREATE TABLE device_data_point (
id VARCHAR(32) PRIMARY KEY,
device_id VARCHAR(32) NOT NULL,
point_name VARCHAR(50) NOT NULL, -- 测点名称:temperature, pressure
point_code VARCHAR(50) NOT NULL, -- 测点编码:T001, P001
data_type VARCHAR(20), -- INTEGER, FLOAT, BOOLEAN, STRING
unit VARCHAR(20), -- 单位:°C, MPa, A
min_value FLOAT, -- 最小值
max_value FLOAT, -- 最大值
alarm_threshold_high FLOAT, -- 高报警阈值
alarm_threshold_low FLOAT, -- 低报警阈值
sampling_interval INT DEFAULT 1, -- 采集间隔(秒)
enabled TINYINT DEFAULT 1,
UNIQUE KEY uk_device_point (device_id, point_code),
FOREIGN KEY (device_id) REFERENCES device(id)
);1.3 Java 实体类
java
@Data
@TableName("device")
public class Device {
@TableId(type = IdType.ASSIGN_ID)
private String id;
private String workstationId;
private String name;
private String code;
private String type;
private String brand;
private String model;
// 通信配置
private ProtocolType protocolType;
private String ipAddress;
private Integer port;
private Integer slaveId;
// 状态
private DeviceStatus status;
private RunStatus runStatus;
// 责任人
private String operatorId;
private String maintainerId;
// 统计
private BigDecimal totalRunningHours;
private LocalDate lastMaintenanceDate;
private LocalDate nextMaintenanceDate;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 判断设备是否可用
*/
public boolean isAvailable() {
return this.status == DeviceStatus.ONLINE
&& this.runStatus != RunStatus.MAINTENANCE;
}
/**
* 获取设备完整路径
*/
public String getFullPath() {
// 通过关联查询获取:工厂 > 车间 > 产线 > 工位 > 设备
return "Factory-A > Workshop-1 > Line-01 > WS-01 > " + this.name;
}
}
public enum DeviceStatus {
ONLINE("在线"),
OFFLINE("离线"),
MAINTENANCE("维护中"),
SCRAPPED("已报废");
private final String description;
DeviceStatus(String description) {
this.description = description;
}
}
public enum RunStatus {
STOPPED("停机"),
RUNNING("运行中"),
IDLE("待机"),
ALARM("告警");
private final String description;
RunStatus(String description) {
this.description = description;
}
}2. 工单管理系统
2.1 工单生命周期
创建 → 审核 → 下发 → 执行中 → 完成 → 验收
↓ ↓
驳回 不合格返工2.2 工单表设计
sql
-- 工单表
CREATE TABLE work_order (
id VARCHAR(32) PRIMARY KEY,
order_no VARCHAR(50) UNIQUE NOT NULL, -- 工单号:WO20240101001
product_id VARCHAR(32) NOT NULL, -- 产品 ID
product_name VARCHAR(100), -- 产品名称(冗余)
quantity INT NOT NULL, -- 计划数量
completed_quantity INT DEFAULT 0, -- 已完成数量
defective_quantity INT DEFAULT 0, -- 不良品数量
-- 工艺路线
process_route_id VARCHAR(32), -- 工艺路线 ID
current_process_id VARCHAR(32), -- 当前工序
-- 排产信息
production_line_id VARCHAR(32), -- 指定产线
planned_start_time DATETIME, -- 计划开始时间
planned_end_time DATETIME, -- 计划结束时间
actual_start_time DATETIME, -- 实际开始时间
actual_end_time DATETIME, -- 实际结束时间
-- 状态
status VARCHAR(20) DEFAULT 'DRAFT', -- DRAFT, APPROVED, RELEASED, IN_PROGRESS, COMPLETED, ACCEPTED, REJECTED
priority TINYINT DEFAULT 2, -- 1:紧急 2:普通 3:低
-- 责任人
creator_id VARCHAR(32), -- 创建人
approver_id VARCHAR(32), -- 审核人
operator_id VARCHAR(32), -- 执行人
-- 备注
remark TEXT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status),
INDEX idx_production_line (production_line_id),
INDEX idx_planned_time (planned_start_time, planned_end_time)
);
-- 工单工序记录表
CREATE TABLE work_order_process (
id VARCHAR(32) PRIMARY KEY,
work_order_id VARCHAR(32) NOT NULL,
process_id VARCHAR(32) NOT NULL, -- 工序 ID
process_name VARCHAR(100), -- 工序名称
sequence INT, -- 工序顺序
workstation_id VARCHAR(32), -- 工位 ID
device_id VARCHAR(32), -- 设备 ID
-- 时间记录
start_time DATETIME, -- 开始时间
end_time DATETIME, -- 结束时间
duration INT, -- 实际耗时(秒)
standard_time INT, -- 标准工时(秒)
-- 产出
output_quantity INT DEFAULT 0, -- 产出数量
defective_quantity INT DEFAULT 0, -- 不良品数量
-- 状态
status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, IN_PROGRESS, COMPLETED, SKIPPED
-- 操作工
operator_id VARCHAR(32),
FOREIGN KEY (work_order_id) REFERENCES work_order(id),
INDEX idx_work_order (work_order_id)
);
-- 工单状态流转日志
CREATE TABLE work_order_status_log (
id VARCHAR(32) PRIMARY KEY,
work_order_id VARCHAR(32) NOT NULL,
from_status VARCHAR(20), -- 原状态
to_status VARCHAR(20), -- 新状态
operator_id VARCHAR(32), -- 操作人
remark VARCHAR(500), -- 备注
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (work_order_id) REFERENCES work_order(id),
INDEX idx_work_order (work_order_id)
);2.3 工单状态机
java
@Component
@Slf4j
public class WorkOrderStateMachine {
/**
* 状态转换规则
*/
private static final Map<String, Set<String>> TRANSITION_RULES = new HashMap<>();
static {
// DRAFT -> APPROVED, REJECTED
TRANSITION_RULES.put("DRAFT", Set.of("APPROVED", "REJECTED"));
// APPROVED -> RELEASED
TRANSITION_RULES.put("APPROVED", Set.of("RELEASED"));
// RELEASED -> IN_PROGRESS
TRANSITION_RULES.put("RELEASED", Set.of("IN_PROGRESS"));
// IN_PROGRESS -> COMPLETED, REJECTED
TRANSITION_RULES.put("IN_PROGRESS", Set.of("COMPLETED", "REJECTED"));
// COMPLETED -> ACCEPTED, REJECTED
TRANSITION_RULES.put("COMPLETED", Set.of("ACCEPTED", "REJECTED"));
}
/**
* 执行状态转换
*/
@Transactional
public void transition(String workOrderId, String targetStatus, String operatorId, String remark) {
WorkOrder workOrder = workOrderMapper.selectById(workOrderId);
if (workOrder == null) {
throw new BusinessException("工单不存在");
}
String currentStatus = workOrder.getStatus();
// 验证状态转换是否合法
if (!canTransition(currentStatus, targetStatus)) {
throw new BusinessException(
String.format("不允许从 %s 转换到 %s", currentStatus, targetStatus)
);
}
// 更新工单状态
workOrder.setStatus(targetStatus);
// 特殊逻辑处理
handleStatusChange(workOrder, currentStatus, targetStatus);
workOrderMapper.updateById(workOrder);
// 记录状态日志
saveStatusLog(workOrderId, currentStatus, targetStatus, operatorId, remark);
log.info("工单 {} 状态变更: {} -> {}", workOrderId, currentStatus, targetStatus);
}
private boolean canTransition(String fromStatus, String toStatus) {
Set<String> allowedTargets = TRANSITION_RULES.get(fromStatus);
return allowedTargets != null && allowedTargets.contains(toStatus);
}
private void handleStatusChange(WorkOrder workOrder, String fromStatus, String toStatus) {
switch (toStatus) {
case "RELEASED":
// 下发工单:分配产线和操作工
assignResources(workOrder);
break;
case "IN_PROGRESS":
// 开始执行:记录实际开始时间
workOrder.setActualStartTime(LocalDateTime.now());
break;
case "COMPLETED":
// 完成:记录实际结束时间,计算合格率
workOrder.setActualEndTime(LocalDateTime.now());
calculateQualityRate(workOrder);
break;
case "ACCEPTED":
// 验收合格:更新库存
updateInventory(workOrder);
break;
}
}
private void assignResources(WorkOrder workOrder) {
// 根据工艺路线自动分配产线和工位
ProcessRoute route = processRouteMapper.selectById(workOrder.getProcessRouteId());
if (route != null) {
workOrder.setProductionLineId(route.getDefaultProductionLineId());
}
}
private void calculateQualityRate(WorkOrder workOrder) {
int totalQuantity = workOrder.getCompletedQuantity();
int defectiveQuantity = workOrder.getDefectiveQuantity();
if (totalQuantity > 0) {
double qualityRate = (double) (totalQuantity - defectiveQuantity) / totalQuantity * 100;
log.info("工单 {} 合格率: {}%", workOrder.getId(), String.format("%.2f", qualityRate));
}
}
private void updateInventory(WorkOrder workOrder) {
// 调用库存服务,增加成品库存
inventoryService.addFinishedGoods(
workOrder.getProductId(),
workOrder.getCompletedQuantity() - workOrder.getDefectiveQuantity()
);
}
private void saveStatusLog(String workOrderId, String fromStatus,
String toStatus, String operatorId, String remark) {
WorkOrderStatusLog log = new WorkOrderStatusLog();
log.setWorkOrderId(workOrderId);
log.setFromStatus(fromStatus);
log.setToStatus(toStatus);
log.setOperatorId(operatorId);
log.setRemark(remark);
statusLogMapper.insert(log);
}
}3. OEE(设备综合效率)计算 🔥
3.1 OEE 定义
OEE = 可用率 × 性能率 × 合格率
| 指标 | 公式 | 说明 | 世界级水平 |
|---|---|---|---|
| 可用率 (Availability) | 运行时间 / 计划生产时间 | 衡量停机损失 | ≥ 90% |
| 性能率 (Performance) | (理论周期 × 总产量) / 运行时间 | 衡量速度损失 | ≥ 95% |
| 合格率 (Quality) | 合格品数量 / 总产量 | 衡量质量损失 | ≥ 99% |
| OEE | 可用率 × 性能率 × 合格率 | 综合效率 | ≥ 85% |
3.2 六大损失
设备时间损失
├─ 计划停机(休息、会议)→ 不计入 OEE
└─ 非计划停机
├─ 故障停机 → 影响可用率
├─ 换模调机 → 影响可用率
├─ 空转短停 → 影响性能率
├─ 降速运行 → 影响性能率
├─ 启动废品 → 影响合格率
└─ 生产废品 → 影响合格率3.3 数据模型
sql
-- 设备运行记录表
CREATE TABLE device_run_record (
id VARCHAR(32) PRIMARY KEY,
device_id VARCHAR(32) NOT NULL,
work_order_id VARCHAR(32), -- 关联工单
-- 时间段
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
duration INT NOT NULL, -- 时长(秒)
-- 状态
run_type VARCHAR(20), -- RUNNING:运行, IDLE:待机, DOWN:停机
down_reason VARCHAR(50), -- 停机原因:FAULT, CHANGE_OVER, NO_MATERIAL
-- 产量
produced_quantity INT DEFAULT 0, -- 生产数量
defective_quantity INT DEFAULT 0, -- 不良品数量
-- 理论参数
theoretical_cycle_time INT, -- 理论节拍(秒/件)
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_device_time (device_id, start_time, end_time),
FOREIGN KEY (device_id) REFERENCES device(id)
);
-- OEE 日报表(预计算,提高查询性能)
CREATE TABLE device_oee_daily (
id VARCHAR(32) PRIMARY KEY,
device_id VARCHAR(32) NOT NULL,
stat_date DATE NOT NULL,
-- 时间统计(秒)
planned_production_time INT, -- 计划生产时间
running_time INT, -- 运行时间
idle_time INT, -- 待机时间
down_time INT, -- 停机时间
-- 产量统计
total_quantity INT, -- 总产量
good_quantity INT, -- 合格品数量
defective_quantity INT, -- 不良品数量
-- OEE 指标
availability DECIMAL(5, 4), -- 可用率
performance DECIMAL(5, 4), -- 性能率
quality DECIMAL(5, 4), -- 合格率
oee DECIMAL(5, 4), -- OEE
-- 停机原因统计(JSON)
down_reasons JSON,
UNIQUE KEY uk_device_date (device_id, stat_date),
FOREIGN KEY (device_id) REFERENCES device(id)
);3.4 OEE 计算引擎
java
@Service
@Slf4j
public class OEECalculator {
@Autowired
private DeviceRunRecordMapper runRecordMapper;
@Autowired
private DeviceOEEDailyMapper oeeDailyMapper;
/**
* 计算设备单日 OEE
*/
@Transactional
public OEEResult calculateDailyOEE(String deviceId, LocalDate date) {
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
// 1. 查询当天的运行记录
List<DeviceRunRecord> records = runRecordMapper.selectList(
new LambdaQueryWrapper<DeviceRunRecord>()
.eq(DeviceRunRecord::getDeviceId, deviceId)
.ge(DeviceRunRecord::getStartTime, dayStart)
.lt(DeviceRunRecord::getEndTime, dayEnd)
);
if (records.isEmpty()) {
return OEEResult.zero();
}
// 2. 计算各项时间
TimeStatistics timeStats = calculateTimeStatistics(records);
// 3. 计算产量统计
ProductionStatistics prodStats = calculateProductionStatistics(records);
// 4. 计算 OEE 三个指标
double availability = calculateAvailability(timeStats);
double performance = calculatePerformance(timeStats, prodStats);
double quality = calculateQuality(prodStats);
double oee = availability * performance * quality;
// 5. 保存结果
DeviceOEEDaily oeeDaily = new DeviceOEEDaily();
oeeDaily.setDeviceId(deviceId);
oeeDaily.setStatDate(date);
oeeDaily.setPlannedProductionTime(timeStats.getPlannedTime());
oeeDaily.setRunningTime(timeStats.getRunningTime());
oeeDaily.setIdleTime(timeStats.getIdleTime());
oeeDaily.setDownTime(timeStats.getDownTime());
oeeDaily.setTotalQuantity(prodStats.getTotalQuantity());
oeeDaily.setGoodQuantity(prodStats.getGoodQuantity());
oeeDaily.setDefectiveQuantity(prodStats.getDefectiveQuantity());
oeeDaily.setAvailability(BigDecimal.valueOf(availability));
oeeDaily.setPerformance(BigDecimal.valueOf(performance));
oeeDaily.setQuality(BigDecimal.valueOf(quality));
oeeDaily.setOee(BigDecimal.valueOf(oee));
oeeDaily.setDownReasons(JSON.toJSONString(timeStats.getDownReasonStats()));
oeeDailyMapper.insertOrUpdate(oeeDaily);
log.info("设备 {} 日期 {} OEE: {}% (A:{}% P:{}% Q:{}%)",
deviceId, date,
String.format("%.2f", oee * 100),
String.format("%.2f", availability * 100),
String.format("%.2f", performance * 100),
String.format("%.2f", quality * 100)
);
return new OEEResult(availability, performance, quality, oee);
}
/**
* 计算时间统计
*/
private TimeStatistics calculateTimeStatistics(List<DeviceRunRecord> records) {
int runningTime = 0;
int idleTime = 0;
int downTime = 0;
Map<String, Integer> downReasonStats = new HashMap<>();
for (DeviceRunRecord record : records) {
int duration = record.getDuration();
switch (record.getRunType()) {
case "RUNNING":
runningTime += duration;
break;
case "IDLE":
idleTime += duration;
break;
case "DOWN":
downTime += duration;
// 统计停机原因
String reason = record.getDownReason() != null ?
record.getDownReason() : "UNKNOWN";
downReasonStats.merge(reason, duration, Integer::sum);
break;
}
}
int plannedTime = runningTime + idleTime + downTime;
return new TimeStatistics(plannedTime, runningTime, idleTime, downTime, downReasonStats);
}
/**
* 计算产量统计
*/
private ProductionStatistics calculateProductionStatistics(List<DeviceRunRecord> records) {
int totalQuantity = records.stream()
.mapToInt(DeviceRunRecord::getProducedQuantity)
.sum();
int defectiveQuantity = records.stream()
.mapToInt(DeviceRunRecord::getDefectiveQuantity)
.sum();
int goodQuantity = totalQuantity - defectiveQuantity;
return new ProductionStatistics(totalQuantity, goodQuantity, defectiveQuantity);
}
/**
* 计算可用率
*/
private double calculateAvailability(TimeStatistics timeStats) {
if (timeStats.getPlannedTime() == 0) {
return 0;
}
return (double) timeStats.getRunningTime() / timeStats.getPlannedTime();
}
/**
* 计算性能率
*/
private double calculatePerformance(TimeStatistics timeStats, ProductionStatistics prodStats) {
if (timeStats.getRunningTime() == 0 || prodStats.getTotalQuantity() == 0) {
return 0;
}
// 获取理论节拍(从设备配置或工艺路线)
int theoreticalCycleTime = getTheoreticalCycleTime();
// 性能率 = (理论周期 × 总产量) / 运行时间
double idealTime = theoreticalCycleTime * prodStats.getTotalQuantity();
return idealTime / timeStats.getRunningTime();
}
/**
* 计算合格率
*/
private double calculateQuality(ProductionStatistics prodStats) {
if (prodStats.getTotalQuantity() == 0) {
return 0;
}
return (double) prodStats.getGoodQuantity() / prodStats.getTotalQuantity();
}
private int getTheoreticalCycleTime() {
// 从设备或工艺路线获取理论节拍
// 这里简化处理,实际应从数据库查询
return 30; // 30 秒/件
}
}
@Data
@AllArgsConstructor
class TimeStatistics {
private int plannedTime;
private int runningTime;
private int idleTime;
private int downTime;
private Map<String, Integer> downReasonStats;
}
@Data
@AllArgsConstructor
class ProductionStatistics {
private int totalQuantity;
private int goodQuantity;
private int defectiveQuantity;
}
@Data
@AllArgsConstructor
class OEEResult {
private double availability;
private double performance;
private double quality;
private double oee;
public static OEEResult zero() {
return new OEEResult(0, 0, 0, 0);
}
}4. 生产节拍分析
4.1 节拍定义
- 理论节拍: 工艺设计的标准时间(如 30 秒/件)
- 实际节拍: 实际生产的平均时间
- 节拍偏差: 实际节拍 - 理论节拍
4.2 瓶颈工位识别
java
@Service
public class BottleneckAnalyzer {
/**
* 分析产线瓶颈工位
*/
public BottleneckResult analyzeBottleneck(String productionLineId, LocalDate date) {
// 1. 获取产线所有工位的实际节拍
List<WorkstationCycleTime> cycleTimes = queryWorkstationCycleTimes(
productionLineId, date
);
// 2. 找出最慢的工位(瓶颈)
WorkstationCycleTime bottleneck = cycleTimes.stream()
.max(Comparator.comparingDouble(WorkstationCycleTime::getActualCycleTime))
.orElse(null);
// 3. 计算平衡率
double balanceRate = calculateBalanceRate(cycleTimes);
return new BottleneckResult(bottleneck, cycleTimes, balanceRate);
}
/**
* 计算产线平衡率
* 平衡率 = 各工位工时总和 / (瓶颈工位工时 × 工位数)
*/
private double calculateBalanceRate(List<WorkstationCycleTime> cycleTimes) {
if (cycleTimes.isEmpty()) {
return 0;
}
double totalTime = cycleTimes.stream()
.mapToDouble(WorkstationCycleTime::getActualCycleTime)
.sum();
double bottleneckTime = cycleTimes.stream()
.mapToDouble(WorkstationCycleTime::getActualCycleTime)
.max()
.orElse(0);
int workstationCount = cycleTimes.size();
return totalTime / (bottleneckTime * workstationCount);
}
}
@Data
class WorkstationCycleTime {
private String workstationId;
private String workstationName;
private int standardTime; // 标准工时
private double actualCycleTime; // 实际节拍
private int outputQuantity; // 产出数量
}
@Data
class BottleneckResult {
private WorkstationCycleTime bottleneck;
private List<WorkstationCycleTime> allWorkstations;
private double balanceRate;
}5. 质量追溯体系
5.1 追溯链设计
原材料批次 → 入库检验 → 领料 → 生产过程 → 成品检验 → 出库
↑ ↑
检验报告 检验报告
↑ ↑
供应商信息 客户信息5.2 追溯表设计
sql
-- 物料批次表
CREATE TABLE material_batch (
id VARCHAR(32) PRIMARY KEY,
material_code VARCHAR(50) NOT NULL, -- 物料编码
batch_no VARCHAR(50) NOT NULL, -- 批次号
supplier_id VARCHAR(32), -- 供应商
quantity DECIMAL(10, 2), -- 数量
inbound_date DATE, -- 入库日期
expiry_date DATE, -- 有效期
inspection_report_id VARCHAR(32), -- 检验报告
UNIQUE KEY uk_material_batch (material_code, batch_no)
);
-- 生产追溯记录表
CREATE TABLE production_traceability (
id VARCHAR(32) PRIMARY KEY,
work_order_id VARCHAR(32) NOT NULL,
product_serial_no VARCHAR(50) NOT NULL, -- 产品序列号
-- 物料使用
material_batch_ids JSON, -- 使用的物料批次 ID 列表
-- 生产过程
production_line_id VARCHAR(32),
device_id VARCHAR(32),
operator_id VARCHAR(32),
-- 时间
produce_time DATETIME,
-- 工艺参数(JSON,记录关键参数)
process_parameters JSON,
-- 质检
inspection_result VARCHAR(20), -- PASS, FAIL
inspection_report_id VARCHAR(32),
UNIQUE KEY uk_product_serial (product_serial_no),
INDEX idx_work_order (work_order_id),
INDEX idx_material_batch ((CAST(material_batch_ids AS CHAR(500))))
);
-- 质检报告表
CREATE TABLE inspection_report (
id VARCHAR(32) PRIMARY KEY,
report_no VARCHAR(50) UNIQUE NOT NULL, -- 报告编号
type VARCHAR(20), -- IQC:来料检验, IPQC:过程检验, FQC:最终检验
product_id VARCHAR(32),
batch_no VARCHAR(50), -- 批次号或序列号
-- 检验项目
inspection_items JSON, -- [{item: "尺寸", standard: "10±0.1", actual: "10.05", result: "PASS"}]
-- 结论
conclusion VARCHAR(20), -- PASS, FAIL, CONcession
defect_description TEXT, -- 不良描述
-- 检验人
inspector_id VARCHAR(32),
inspect_time DATETIME,
-- 附件(照片、文档)
attachments JSON,
INDEX idx_product (product_id),
INDEX idx_batch (batch_no)
);5.3 追溯查询服务
java
@Service
public class TraceabilityService {
/**
* 正向追溯:从原材料到成品
*/
public ForwardTraceResult forwardTrace(String materialBatchNo) {
// 1. 查询使用该批次物料的生產记录
List<ProductionTraceability> traces = traceabilityMapper.selectByMaterialBatch(materialBatchNo);
// 2. 关联工单和产品信息
List<ProductInfo> products = traces.stream()
.map(trace -> {
WorkOrder workOrder = workOrderMapper.selectById(trace.getWorkOrderId());
Product product = productMapper.selectById(workOrder.getProductId());
return new ProductInfo(
trace.getProductSerialNo(),
product.getName(),
trace.getProduceTime(),
trace.getInspectionResult()
);
})
.collect(Collectors.toList());
return new ForwardTraceResult(materialBatchNo, products);
}
/**
* 反向追溯:从成品到原材料
*/
public BackwardTraceResult backwardTrace(String productSerialNo) {
// 1. 查询产品生产记录
ProductionTraceability trace = traceabilityMapper.selectBySerialNo(productSerialNo);
if (trace == null) {
throw new BusinessException("未找到产品追溯记录");
}
// 2. 查询使用的物料批次
List<String> materialBatchIds = JSON.parseArray(
trace.getMaterialBatchIds(), String.class
);
List<MaterialBatch> materials = materialBatchMapper.selectBatchIds(materialBatchIds);
// 3. 查询供应商信息
List<SupplierInfo> suppliers = materials.stream()
.map(batch -> {
Supplier supplier = supplierMapper.selectById(batch.getSupplierId());
return new SupplierInfo(
batch.getBatchNo(),
batch.getMaterialCode(),
supplier.getName(),
batch.getInboundDate()
);
})
.collect(Collectors.toList());
return new BackwardTraceResult(productSerialNo, suppliers);
}
}📊 业务报表设计
1. 生产日报
sql
-- 生产日报视图
CREATE VIEW daily_production_report AS
SELECT
DATE(wr.start_time) as stat_date,
pl.name as production_line_name,
COUNT(DISTINCT wr.work_order_id) as order_count,
SUM(wr.produced_quantity) as total_output,
SUM(wr.defective_quantity) as total_defective,
ROUND(SUM(wr.produced_quantity - wr.defective_quantity) * 100.0 /
NULLIF(SUM(wr.produced_quantity), 0), 2) as quality_rate,
ROUND(AVG(wr.duration), 2) as avg_cycle_time
FROM device_run_record wr
JOIN device d ON wr.device_id = d.id
JOIN workstation ws ON d.workstation_id = ws.id
JOIN production_line pl ON ws.production_line_id = pl.id
WHERE wr.run_type = 'RUNNING'
GROUP BY DATE(wr.start_time), pl.name;2. 设备利用率报表
java
@Service
public class DeviceUtilizationReport {
/**
* 生成设备利用率月报
*/
public List<DeviceUtilizationDTO> generateMonthlyReport(String factoryId, YearMonth month) {
LocalDate startDate = month.atDay(1);
LocalDate endDate = month.atEndOfMonth();
// 查询所有设备
List<Device> devices = deviceMapper.selectByFactory(factoryId);
return devices.stream().map(device -> {
DeviceUtilizationDTO dto = new DeviceUtilizationDTO();
dto.setDeviceId(device.getId());
dto.setDeviceName(device.getName());
// 查询 OEE 数据
List<DeviceOEEDaily> oeeRecords = oeeDailyMapper.selectList(
new LambdaQueryWrapper<DeviceOEEDaily>()
.eq(DeviceOEEDaily::getDeviceId, device.getId())
.ge(DeviceOEEDaily::getStatDate, startDate)
.le(DeviceOEEDaily::getStatDate, endDate)
);
// 计算平均值
if (!oeeRecords.isEmpty()) {
double avgOEE = oeeRecords.stream()
.mapToDouble(r -> r.getOee().doubleValue())
.average()
.orElse(0);
double avgAvailability = oeeRecords.stream()
.mapToDouble(r -> r.getAvailability().doubleValue())
.average()
.orElse(0);
dto.setAvgOEE(avgOEE);
dto.setAvgAvailability(avgAvailability);
dto.setTotalRunningHours(oeeRecords.stream()
.mapToInt(DeviceOEEDaily::getRunningTime)
.sum() / 3600.0);
}
return dto;
}).collect(Collectors.toList());
}
}💡 最佳实践
1. 数据冗余 vs 实时计算
java
/**
* 策略:高频查询字段冗余,低频查询实时计算
*/
// ✅ 冗余:设备当前状态(频繁查询)
UPDATE device SET run_status = 1 WHERE id = 'xxx';
// ✅ 实时计算:历史 OEE 趋势(低频查询)
SELECT AVG(oee) FROM device_oee_daily
WHERE device_id = 'xxx'
AND stat_date >= DATE_SUB(NOW(), INTERVAL 30 DAY);2. 状态机防并发
java
@Transactional
public void transition(String workOrderId, String targetStatus) {
// 使用乐观锁防止并发修改
WorkOrder workOrder = workOrderMapper.selectById(workOrderId);
int rows = workOrderMapper.update(null,
new LambdaUpdateWrapper<WorkOrder>()
.eq(WorkOrder::getId, workOrderId)
.eq(WorkOrder::getStatus, workOrder.getStatus()) // 乐观锁
.set(WorkOrder::getStatus, targetStatus)
);
if (rows == 0) {
throw new BusinessException("状态已被其他用户修改,请刷新后重试");
}
}3. OEE 定时计算任务
java
@Component
public class OEECalculationTask {
/**
* 每天凌晨 1 点计算前一天的 OEE
*/
@Scheduled(cron = "0 0 1 * * ?")
public void calculateYesterdayOEE() {
LocalDate yesterday = LocalDate.now().minusDays(1);
// 获取所有设备
List<Device> devices = deviceMapper.selectAll();
for (Device device : devices) {
try {
oeeCalculator.calculateDailyOEE(device.getId(), yesterday);
} catch (Exception e) {
log.error("计算设备 {} OEE 失败", device.getId(), e);
}
}
}
}🔗 相关资源
下一步: 学习 06-轻量MES能力实现,将业务模型落地为具体功能模块。