Files
bj_power/bj_power_mes/internal/logic/processflow.go
T
SunYF ae4c292487 feat: 完成产线MES系统全流程功能迭代与优化
本提交完成了多个核心模块的功能完善与业务对齐:
1. 工位终端:固定工位配置、移除自选工位逻辑、适配配置化工位号
2. MES核心:重构工位组合逻辑、新增工单/流程状态管理、补全报工/追溯逻辑
3. WMS客户端:新增修改密码功能、优化帮助弹窗逻辑
4. 文档与权限:补充完整操作手册、统一菜单名称与权限描述
5. 修复多业务校验:排产数量校验、工单状态校验、工位绑定规则
2026-08-31 12:58:28 +08:00

426 lines
14 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"
"sort"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/processflow"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/station"
"bj_power_mes/ent/stepcriterion"
"bj_power_mes/ent/workorder"
"bj_power_mes/ent/workpieceprocess"
)
// ---------- 工艺流程(块3 ----------
type FlowReq struct {
Id int `json:"id"`
Name string `json:"name"`
StationNo int `json:"stationNo"`
PdfFile string `json:"pdfFile"`
Status string `json:"status"`
Remark string `json:"remark"`
Steps []StepItemReq `json:"steps"`
}
// FlowVO 工艺流程视图(含工序步骤 + 绑定该流程的工位号)
type FlowVO struct {
Id int `json:"id"`
Name string `json:"name"`
StationNo int `json:"stationNo"`
PdfFile string `json:"pdfFile"`
Status string `json:"status"`
Remark string `json:"remark"`
Stations []int `json:"stations"`
Steps []*ProcessStepTemplate `json:"steps"`
CreatedAt time.Time `json:"createdAt"`
}
// ListFlows 查询工艺流程(可按工位号过滤)
func (s *Service) ListFlows(ctx context.Context, stationNo int) ([]*FlowVO, error) {
q := s.ctx.EntClient.ProcessFlow.Query().
Order(ent.Asc(processflow.FieldProcessCode), ent.Asc(processflow.FieldID))
if stationNo > 0 {
q = q.Where(processflow.ProcessCode(stationNo))
}
flows, err := q.All(ctx)
if err != nil {
return nil, err
}
stations, _ := s.ctx.EntClient.Station.Query().All(ctx)
out := make([]*FlowVO, 0, len(flows))
for _, f := range flows {
vo := &FlowVO{
Id: f.ID, Name: f.Name, StationNo: f.ProcessCode,
PdfFile: f.PdfFile, Status: f.Status, Remark: f.Remark,
Stations: []int{}, Steps: s.flowSteps(ctx, f.ID, f.ProcessCode), CreatedAt: f.CreatedAt,
}
for _, st := range stations {
if st.FlowId == f.ID {
vo.Stations = append(vo.Stations, st.StationNo)
}
}
out = append(out, vo)
}
return out, nil
}
func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) error {
if req.Name == "" {
return errors.New("流程名称必填")
}
if req.StationNo < 1 || req.StationNo > 12 {
return errors.New("请选择工位号(1~12,固定12个工位)")
}
status := req.Status
if status == "" {
status = "ACTIVE"
}
oldStationNo := 0
if req.Id > 0 {
if old, err := s.ctx.EntClient.ProcessFlow.Get(ctx, req.Id); err == nil {
oldStationNo = old.ProcessCode
}
}
if status == "ACTIVE" {
q := s.ctx.EntClient.ProcessFlow.Query().
Where(processflow.ProcessCode(req.StationNo), processflow.Status("ACTIVE"))
if req.Id > 0 {
q = q.Where(processflow.IDNEQ(req.Id))
}
if exist, err := q.Exist(ctx); err == nil && exist {
return fmt.Errorf("工位 %d 已存在启用的工艺流程,请先停用旧流程", req.StationNo)
}
}
var flowID int
if req.Id > 0 {
upd := s.ctx.EntClient.ProcessFlow.UpdateOneID(req.Id).
SetName(req.Name).SetProcessCode(req.StationNo).SetStatus(status).SetRemark(req.Remark)
if req.PdfFile != "" {
upd.SetPdfFile(req.PdfFile)
}
if _, err := upd.Save(ctx); err != nil {
return err
}
flowID = req.Id
} else {
f, err := s.ctx.EntClient.ProcessFlow.Create().
SetName(req.Name).SetProcessCode(req.StationNo).SetPdfFile(req.PdfFile).
SetStatus(status).SetRemark(req.Remark).Save(ctx)
if err != nil {
return err
}
flowID = f.ID
}
// 工位号变更:解绑旧工位
if oldStationNo > 0 && oldStationNo != req.StationNo {
_, _ = s.ctx.EntClient.Station.Update().
Where(station.StationNo(oldStationNo), station.FlowId(flowID)).
SetFlowId(0).Save(ctx)
}
// 绑定同步:工位N 绑定该启用流程
if status == "ACTIVE" {
s.syncStationFlow(ctx, req.StationNo, flowID)
} else {
_, _ = s.ctx.EntClient.Station.Update().
Where(station.StationNo(req.StationNo), station.FlowId(flowID)).
SetFlowId(0).Save(ctx)
}
// 覆盖式重建该流程的工序步骤及考核标准
_, _ = s.ctx.EntClient.ProcessStep.Delete().Where(processstep.FlowId(flowID)).Exec(ctx)
for _, st := range req.Steps {
if st.Name == "" {
continue
}
rec, err := s.ctx.EntClient.ProcessStep.Create().
SetFlowId(flowID).SetProcessCode(req.StationNo).SetSeq(st.Seq).SetName(st.Name).
SetCollectType(st.CollectType).SetIsTorque(st.IsTorque).SetRemark(st.Remark).Save(ctx)
if err != nil {
return err
}
_, _ = s.ctx.EntClient.StepCriterion.Delete().Where(stepcriterion.StepId(rec.ID)).Exec(ctx)
for _, c := range st.Criteria {
if c.Name == "" || c.Logic == "" || c.Logic == "NONE" {
continue
}
b := s.ctx.EntClient.StepCriterion.Create().
SetStepId(rec.ID).SetName(c.Name).SetUnit(c.Unit).SetLogic(c.Logic).SetTarget(c.Target)
if c.Min != nil {
b = b.SetMin(*c.Min)
}
if c.Max != nil {
b = b.SetMax(*c.Max)
}
if err := b.Exec(ctx); err != nil {
return err
}
}
}
s.ctx.EventLog.Write(ctx, "process.flow.save", "", operator, "process_flow", "", "维护工艺流程",
map[string]any{"flowId": flowID, "stationNo": req.StationNo, "steps": len(req.Steps)})
return nil
}
// syncStationFlow 同步工位绑定:工位 stationNo 绑定到 flowID(无工位记录时自动创建)
func (s *Service) syncStationFlow(ctx context.Context, stationNo, flowID int) {
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx)
if err != nil {
_, _ = s.ctx.EntClient.Station.Create().
SetStationNo(stationNo).SetName(fmt.Sprintf("工位%d", stationNo)).
SetFlowId(flowID).Save(ctx)
return
}
_, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(flowID).Save(ctx)
}
func (s *Service) DeleteFlow(ctx context.Context, id int, operator string) error {
// 有工位绑定的流程不允许删除
cnt, err := s.ctx.EntClient.Station.Query().Where(station.FlowId(id)).Count(ctx)
if err == nil && cnt > 0 {
return errors.New("该流程已被工位绑定,请先解绑工位")
}
_, _ = s.ctx.EntClient.ProcessStep.Delete().Where(processstep.FlowId(id)).Exec(ctx)
s.ctx.EventLog.Write(ctx, "process.flow.delete", "", operator, "process_flow", "", "删除工艺流程", map[string]any{"flowId": id})
return s.ctx.EntClient.ProcessFlow.DeleteOneID(id).Exec(ctx)
}
// flowSteps 查询某流程的工序步骤(含考核标准)
func (s *Service) flowSteps(ctx context.Context, flowId, processCode int) []*ProcessStepTemplate {
steps, _ := s.ctx.EntClient.ProcessStep.Query().
Where(processstep.FlowId(flowId)).Order(ent.Asc(processstep.FieldSeq)).All(ctx)
out := make([]*ProcessStepTemplate, 0, len(steps))
for _, st := range steps {
t := &ProcessStepTemplate{
ID: st.ID, ProcessCode: st.ProcessCode, Seq: st.Seq,
Name: st.Name, CollectType: st.CollectType, IsTorque: st.IsTorque,
Remark: st.Remark, Criteria: []CriterionVO{},
}
crits, _ := s.ctx.EntClient.StepCriterion.Query().
Where(stepcriterion.StepId(st.ID)).All(ctx)
for _, c := range crits {
t.Criteria = append(t.Criteria, CriterionVO{
ID: c.ID, Name: c.Name, Unit: c.Unit, Logic: c.Logic,
Target: c.Target, Min: c.Min, Max: c.Max,
})
}
out = append(out, t)
}
return out
}
// ---------- 工位主数据(块3 ----------
type StationReq struct {
Id int `json:"id"`
StationNo int `json:"stationNo"`
Name string `json:"name"`
FlowId int `json:"flowId"`
}
type StationVO struct {
Id int `json:"id"`
StationNo int `json:"stationNo"`
Name string `json:"name"`
FlowId int `json:"flowId"`
FlowName string `json:"flowName"`
ProcessCode int `json:"processCode"`
Status string `json:"status"`
}
func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
stas, err := s.ctx.EntClient.Station.Query().Order(ent.Asc(station.FieldStationNo)).All(ctx)
if err != nil {
return nil, err
}
flows, _ := s.ctx.EntClient.ProcessFlow.Query().All(ctx)
flowMap := map[int]*ent.ProcessFlow{}
for _, f := range flows {
flowMap[f.ID] = f
}
out := make([]*StationVO, 0, len(stas))
for _, st := range stas {
vo := &StationVO{Id: st.ID, StationNo: st.StationNo, Name: st.Name, FlowId: st.FlowId, Status: st.Status}
if f, ok := flowMap[st.FlowId]; ok {
vo.FlowName = f.Name
vo.ProcessCode = f.ProcessCode
}
out = append(out, vo)
}
return out, nil
}
func (s *Service) SaveStation(ctx context.Context, req StationReq, operator string) error {
if req.StationNo <= 0 || req.StationNo > 12 {
return errors.New("工位号必须在 1~12")
}
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx)
if err != nil {
if _, err := s.ctx.EntClient.Station.Create().
SetStationNo(req.StationNo).SetName(req.Name).Save(ctx); err != nil {
return err
}
} else {
// 仅改名称:flowId 不传时保留原绑定
upd := s.ctx.EntClient.Station.UpdateOneID(st.ID).SetName(req.Name)
if req.FlowId > 0 {
flow, fErr := s.ctx.EntClient.ProcessFlow.Get(ctx, req.FlowId)
if fErr != nil {
return errors.New("所选工艺流程不存在")
}
if flow.Status != "ACTIVE" {
return errors.New("该工艺流程已停用,请先启用后再绑定工位")
}
upd.SetFlowId(req.FlowId)
}
if _, err := upd.Save(ctx); err != nil {
return err
}
}
s.ctx.EventLog.Write(ctx, "station.save", "", operator, "station", "", "维护工位",
map[string]any{"stationNo": req.StationNo})
return nil
}
// SetFlowStatus 工艺流程启用/停用。
// 停用:自动解绑对应工位;启用:校验该工位无其他启用流程后自动绑定。
func (s *Service) SetFlowStatus(ctx context.Context, id int, status, operator string) error {
if id <= 0 {
return errors.New("缺少 id")
}
if status != "ACTIVE" && status != "INACTIVE" {
return errors.New("非法状态")
}
flow, err := s.ctx.EntClient.ProcessFlow.Get(ctx, id)
if err != nil {
return errors.New("工艺流程不存在")
}
if status == "INACTIVE" {
// 自动解绑该流程对应的工位
_, _ = s.ctx.EntClient.Station.Update().
Where(station.StationNo(flow.ProcessCode), station.FlowId(flow.ID)).
SetFlowId(0).Save(ctx)
} else {
q := s.ctx.EntClient.ProcessFlow.Query().
Where(processflow.ProcessCode(flow.ProcessCode), processflow.Status("ACTIVE"), processflow.IDNEQ(flow.ID))
if exist, err := q.Exist(ctx); err == nil && exist {
return fmt.Errorf("工位 %d 已存在其他启用的工艺流程,请先停用旧流程", flow.ProcessCode)
}
s.syncStationFlow(ctx, flow.ProcessCode, flow.ID)
}
if err := s.ctx.EntClient.ProcessFlow.UpdateOneID(id).SetStatus(status).Exec(ctx); err != nil {
return err
}
action := "停用工艺流程"
if status == "ACTIVE" {
action = "启用工艺流程"
}
s.ctx.EventLog.Write(ctx, "process.flow.status", "", operator, "process_flow", "", action, map[string]any{"flowId": id, "status": status, "name": flow.Name, "stationNo": flow.ProcessCode})
return nil
}
// ---------- 工位任务(块4/5:工位终端拉取) ----------
func (s *Service) StationTask(ctx context.Context, stationNo int) (map[string]any, error) {
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx)
if err != nil {
return nil, errors.New("工位不存在或未配置")
}
var flow *ent.ProcessFlow
if st.FlowId > 0 {
flow, _ = s.ctx.EntClient.ProcessFlow.Get(ctx, st.FlowId)
}
steps := []*ProcessStepTemplate{}
if flow != nil {
steps = s.flowSteps(ctx, flow.ID, flow.ProcessCode)
}
orderNos := []string{}
wos, _ := s.ctx.EntClient.WorkOrder.Query().
Where(workorder.StatusIn("CREATED", "RELEASED", "IN_PROGRESS")).
Order(ent.Asc(workorder.FieldID)).Limit(10).All(ctx)
for _, w := range wos {
orderNos = append(orderNos, w.WorkOrderNo)
}
return map[string]any{
"stationNo": st.StationNo,
"stationName": st.Name,
"flow": flow,
"steps": steps,
"orderNos": orderNos,
}, nil
}
// ---------- 工作量/绩效报表(块6 ----------
type WorkloadRow struct {
Operator string `json:"operator"`
StationNo string `json:"stationNo"`
Date string `json:"date"`
ProcessCode int `json:"processCode"`
ProcessName string `json:"processName"`
DoneCount int `json:"doneCount"`
OkCount int `json:"okCount"`
NgCount int `json:"ngCount"`
}
func (s *Service) Workload(ctx context.Context, operator, stationNo, from, to string) (map[string]any, error) {
q := s.ctx.EntClient.WorkpieceProcess.Query()
if operator != "" {
q = q.Where(workpieceprocess.Operator(operator))
}
if stationNo != "" {
q = q.Where(workpieceprocess.StationNo(stationNo))
}
if from != "" {
if f, err := time.Parse("2006-01-02", from); err == nil {
q = q.Where(workpieceprocess.CreatedAtGTE(f))
}
}
if to != "" {
if t, err := time.Parse("2006-01-02", to); err == nil {
q = q.Where(workpieceprocess.CreatedAtLT(t.Add(24 * time.Hour)))
}
}
rows, err := q.All(ctx)
if err != nil {
return nil, err
}
type key struct{ op, station, date string; code int }
agg := map[key]*WorkloadRow{}
for _, r := range rows {
k := key{r.Operator, r.StationNo, r.CreatedAt.Format("2006-01-02"), r.ProcessCode}
row, ok := agg[k]
if !ok {
row = &WorkloadRow{Operator: r.Operator, StationNo: r.StationNo, Date: k.date, ProcessCode: r.ProcessCode, ProcessName: r.ProcessName}
agg[k] = row
}
row.DoneCount++
if r.Result == "OK" {
row.OkCount++
} else {
row.NgCount++
}
}
list := make([]*WorkloadRow, 0, len(agg))
for _, v := range agg {
list = append(list, v)
}
sort.Slice(list, func(i, j int) bool {
if list[i].Operator != list[j].Operator {
return list[i].Operator < list[j].Operator
}
if list[i].StationNo != list[j].StationNo {
return list[i].StationNo < list[j].StationNo
}
if list[i].Date != list[j].Date {
return list[i].Date < list[j].Date
}
return list[i].ProcessCode < list[j].ProcessCode
})
return map[string]any{"rows": list, "detail": rows}, nil
}