Files
bj_power/bj_power_mes/internal/logic/workorder.go
T
SunYF 0ab21b1234 feat&refactor: 完成多模块功能迭代与配置优化
本次提交覆盖多个业务模块的功能完善与体验优化:
1.  **鉴权与配置调整**:
    - 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
    - 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
    - 工位配置放开1~12限制,改为仅校验大于0
2.  **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3.  **功能新增**:
    - 新增工位类型、工艺路线与产线点位台账模块
    - 添加工艺PDF预览面板、工位终端代理转发接口
    - 新增操作日志按操作人列表筛选、工位登出日志记录
    - 新增PLC移料指令与产线点位状态管理
4.  **业务流程优化**:
    - 调整BOM物料删除校验逻辑,优化工单备料计算
    - 补充物料图号、检测单号等追溯字段
    - 完善工艺流程图与工位绑定关系说明
    - 优化前端页面文案与交互细节
5.  **代码规范与维护**:
    - 新增通用工具函数与前端静态资源
    - 整理路由权限与中间件逻辑
    - 修复部分接口与配置的不兼容问题
2026-09-10 16:59:15 +08:00

284 lines
9.9 KiB
Go

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"`
RouteId int `json:"routeId"`
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"`
}
// 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" // 创建恒为已创建;不允许直接创建到后续状态
}
routeSnapshot := []map[string]any{}
if req.RouteId > 0 {
route, rerr := s.GetRoute(ctx, req.RouteId)
if rerr != nil {
return rerr
}
snapshot, lineStations := s.BuildRouteSnapshot(route)
routeSnapshot = snapshot
if len(lineStations) > 0 {
parts := make([]string, 0, len(lineStations))
for _, no := range lineStations {
parts = append(parts, strconv.Itoa(no))
}
req.ProcessSeq = strings.Join(parts, ",")
} else {
req.ProcessSeq = ""
}
} else {
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).SetRouteId(req.RouteId).SetRouteSnapshot(routeSnapshot).
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)
}
}
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)
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 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 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))
}
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
}