Files
bj_power/bj_power_mes/internal/logic/workorder.go
T
SunYF 37f10d3a13 refactor: 重构产线工位与备料流程,移除冗余字段
1. 移除工单、工件、日排产中的冗余工位组合/接驳台字段
2. 新增工位接驳台编码字段与唯一约束,关联WMS接驳台主数据
3. 重构日排产为工位产量分配模式,替代原接驳台列表
4. 新增备料单工位级字段与分批出库支持
5. 移除定时同步可生产数量,改为WMS实时拉取接口
6. 过滤操作日志,排除登录/退出相关日志
7. 调整仪表盘工序展示逻辑为今日派工路线
8. 新增接驳台维护菜单与基础数据
2026-09-19 07:50:21 +08:00

332 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"`
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" // 创建恒为已创建;不允许直接创建到后续状态
}
// 工位↔工序路线现由「工位派工(station_process)」按日定义,工单不再存路线串
b := s.ctx.EntClient.WorkOrder.Create().
SetWorkOrderNo(req.WorkOrderNo).
SetProductTypeId(req.ProductTypeId).
SetProductCode(req.ProductCode).
SetProductName(req.ProductName).
SetQuantity(req.Quantity).
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("仅「已创建」状态的工单可编辑;生产中或已结束的工单只能做状态流转")
}
u := s.ctx.EntClient.WorkOrder.UpdateOneID(req.Id).
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
}