Files
bj_power/bj_power_mes/internal/logic/workorder.go
T
SunYF 1cc93795ce feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
2026-09-17 12:49:07 +08:00

304 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"errors"
"fmt"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/dailyplan"
"bj_power_mes/ent/materialrequest"
"bj_power_mes/ent/workorder"
"bj_power_mes/ent/workpiece"
)
type WorkOrderReq struct {
Id int `json:"id"`
WorkOrderNo string `json:"workOrderNo"`
ProductTypeId int `json:"productTypeId"`
ProductCode string `json:"productCode"`
ProductName string `json:"productName"`
Quantity int `json:"quantity"`
ProcessSeq string `json:"processSeq"`
Status string `json:"status"`
PlanStart string `json:"planStart"`
PlanEnd string `json:"planEnd"`
// 工程编号体系(合同 → 工程编号 → 产品序号)
ContractNo string `json:"contractNo"`
ProjectNo string `json:"projectNo"`
ProductSerial string `json:"productSerial"`
// 完成日期(必填,前端强制;yyyy-MM-dd)
DueDate string `json:"dueDate"`
}
// CreateWorkOrder 创建工单(状态恒 CREATED,后续用状态流转按钮推进)
func (s *Service) CreateWorkOrder(ctx context.Context, req WorkOrderReq, operator string) error {
if req.WorkOrderNo == "" {
return errors.New("工单号不能为空")
}
if req.ProductTypeId == 0 {
return errors.New("请选择产品型号")
}
if req.Quantity <= 0 {
req.Quantity = 1
}
status := req.Status
if status == "" {
status = "CREATED"
}
if status != "CREATED" {
status = "CREATED" // 创建恒为已创建;不允许直接创建到后续状态
}
// 工位组合=本工单要经过的工位,按工位号升序(传送带不回走)。
// 完整的产线定义 = 关联工位(station.flow_id+ 工位号 1→12 顺序,不再有独立的"工艺路线"对象。
if req.ProcessSeq == "" {
req.ProcessSeq = FullProcessSeq
} else if err := ValidateProcessSeq(req.ProcessSeq); err != nil {
return err
}
req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq)
b := s.ctx.EntClient.WorkOrder.Create().
SetWorkOrderNo(req.WorkOrderNo).
SetProductTypeId(req.ProductTypeId).
SetProductCode(req.ProductCode).
SetProductName(req.ProductName).
SetQuantity(req.Quantity).
SetProcessSeq(req.ProcessSeq).
SetContractNo(req.ContractNo).
SetProjectNo(req.ProjectNo).
SetProductSerial(req.ProductSerial).
SetStatus(status)
if req.PlanStart != "" {
if t, err := parseDate(req.PlanStart); err == nil {
b.SetPlanStart(t)
}
}
if req.PlanEnd != "" {
if t, err := parseDate(req.PlanEnd); err == nil {
b.SetPlanEnd(t)
}
}
// 完成日期(Item M):前端必填,后端有值即落库
if req.DueDate != "" {
if t, err := parseDate(req.DueDate); err == nil {
b.SetDueDate(t)
}
}
// 创建人(Item M):取当前登录用户
b.SetCreatedBy(operator)
if _, err := b.Save(ctx); err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "work.order.create", req.WorkOrderNo, operator, "work_order", req.WorkOrderNo, "创建工单", map[string]any{"quantity": req.Quantity})
s.notifyDashboard()
return nil
}
// UpdateWorkOrder 编辑工单。仅「已创建」状态允许编辑(已下发/执行中/已暂停只能流转,已完成/已取消只读)。
func (s *Service) UpdateWorkOrder(ctx context.Context, req WorkOrderReq, operator string) error {
if req.Id == 0 {
return errors.New("缺少 id")
}
old, err := s.ctx.EntClient.WorkOrder.Get(ctx, req.Id)
if err != nil {
return errors.New("工单不存在")
}
if old.Status != "CREATED" && old.Status != "PENDING" {
return errors.New("仅「已创建」状态的工单可编辑;生产中或已结束的工单只能做状态流转")
}
if req.ProcessSeq != "" {
if err := ValidateProcessSeq(req.ProcessSeq); err != nil {
return err
}
req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq)
}
u := s.ctx.EntClient.WorkOrder.UpdateOneID(req.Id).SetProcessSeq(req.ProcessSeq).
SetContractNo(req.ContractNo).SetProjectNo(req.ProjectNo).SetProductSerial(req.ProductSerial)
if req.Quantity > 0 {
u.SetQuantity(req.Quantity)
}
if req.ProductCode != "" {
u.SetProductCode(req.ProductCode)
}
if req.ProductName != "" {
u.SetProductName(req.ProductName)
}
if req.PlanStart != "" {
if t, err := parseDate(req.PlanStart); err == nil {
u.SetPlanStart(t)
}
}
if req.PlanEnd != "" {
if t, err := parseDate(req.PlanEnd); err == nil {
u.SetPlanEnd(t)
}
}
if req.DueDate != "" {
if t, err := parseDate(req.DueDate); err == nil {
u.SetDueDate(t)
}
}
if err := u.Exec(ctx); err != nil {
return err
}
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, req.Id)
if err == nil {
s.ctx.EventLog.Write(ctx, "work.order.update", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "更新工单", nil)
}
s.notifyDashboard()
return nil
}
// ListWorkOrders 查询工单(支持工单号/状态/产品编码/产品名称 过滤)
func (s *Service) ListWorkOrders(ctx context.Context, orderNo, status, productCode, productName, contractNo, projectNo, dueFrom, dueTo string) ([]*ent.WorkOrder, error) {
q := s.ctx.EntClient.WorkOrder.Query()
if orderNo != "" {
q = q.Where(workorder.WorkOrderNoContains(orderNo))
}
if status != "" {
q = q.Where(workorder.Status(status))
}
if productCode != "" {
q = q.Where(workorder.ProductCodeContains(productCode))
}
if productName != "" {
q = q.Where(workorder.ProductNameContains(productName))
}
if contractNo != "" {
q = q.Where(workorder.ContractNoContains(contractNo))
}
if projectNo != "" {
q = q.Where(workorder.ProjectNoContains(projectNo))
}
// 完成日期范围(Item M
if dueFrom != "" {
if t, err := parseDate(dueFrom); err == nil {
q = q.Where(workorder.DueDateGTE(t))
}
}
if dueTo != "" {
if t, err := parseDate(dueTo); err == nil {
q = q.Where(workorder.DueDateLTE(t))
}
}
return q.Order(ent.Desc(workorder.FieldCreatedAt), ent.Desc(workorder.FieldID)).All(ctx)
}
func (s *Service) GetWorkOrder(ctx context.Context, id int) (*ent.WorkOrder, error) {
if id > 0 {
return s.ctx.EntClient.WorkOrder.Get(ctx, id)
}
return s.ctx.EntClient.WorkOrder.Query().Order(ent.Desc(workorder.FieldID)).First(ctx)
}
// 工单状态白名单(统一枚举:已创建/已下发/执行中/已暂停/已完成/已取消)
var workOrderStatuses = map[string]bool{
"CREATED": true, "RELEASED": true, "IN_PROGRESS": true,
"PAUSED": true, "DONE": true, "CANCELLED": true,
}
// workOrderTransitions 工单状态流转表:from → {to...}
// CREATED 可下发/取消;RELEASED 可开工/取消;IN_PROGRESS 可暂停/完工;
// PAUSED 可恢复/取消;DONE/CANCELLED 为终态,不再流转。
var workOrderTransitions = map[string]map[string]bool{
"CREATED": {"RELEASED": true, "CANCELLED": true},
"RELEASED": {"IN_PROGRESS": true, "CANCELLED": true},
"IN_PROGRESS": {"PAUSED": true, "DONE": true},
"PAUSED": {"IN_PROGRESS": true, "CANCELLED": true},
"DONE": {},
"CANCELLED": {},
// 兼容存量旧枚举:PENDING(待排产)≈已创建可发布,CONFIRMED(已确认)≈已下发可开工
"PENDING": {"RELEASED": true, "CANCELLED": true},
"CONFIRMED": {"IN_PROGRESS": true, "CANCELLED": true},
}
// SetWorkOrderStatus 工单状态流转(发布/开工/暂停/恢复/完工/取消)。
// 校验流转合法性;写 eventlog 记录 原状态/新状态/原因;终态联动处理(见 cancelPlansOfOrder)。
func (s *Service) SetWorkOrderStatus(ctx context.Context, id int, status, reason, operator string) error {
if id <= 0 {
return errors.New("缺少 id")
}
if !workOrderStatuses[status] {
return errors.New("非法的工单状态")
}
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, id)
if err != nil {
return errors.New("工单不存在")
}
if wo.Status == status {
return nil // 幂等:状态相同直接成功
}
if !workOrderTransitions[wo.Status][status] {
return fmt.Errorf("不允许从「%s」变更为「%s」", statusTextCN(wo.Status), statusTextCN(status))
}
// 单工单互斥:一条产线同时只能有一个在产工单(下发/开工前校验)
if status == "RELEASED" || status == "IN_PROGRESS" {
others, _ := s.ctx.EntClient.WorkOrder.Query().
Where(workorder.StatusIn("IN_PROGRESS", "PAUSED")).
All(ctx)
for _, o := range others {
if o.ID != id {
return fmt.Errorf("产线上已有在产工单「%s」(%s),完成或暂停后方可下发/开工新工单", o.WorkOrderNo, statusTextCN(o.Status))
}
}
}
upd := s.ctx.EntClient.WorkOrder.UpdateOneID(id).SetStatus(status)
if status == "DONE" {
now := time.Now()
upd.SetCompletedAt(now)
}
if err := upd.Exec(ctx); err != nil {
return err
}
// 状态变更操作日志:原状态/新状态/原因
s.ctx.EventLog.Write(ctx, "work.order.status", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "变更工单状态",
map[string]any{"from": wo.Status, "to": status, "reason": reason})
// 联动:DONE/CANCELLED → 该工单未完成排产批量取消;IN_PROGRESS → 排产置执行中
s.syncPlansByOrderStatus(ctx, wo.WorkOrderNo, status, operator)
s.notifyDashboard()
return nil
}
// statusTextCN 状态枚举中文文案
func statusTextCN(s string) string {
m := map[string]string{
"CREATED": "已创建", "RELEASED": "已下发", "IN_PROGRESS": "执行中",
"PAUSED": "已暂停", "DONE": "已完成", "CANCELLED": "已取消",
}
if v, ok := m[s]; ok {
return v
}
return s
}
// DeleteWorkOrder 删除工单。
// 安全条件:① 状态仅限 已创建(CREATED,兼容存量 PENDING/CONFIRMED 未开始生产);② 无日排产记录;
// ③ 未生成备料单;④ 无任何工件生产数据。已下发/执行中/已暂停/已完成/已取消一律不可删除。
func (s *Service) DeleteWorkOrder(ctx context.Context, id int, operator string) error {
if id <= 0 {
return errors.New("缺少 id")
}
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, id)
if err != nil {
return errors.New("工单不存在")
}
if wo.Status != "CREATED" && wo.Status != "PENDING" && wo.Status != "CONFIRMED" {
return errors.New("仅「已创建」状态的工单可删除;已下发、执行中或已结束的工单不能删除")
}
if n, err := s.ctx.EntClient.DailyPlan.Query().Where(dailyplan.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 {
return errors.New("该工单已有日排产记录,请先删除对应排产后再删除工单")
}
if n, err := s.ctx.EntClient.MaterialRequest.Query().Where(materialrequest.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 {
return errors.New("该工单已生成备料单,不能删除")
}
if n, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 {
return errors.New("该工单已有工件生产数据,不能删除")
}
if err := s.ctx.EntClient.WorkOrder.DeleteOneID(id).Exec(ctx); err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "work.order.delete", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "删除工单", map[string]any{"status": wo.Status})
return nil
}