Files
bj_power/bj_power_mes/internal/logic/processflow.go
T
SunYF e852c7cd37 chore: 批量完成项目多模块迭代优化
1. 废弃半成品库存表并统一库存主表
2. 修复列表排序与搜索大小写不敏感问题
3. 新增用户最近登录时间、工单BOM名称字段
4. 完善角色管理、工位选择器功能
5. 增加拧紧数据补录、成品自动回流WMS能力
6. 统一导出时间范围校验规则
7. 丰富物料/备料单筛选条件
8. 调整菜单与权限命名适配产品型号业务
2026-09-08 11:44:04 +08:00

519 lines
17 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 ----------
// FlowReq 工艺流程保存请求
// StationNo:主工序编号 1..12(流程承载的工序号,沿用既有语义);
// Stations:绑定工位列表(可多选,一个流程可绑多个工位;不传时默认只绑 StationNo)。
type FlowReq struct {
Id int `json:"id"`
Name string `json:"name"`
StationNo int `json:"stationNo"`
Stations []int `json:"stations"`
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 查询工艺流程(可按工位号过滤:命中绑定该工位或主工序号的流程)
// page>0 时真分页返回 map{total,list,page,pageSize}(管理页用);page=0 全量数组(工位终端兼容)
func (s *Service) ListFlows(ctx context.Context, stationNo, page, pageSize int) (any, error) {
q := s.ctx.EntClient.ProcessFlow.Query().
Order(ent.Asc(processflow.FieldProcessCode), ent.Asc(processflow.FieldID))
flows, err := q.All(ctx)
if err != nil {
return nil, err
}
stations, _ := s.ctx.EntClient.Station.Query().All(ctx)
flowToStations := map[int][]int{}
for _, st := range stations {
if st.FlowId > 0 {
flowToStations[st.FlowId] = append(flowToStations[st.FlowId], st.StationNo)
}
}
out := make([]*FlowVO, 0, len(flows))
for _, f := range flows {
// 过滤:指定工位号时,只返回"绑定列表或主工序号"命中该工位的流程
if stationNo > 0 {
hit := false
for _, st := range flowToStations[f.ID] {
if st == stationNo {
hit = true
break
}
}
if !hit && f.ProcessCode != stationNo {
continue
}
}
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,
}
if list, ok := flowToStations[f.ID]; ok {
vo.Stations = list
}
out = append(out, vo)
}
if page > 0 {
total := len(out)
start := (page - 1) * pageSize
if start > total {
start = total
}
end := start + pageSize
if end > total {
end = total
}
return map[string]any{"total": total, "list": out[start:end], "page": page, "pageSize": pageSize}, nil
}
return out, nil
}
// SaveFlow 保存工艺流程(可一次绑定多个工位)。
// 约束:① 工位号/绑定工位 1..12;② 一个工位同时只能绑定一个"启用"流程(保存时校验冲突);
// ③ 停用流程不再自动解绑工位(绑定关系由本保存接口显式维护,启停只切流程状态)。
func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) error {
if req.Name == "" {
return errors.New("流程名称必填")
}
// 工序编号与物理工位解耦:工序是流程步骤编号(1~999,流程内唯一即可),
// 工位对应关系由「关联工位」页维护(2026-09-08)
if req.StationNo < 1 || req.StationNo > 999 {
return errors.New("主工序编号应为 1~999 的整数")
}
// 解析绑定工位列表:显式 stations 优先,否则回退到 StationNo(兼容旧调用)
bindStations := req.Stations
if len(bindStations) == 0 {
bindStations = []int{req.StationNo}
}
seen := map[int]bool{}
stations := []int{}
for _, no := range bindStations {
if no < 1 || no > 12 || seen[no] {
continue
}
seen[no] = true
stations = append(stations, no)
}
if len(stations) == 0 {
return errors.New("请至少选择一个绑定工位")
}
status := req.Status
if status == "" {
status = "ACTIVE"
}
if status != "ACTIVE" && status != "INACTIVE" {
return errors.New("非法状态")
}
// 绑定语义(2026-09-08 定稿):直接更换——一个工位只有一条关联数据,
// 保存时所选工位一律改绑到本流程(覆盖旧绑定),不再报"已被启用流程绑定"冲突。
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
}
// 绑定同步:把绑定列表整体指向该流程(旧绑定该流程但不在列表内的工位解绑)
boundNow, _ := s.ctx.EntClient.Station.Query().Where(station.FlowId(flowID)).All(ctx)
for _, st := range boundNow {
if !seen[st.StationNo] {
_, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(0).Save(ctx)
}
}
for _, no := range stations {
s.syncStationFlow(ctx, no, flowID)
}
// 覆盖式重建该流程的工序步骤及考核标准
_, _ = 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, "processCode": req.StationNo, "stations": stations, "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 {
// 删除前先解除所有工位绑定,再清步骤与流程本身
_, _ = s.ctx.EntClient.Station.Update().
Where(station.FlowId(id)).
SetFlowId(0).Save(ctx)
_, _ = 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 工艺流程启用/停用。
// 新语义:绑定关系由「保存流程」显式维护(一个流程可绑多工位),启停只切换流程状态,
// 不再自动解绑/绑定工位;停用流程对已绑定工位不可用(工位取步骤时按 ACTIVE 过滤)。
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 flow.Status == status {
return nil
}
// 启用无需额外动作:绑定由保存流程时校验(一个工位不会被两个启用流程占用)
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, "processCode": 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 && flow.Status == "ACTIVE" {
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,
"flowActive": flow != nil && flow.Status == "ACTIVE",
"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,
opPage, opSize, stPage, stSize, detPage, detSize int) (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
})
// 三个视图分别聚合(2026-09-08 绩效报表改造:按人/按工位/明细,各自真分页)
byOpAgg := map[string]*WorkloadRow{}
byStAgg := map[string]*WorkloadRow{}
for _, v := range list {
opKey := v.Operator
if o, ok := byOpAgg[opKey]; ok {
o.DoneCount += v.DoneCount; o.OkCount += v.OkCount; o.NgCount += v.NgCount
} else {
byOpAgg[opKey] = &WorkloadRow{Operator: v.Operator, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount}
}
stKey := v.StationNo
if o, ok := byStAgg[stKey]; ok {
o.DoneCount += v.DoneCount; o.OkCount += v.OkCount; o.NgCount += v.NgCount
} else {
byStAgg[stKey] = &WorkloadRow{StationNo: v.StationNo, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount}
}
}
paginateRows := func(all []*ent.WorkpieceProcess, page, size int) (int, []*ent.WorkpieceProcess) {
total := len(all)
start := (page - 1) * size
if start > total {
start = total
}
end := start + size
if end > total {
end = total
}
return total, all[start:end]
}
paginate := func(all []*WorkloadRow, page, size int) (int, []*WorkloadRow) {
total := len(all)
start := (page - 1) * size
if start > total {
start = total
}
end := start + size
if end > total {
end = total
}
return total, all[start:end]
}
byOpList := make([]*WorkloadRow, 0, len(byOpAgg))
for _, v := range byOpAgg {
byOpList = append(byOpList, v)
}
sort.Slice(byOpList, func(i, j int) bool { return byOpList[i].Operator < byOpList[j].Operator })
byStList := make([]*WorkloadRow, 0, len(byStAgg))
for _, v := range byStAgg {
byStList = append(byStList, v)
}
sort.Slice(byStList, func(i, j int) bool { return byStList[i].StationNo < byStList[j].StationNo })
opTotal, byOpPage := paginate(byOpList, opPage, opSize)
stTotal, byStPage := paginate(byStList, stPage, stSize)
detTotal, detRowsPage := paginateRows(rows, detPage, detSize)
return map[string]any{
"byOperator": map[string]any{"total": opTotal, "list": byOpPage},
"byStation": map[string]any{"total": stTotal, "list": byStPage},
"detail": map[string]any{"total": detTotal, "list": detRowsPage},
}, nil
}