Files
bj_power/bj_power_mes/internal/logic/workorder.go
T
SunYF 672cd23329 feat: 新增多类功能并优化现有流程
1. 工序工位支持0号上线位,更新解析逻辑与注释
2. 物料清单模块重命名为产品物料清单并优化文案
3. WMS与工位端新增用户/角色详情抽屉组件
4. 工单模块新增自动生成工单号、工位分组选择器
5. 日排产支持批量生成与详情查看
6. 工单列表重构操作菜单与表单优化
2026-09-18 13:54:30 +08:00

344 lines
12 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"
"strconv"
"strings"
"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.ProductTypeId == 0 {
return errors.New("请选择产品型号")
}
if req.Quantity <= 0 {
req.Quantity = 1
}
// 工单号:未传入则按规则生成 WO-日期-001;若与已有工单号重复,自动递增序号(001→002…)。
if req.WorkOrderNo == "" {
req.WorkOrderNo = defaultWorkOrderNo()
}
finalNo, err := s.resolveUniqueWorkOrderNo(ctx, req.WorkOrderNo)
if err != nil {
return err
}
req.WorkOrderNo = finalNo
status := req.Status
if status == "" {
status = "CREATED"
}
if status != "CREATED" {
status = "CREATED" // 创建恒为已创建;不允许直接创建到后续状态
}
// 工位组合为本工单要经过的工位,按工位号升序(传送带不回走),必填项,禁止留空/自动默认全选。
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
}
// defaultWorkOrderNo 工单号默认规则:WO-YYYYMMDD-001(如 WO-20260918-001)。
func defaultWorkOrderNo() string {
return fmt.Sprintf("WO-%s-001", time.Now().Format("20060102"))
}
// resolveUniqueWorkOrderNo 工单号唯一性保障:若 want 已存在,则按尾部序号自动递增
// WO-20260918-001 → 002 → 003…),最多尝试 999 次,仍冲突则报错由前端手动指定。
// 尾部序号解析失败(无数字后缀)时,退化为在末尾追加 -001/-002…。
func (s *Service) resolveUniqueWorkOrderNo(ctx context.Context, want string) (string, error) {
parts := strings.Split(want, "-")
prefix := want
seq := 0
if len(parts) >= 2 {
if n, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
seq = n
prefix = strings.Join(parts[:len(parts)-1], "-")
}
}
for i := 0; i < 999; i++ {
cand := want
if i > 0 {
cand = fmt.Sprintf("%s-%03d", prefix, seq+i)
}
cnt, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(cand)).Count(ctx)
if err != nil {
return "", err
}
if cnt == 0 {
return cand, nil
}
}
return "", errors.New("工单号自动编号失败(序号已用尽),请手动指定工单号")
}
// 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
}