生产工单 (mes_pro_work_order)
├── 工单工序 (mes_pro_work_order_process) -- 工单级别的工序安排
├── 工单BOM (mes_pro_work_order_bom) -- 工单级别的物料清单
└── 生产任务 (mes_pro_task) -- 排产后的生产任务
├── 任务投料 (mes_pro_task_issue) -- 任务级别的投料记录
└── 报工单 (mes_pro_feedback) -- 任务级别的报工记录
├── 物料消耗 (mes_wm_item_consume) -- 报工消耗的物料
└── 产出单 (mes_wm_product_produce) -- 报工产出的产品
| 实体 | 状态流转 | 触发条件 |
|---|---|---|
| 生产工单 | 草稿 → 已确认 → 已完成/已取消 | 手动确认、手动完成/取消 |
| 生产任务 | 草稿 → 已完成/已取消 | 工单完成/取消时级联更新 |
| 报工单 | 草稿 → 审批中 → 待检验/已完成 → 已完成 | 手动提交、审批通过 |
MesProWorkOrderProcessDO 记录工单的工序安排,但创建任务时没有校验工序是否在工单工序中quantityScheduled(已排产数量)字段存在但没有累加逻辑新增任务状态枚举值:
| 状态 | 值 | 说明 | 进入条件 |
|---|---|---|---|
| PREPARE | 0 | 草稿 | 任务创建时 |
| PRODUCING | 1 | 生产中 | 首次报工审批通过后 |
| PARTIAL_FINISHED | 2 | 部分完成 | 已生产数量 > 0 且 < 排产数量 |
| FINISHED | 3 | 已完成 | 已生产数量 >= 排产数量 或 工单完成 |
| CANCELED | 4 | 已取消 | 工单取消时 |
修改文件:MesProTaskStatusEnum.java
public enum MesProTaskStatusEnum implements ArrayValuable<Integer> {
PREPARE(0, "草稿"),
PRODUCING(1, "生产中"),
PARTIAL_FINISHED(2, "部分完成"),
FINISHED(3, "已完成"),
CANCELED(4, "已取消");
/**
* 判断是否为终态
*/
public static boolean isEndStatus(Integer status) {
return ObjectUtils.equalsAny(status, FINISHED.status, CANCELED.status);
}
/**
* 判断是否可以报工(草稿或非终态)
*/
public static boolean canFeedback(Integer status) {
return !isEndStatus(status);
}
}
新增字段(已存在,补充逻辑):
| 字段 | 类型 | 说明 |
|---|---|---|
| quantityScheduled | decimal(24,6) | 已排产数量(关键工序任务数量之和) |
| quantityProduced | decimal(24,6) | 已生产数量(关键工序报工数量之和) |
新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| route_id | bigint | 工艺路线编号(确认时从产品默认路线获取或手动选择) |
| auto_finish_flag | tinyint(1) | 是否自动完成(默认 true,报工达标后自动完成工单) |
新增字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| first_feedback_time | datetime | 首次报工时间(状态变为生产中时记录) |
| last_feedback_time | datetime | 最近报工时间 |
数据库脚本:
-- 任务表新增字段
ALTER TABLE mes_pro_task ADD COLUMN first_feedback_time datetime COMMENT '首次报工时间' AFTER cancel_date;
ALTER TABLE mes_pro_task ADD COLUMN last_feedback_time datetime COMMENT '最近报工时间' AFTER first_feedback_time;
-- 工单表新增字段
ALTER TABLE mes_pro_work_order ADD COLUMN route_id bigint COMMENT '工艺路线编号' AFTER batch_code;
ALTER TABLE mes_pro_work_order ADD COLUMN auto_finish_flag tinyint(1) DEFAULT 1 COMMENT '是否自动完成' AFTER route_id;
规则:
- 创建任务时校验:工单.已排产数量 + 本次排产数量 <= 工单.生产数量
- 任务创建成功后累加工单的 quantityScheduled
- 任务删除时扣减工单的 quantityScheduled
接口变更:MesProWorkOrderService
/**
* 累加工单的已排产数量
*
* @param id 工单编号
* @param incrQuantityScheduled 本次排产数量增量
*/
void updateScheduledQuantity(Long id, BigDecimal incrQuantityScheduled);
Mapper 新增方法:MesProWorkOrderMapper
@Update("UPDATE mes_pro_work_order SET quantity_scheduled = quantity_scheduled + #{incrQuantity} " +
"WHERE id = #{id} AND deleted = 0")
void updateScheduledQuantity(@Param("id") Long id, @Param("incrQuantity") BigDecimal incrQuantity);
接口设计:
/**
* 一键排产:根据工艺路线自动生成所有工序任务
*
* @param workOrderId 工单编号
* @param routeId 工艺路线编号(可选,不传则使用工单关联的工艺路线)
* @return 生成的任务ID列表
*/
List<Long> autoGenerateTasks(Long workOrderId, Long routeId);
业务逻辑:
mes_pro_work_order_process)Controller 接口:
@PostMapping("/auto-generate")
@Operation(summary = "一键排产")
@PreAuthorize("@ss.hasPermission('mes:pro-task:create')")
public CommonResult<List<Long>> autoGenerateTasks(
@RequestParam("workOrderId") Long workOrderId,
@RequestParam(value = "routeId", required = false) Long routeId) {
return success(taskService.autoGenerateTasks(workOrderId, routeId));
}
触发时机:报工审批通过后
状态流转规则:
当前状态 报工后状态判断
─────────────────────────────────────────────────
草稿(0) → 生产中(1) 或 部分完成(2) 或 已完成(3)
生产中(1) → 生产中(1) 或 部分完成(2) 或 已完成(3)
部分完成(2)→ 部分完成(2) 或 已完成(3)
判断逻辑:
if (producedQuantity.compareTo(quantity) >= 0) {
// 已生产 >= 计划 → 已完成
newStatus = MesProTaskStatusEnum.FINISHED;
} else if (producedQuantity.compareTo(BigDecimal.ZERO) > 0) {
// 0 < 已生产 < 计划 → 部分完成(或保持生产中)
newStatus = MesProTaskStatusEnum.PARTIAL_FINISHED;
} else {
// 已生产 = 0 且有报工 → 生产中(首次报工)
newStatus = MesProTaskStatusEnum.PRODUCING;
}
规则:
- 所有关键工序的报工审批通过后,累加工单的 quantityProduced
- 非关键工序的报工不更新工单进度(只更新任务进度)
触发时机:报工审批通过后
// MesProFeedbackServiceImpl#approveFeedback
if (keyFlag) {
// 关键工序:更新工单已生产数量
workOrderService.updateProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity());
// 检查是否自动完成
checkAndAutoFinishWorkOrder(feedback.getWorkOrderId());
}
规则:
- 当工单的 autoFinishFlag = true 时,检查是否满足自动完成条件
- 条件:所有关键工序任务的已生产数量 >= 工单生产数量
- 或者:工单下所有任务状态都为「已完成」
接口设计:
/**
* 检查并自动完成工单
*
* @param workOrderId 工单编号
* @return true=已自动完成, false=未满足条件
*/
boolean checkAndAutoFinishWorkOrder(Long workOrderId);
业务逻辑:
public boolean checkAndAutoFinishWorkOrder(Long workOrderId) {
MesProWorkOrderDO workOrder = getWorkOrder(workOrderId);
if (workOrder == null || !Boolean.TRUE.equals(workOrder.getAutoFinishFlag())) {
return false;
}
if (workOrder.getStatus() != MesProWorkOrderStatusEnum.CONFIRMED.getStatus()) {
return false;
}
// 检查:已生产数量 >= 生产数量
if (workOrder.getQuantityProduced().compareTo(workOrder.getQuantity()) >= 0) {
finishWorkOrder(workOrderId);
return true;
}
return false;
}
规则:
- 创建任务时,校验工序必须在工单工序列表中
- 如果工单没有工序记录,允许创建(兼容老数据)
修改:MesProTaskServiceImpl#createTask
// 校验工序在工单工序中(如果工单有工序配置)
List<MesProWorkOrderProcessDO> workOrderProcesses = workOrderProcessService.getListByWorkOrderId(createReqVO.getWorkOrderId());
if (CollUtil.isNotEmpty(workOrderProcesses)) {
boolean processExists = workOrderProcesses.stream()
.anyMatch(p -> ObjUtil.equal(p.getProcessId(), createReqVO.getProcessId()));
if (!processExists) {
throw exception(PRO_TASK_PROCESS_NOT_IN_WORK_ORDER);
}
}
设计思路:将工单、排产、报工整合到一个页面,通过 Tab 切换不同视图
页面结构:
┌─────────────────────────────────────────────────────────────────────┐
│ 工单中心 │
├─────────────────────────────────────────────────────────────────────┤
│ 【搜索条件区】 │
│ 工单编码: [____] 产品: [____] 状态: [全部 ▼] 客户: [____] │
│ [查询] [重置] │
├─────────────────────────────────────────────────────────────────────┤
│ 【工单列表区】 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 工单编码 │ 产品 │ 计划数量 │ 已排产 │ 已生产 │ 进度 │ 状态 │ 操作│ │
│ │ WO001 │ A │ 100 │ 100 │ 80 │ ████████░░ │ 生产中│ 详情│
│ │ WO002 │ B │ 50 │ 0 │ 0 │ ░░░░░░░░░░ │ 已确认│ 排产│
│ └─────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ 【工单详情区】(点击工单编码展开) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 【工序进度条】 │ │
│ │ [下料] → [机加工] → [热处理] → [质检] → [包装] │ │
│ │ ✓ 生产中 待生产 待生产 待生产 │ │
│ │ 100% 60% 0% 0% 0% │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ 【当前工序任务列表】(点击工序tab切换) │ │
│ │ [下料] [机加工] [热处理] [质检] [包装] │ │
│ │ ┌───────────────────────────────────────────────────────────────┐│ │
│ │ │ 任务编码 │ 工作站 │ 计划 │ 已报工 │ 进度 │ 状态 │ 操作 ││ │
│ │ │ T001 │ A-01 │ 60 │ 36 │ 60% │ 生产中│ [报工] ││ │
│ │ │ T002 │ A-02 │ 40 │ 0 │ 0% │ 草稿 │ [编辑] [删除]││ │
│ │ └───────────────────────────────────────────────────────────────┘│ │
│ │ [新增任务] [一键排产] │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
场景1:从任务列表报工
任务行操作列 → 点击「报工」按钮 → 弹出报工表单
┌────────────────────────────────────┐
│ 快速报工 │
├────────────────────────────────────┤
│ 任务编码: T001 (自动带入,不可改) │
│ 工作站: A-01 (自动带入) │
│ 计划数量: 60 (自动带入) │
│ 已报工: 36 (自动带入) │
│ ───────────────────────────── │
│ 本次报工数量: [____] * │
│ 合格品数量: [____] │
│ 不良品数量: [____] │
│ 备注: [________________] │
├────────────────────────────────────┤
│ [取消] [保存并提交] │
└────────────────────────────────────┘
场景2:批量报工
选中多个任务 → 点击「批量报工」→ 填写相同数量批量提交
┌────────────────────────────────────┐
│ 批量报工 │
├────────────────────────────────────┤
│ 已选择 3 个任务: │
│ - T001 (机加工, 计划60, 已报36) │
│ - T003 (机加工, 计划50, 已报20) │
│ - T005 (机加工, 计划40, 已报0) │
│ ───────────────────────────── │
│ 本次报工数量: [____] (应用到所有) │
│ 备注: [________________] │
├────────────────────────────────────┤
│ [取消] [保存并提交] │
└────────────────────────────────────┘
┌────────────────────────────────────────┐
│ 一键排产 │
├────────────────────────────────────────┤
│ 工单编号: WO002 │
│ 产品: 产品A │
│ 生产数量: 100 │
│ ───────────────────────────── │
│ 工艺路线: [产品A默认路线 ▼] │
│ │
│ 预生成任务预览: │
│ ┌────────────────────────────────────┐ │
│ │ 工序 │ 工作站 │ 数量 │ 时间 │ │
│ │ 下料 │ 自动 │ 100 │ 8:00 │ │
│ │ 机加工 │ 自动 │ 100 │ 10:00 │ │
│ │ 热处理 │ 自动 │ 100 │ 14:00 │ │
│ │ 质检 │ 自动 │ 100 │ 16:00 │ │
│ │ 包装 │ 自动 │ 100 │ 17:00 │ │
│ └────────────────────────────────────┘ │
│ ☑ 自动分配工作站 │
│ ☑ 自动计算生产时间 │
├────────────────────────────────────────┤
│ [取消] [确认排产] │
└────────────────────────────────────────┘
工单列表进度条:
<template #progress="{ row }">
<div class="flex items-center gap-2">
<Progress
:percent="calculateProgress(row)"
:stroke-color="getProgressColor(row.status)"
size="small"
/>
<span class="text-xs text-gray-500">
{{ row.quantityProduced }}/{{ row.quantity }}
</span>
</div>
</template>
工序进度条:
<div class="flex items-center gap-1">
<div
v-for="process in processList"
:key="process.processId"
class="flex-1 rounded px-2 py-1 text-center text-xs"
:class="getProcessStatusClass(process)"
>
<div class="font-medium">{{ process.processName }}</div>
<div class="text-gray-500">{{ process.progress }}%</div>
</div>
<RightOutlined class="text-gray-400" />
</div>
| 接口 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 一键排产 | POST | /mes/pro-task/auto-generate | 根据工艺路线自动生成所有工序任务 |
| 批量报工 | POST | /mes/pro-feedback/batch-create | 批量创建报工单 |
| 工单进度 | GET | /mes/pro-work-order/progress/{id} | 获取工单进度详情(含各工序进度) |
| 工单自动完成检查 | POST | /mes/pro-work-order/check-auto-finish/{id} | 检查并自动完成工单 |
| 接口 | 方法 | 路径 | 变更说明 |
|---|---|---|---|
| 创建任务 | POST | /mes/pro-task/create | 增加工序校验、排产数量校验、累加工单已排产数量 |
| 删除任务 | DELETE | /mes/pro-task/delete | 扣减工单已排产数量 |
| 审批报工 | POST | /mes/pro-feedback/approve | 增加任务状态流转逻辑、工单自动完成检查 |
| 确认工单 | POST | /mes/pro-work-order/confirm | 可选择关联工艺路线 |
| 工单详情 | GET | /mes/pro-work-order/get | 增加返回 routeId、quantityScheduled、quantityProduced |
@Data
public class MesProWorkOrderProgressRespVO {
/** 工单ID */
private Long workOrderId;
/** 工单编码 */
private String workOrderCode;
/** 生产数量 */
private BigDecimal quantity;
/** 已排产数量 */
private BigDecimal quantityScheduled;
/** 已生产数量 */
private BigDecimal quantityProduced;
/** 总进度百分比 */
private BigDecimal progressPercent;
/** 工序进度列表 */
private List<ProcessProgressVO> processList;
@Data
public static class ProcessProgressVO {
/** 工序ID */
private Long processId;
/** 工序编码 */
private String processCode;
/** 工序名称 */
private String processName;
/** 是否关键工序 */
private Boolean keyFlag;
/** 计划数量(关键工序) */
private BigDecimal plannedQuantity;
/** 已生产数量 */
private BigDecimal producedQuantity;
/** 进度百分比 */
private BigDecimal progressPercent;
/** 任务列表 */
private List<TaskProgressVO> taskList;
}
@Data
public static class TaskProgressVO {
/** 任务ID */
private Long taskId;
/** 任务编码 */
private String taskCode;
/** 工作站名称 */
private String workstationName;
/** 排产数量 */
private BigDecimal quantity;
/** 已生产数量 */
private BigDecimal producedQuantity;
/** 进度百分比 */
private BigDecimal progressPercent;
/** 任务状态 */
private Integer status;
}
}
@Data
public class MesProTaskAutoGenerateReqVO {
/** 工单ID */
@NotNull(message = "工单ID不能为空")
private Long workOrderId;
/** 工艺路线ID(可选,不传则使用工单关联的工艺路线) */
private Long routeId;
/** 是否自动分配工作站 */
private Boolean autoAssignWorkstation = true;
/** 是否自动计算生产时间 */
private Boolean autoCalculateTime = true;
}
@Data
public class MesProTaskAutoGeneratePreviewRespVO {
/** 工单ID */
private Long workOrderId;
/** 工艺路线ID */
private Long routeId;
/** 工艺路线名称 */
private String routeName;
/** 预生成任务列表 */
private List<TaskPreviewVO> taskList;
@Data
public static class TaskPreviewVO {
/** 工序ID */
private Long processId;
/** 工序名称 */
private String processName;
/** 工序顺序 */
private Integer sort;
/** 建议工作站ID */
private Long suggestWorkstationId;
/** 建议工作站名称 */
private String suggestWorkstationName;
/** 排产数量 */
private BigDecimal quantity;
/** 预计开始时间 */
private LocalDateTime suggestStartTime;
/** 预计结束时间 */
private LocalDateTime suggestEndTime;
/** 是否关键工序 */
private Boolean keyFlag;
}
}
@Data
public class MesProFeedbackBatchCreateReqVO {
/** 任务ID列表 */
@NotEmpty(message = "任务ID列表不能为空")
private List<Long> taskIds;
/** 本次报工数量(应用到所有任务) */
@NotNull(message = "报工数量不能为空")
@DecimalMin(value = "0.01", message = "报工数量必须大于0")
private BigDecimal feedbackQuantity;
/** 合格品数量(应用到所有任务,非质检工序必填) */
private BigDecimal qualifiedQuantity;
/** 不良品数量 */
private BigDecimal unqualifiedQuantity;
/** 备注 */
private String remark;
}
-- =============================================
-- 生产任务表新增字段
-- =============================================
ALTER TABLE mes_pro_task
ADD COLUMN first_feedback_time datetime COMMENT '首次报工时间' AFTER cancel_date,
ADD COLUMN last_feedback_time datetime COMMENT '最近报工时间' AFTER first_feedback_time;
-- =============================================
-- 生产工单表新增字段
-- =============================================
ALTER TABLE mes_pro_work_order
ADD COLUMN route_id bigint COMMENT '工艺路线编号' AFTER batch_code,
ADD COLUMN auto_finish_flag tinyint(1) DEFAULT 1 COMMENT '是否自动完成(报工达标后自动完成工单)' AFTER route_id;
-- =============================================
-- 补充索引
-- =============================================
ALTER TABLE mes_pro_task ADD INDEX idx_work_order_process (work_order_id, process_id);
-- 任务状态枚举扩展
-- 0=草稿, 1=生产中, 2=部分完成, 3=已完成, 4=已取消
-- 注意:需要更新字典数据 mes_pro_task_status
INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark)
VALUES
(1, '生产中', '1', 'mes_pro_task_status', 0, 'processing', '', '任务正在生产中'),
(2, '部分完成', '2', 'mes_pro_task_status', 0, 'warning', '', '任务部分完成');
-- 更新原有枚举的 sort
UPDATE system_dict_data SET sort = 0 WHERE dict_type = 'mes_pro_task_status' AND value = '0';
UPDATE system_dict_data SET sort = 3 WHERE dict_type = 'mes_pro_task_status' AND value = '3';
UPDATE system_dict_data SET sort = 4 WHERE dict_type = 'mes_pro_task_status' AND value = '4';
| 任务 | 说明 | 优先级 |
|---|---|---|
| 任务状态枚举扩展 | 新增 PRODUCING、PARTIAL_FINISHED 状态 | P0 |
| 数据库变更执行 | 执行 SQL 脚本 | P0 |
| 排产数量校验 | 创建/删除任务时校验和更新工单已排产数量 | P0 |
| 任务状态自动流转 | 报工审批后自动更新任务状态 | P0 |
| 工单进度自动更新 | 报工审批后更新工单已生产数量 | P0 |
| 工单自动完成检查 | 报工达标后自动完成工单 | P1 |
| 任务 | 说明 | 优先级 |
|---|---|---|
| 一键排产接口 | 根据工艺路线自动生成任务 | P0 |
| 工单进度接口 | 返回工单各工序进度详情 | P0 |
| 批量报工接口 | 批量创建报工单 | P1 |
| 任务 | 说明 | 优先级 |
|---|---|---|
| 工单中心页面 | 整合工单、排产、报工到一个页面 | P0 |
| 工序进度条组件 | 可视化展示各工序进度 | P0 |
| 快速报工入口 | 任务行直接报工 | P0 |
| 一键排产弹窗 | 选择工艺路线后预览并确认排产 | P1 |
| 批量报工功能 | 选中多任务批量报工 | P2 |
status 需要按新枚举值迁移详见:docs/work_order_center_integration.md(待创建)