Files
bj_power/bj_power_mes/internal/logic/processflow.go
T
SunYF d609ff8a9e refactor: 重构工艺工序相关命名与数据模型
1.  全局替换"工序"为"工艺"统一术语,包括页面文案、枚举、注释
2.  重构工单与工件工艺数据模型:
    - 新增工艺组合表flow_material作为备料/齐套唯一源头
    - 新增工位状态表station_state支持自主停单/恢复接单
    - 移除station_process派工表,改用工单工艺组合作为路线唯一源头
    - 替换processCode为flowId作为工艺关联标识
    - 重构工件当前进度字段为currentStationNo
3.  删除冗余的静态备份资源文件
4.  调整物料选择组件默认启用仅显示激活物料
5.  优化工单创建/编辑逻辑,新增工艺组合校验与保存
2026-09-22 11:32:29 +08:00

701 lines
25 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"
"strings"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/flowmaterial"
"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 工艺流程保存请求(流程=菜谱,只描述"怎么干"
// Stations:绑定工位列表(可多选,一个流程可绑多个工位)。
type FlowReq struct {
Id int `json:"id"`
Name string `json:"name"`
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"`
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 查询工艺流程(可按工位号过滤:只认「关联工位」页的绑定关系 station.flow_id
// 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.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 {
continue
}
}
vo := &FlowVO{
Id: f.ID, Name: f.Name,
PdfFile: f.PdfFile, Status: f.Status, Remark: f.Remark,
Stations: []int{}, Steps: s.flowSteps(ctx, f.ID), 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 保存工艺流程(可一次绑定多个工位)。
// 约束:① 绑定工位必须已存在(工位主数据由数据库维护,本接口不自动创建);
// ② 一个工位同时只能绑定一个流程(保存时直接覆盖旧绑定);
// ③ 停用流程不再自动解绑工位(绑定关系由本保存接口显式维护,启停只切流程状态)。
func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) error {
if req.Name == "" {
return errors.New("流程名称必填")
}
// 解析绑定工位列表
seen := map[int]bool{}
stations := []int{}
for _, no := range req.Stations {
if no < 1 || seen[no] {
continue
}
seen[no] = true
stations = append(stations, no)
}
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).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).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 {
if err := s.syncStationFlow(ctx, no, flowID); err != nil {
return err
}
}
// 覆盖式重建该流程的工序步骤及考核标准
_, _ = 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).SetSeq(st.Seq).SetName(st.Name).
SetCollectType(st.CollectType).SetIsTorque(st.IsTorque).SetNeedCheck(st.NeedCheck).
SetRemark(st.Remark).SetAttachment(st.Attachment).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, "stations": stations, "steps": len(req.Steps)})
return nil
}
// syncStationFlow 同步工位绑定:工位 stationNo 绑定到 flowID(工位不存在时报错,不自动创建)
func (s *Service) syncStationFlow(ctx context.Context, stationNo, flowID int) error {
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx)
if err != nil {
return fmt.Errorf("工位 %d 不存在,请先在工位主数据中维护", stationNo)
}
_, err = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(flowID).Save(ctx)
return err
}
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 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, Seq: st.Seq,
Name: st.Name, CollectType: st.CollectType, IsTorque: st.IsTorque, NeedCheck: st.NeedCheck,
Remark: st.Remark, Attachment: st.Attachment, 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"`
StationType *string `json:"stationType"`
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"`
StationType string `json:"stationType"`
Status string `json:"status"`
IsBuiltin bool `json:"isBuiltin"`
HasDock bool `json:"hasDock"`
DockCode string `json:"dockCode"`
}
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, StationType: st.StationType, Status: st.Status, IsBuiltin: st.IsBuiltin, HasDock: st.HasDock, DockCode: st.DockCode}
if f, ok := flowMap[st.FlowId]; ok {
vo.FlowName = f.Name
}
out = append(out, vo)
}
return out, nil
}
// SaveStation 维护工位:
// - 工位号已存在 → 只改名称/类型/绑定流程(不增删工位)
// - 工位号不存在 → 新增工位;新增的工位号由用户自定义,允许扩展 14、15…(虚拟/物理均可)
//
// 工位数量以数据库为准,前端下拉随 station 表自动增减;虚拟工位(0/13/14...)不连 PLC、仅记录。
// 内置工位(种子/初始化数据,is_builtin=true)需连 PLC 并必须按工位号顺序流转,
// 不允许手动更改工位类型;页面新增的工位一律为非内置,可改类型、可删除。
// 「是否有接驳台」对应实体位置,由内置数据给定,不接收页面入参。
// 接驳台↔工位 1:1 绑定为系统预置只读主数据,由种子初始化,页面不可更改、也不接收页面入参。
func (s *Service) SaveStation(ctx context.Context, req StationReq, operator string) error {
if req.StationNo < 0 {
return errors.New("工位号非法(需 ≥ 0")
}
if req.StationNo == 0 && req.Name == "" {
return errors.New("工位号非法")
}
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx)
if err != nil {
// 新增工位:工位号不存在即创建
if req.Name == "" {
return errors.New("请填写工位名称")
}
stype := "LINE"
if req.StationType != nil {
stype = *req.StationType
if stype != "LINE" && stype != "OFFLINE" {
return errors.New("工位类型仅支持 LINE/OFFLINE")
}
}
cr := s.ctx.EntClient.Station.Create().
SetStationNo(req.StationNo).SetName(req.Name).SetStationType(stype).SetStatus("ENABLED").
// 页面「新增工位」创建的是非内置工位(可改类型、可删除);内置工位只能由种子/初始化数据产生。
// 是否有接驳台对应实体位置,由内置数据给定,页面新增时一律为 false 且不可手改。
SetIsBuiltin(false).SetHasDock(false)
if req.FlowId != nil && *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("该工艺流程已停用,请先启用后再绑定工位")
}
cr.SetFlowId(*req.FlowId)
}
created, cErr := cr.Save(ctx)
if cErr != nil {
return cErr
}
s.ctx.EventLog.Write(ctx, "station.create", "", operator, "station", "",
"新增工位", map[string]any{"stationNo": created.StationNo, "name": created.Name})
return nil
}
// 仅改名称:flowId 不传(nil)保留原绑定;0=解绑;>0=绑定指定启用流程
upd := s.ctx.EntClient.Station.UpdateOneID(st.ID).SetName(req.Name)
if req.StationType != nil {
t := *req.StationType
if t != "LINE" && t != "OFFLINE" {
return errors.New("工位类型仅支持 LINE/OFFLINE")
}
// 内置工位对应实体产线固定位置(含 PLC 通讯与顺序流转),工位类型不可手动更改。
if st.IsBuiltin && t != st.StationType {
return errors.New("内置工位不允许修改工位类型")
}
if !st.IsBuiltin {
upd.SetStationType(t)
}
}
if req.FlowId != nil {
fid := *req.FlowId
if fid > 0 {
flow, fErr := s.ctx.EntClient.ProcessFlow.Get(ctx, fid)
if fErr != nil {
return errors.New("所选工艺流程不存在")
}
if flow.Status != "ACTIVE" {
return errors.New("该工艺流程已停用,请先启用后再绑定工位")
}
upd.SetFlowId(fid)
} else {
upd.SetFlowId(0)
}
}
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
}
// DeleteStation 删除工位:内置工位不允许删除(对应实体产线位置,需连 PLC 且必须按顺序流转);
// 非内置工位(页面新增的)允许删除。
func (s *Service) DeleteStation(ctx context.Context, id int, operator string) error {
if id <= 0 {
return errors.New("缺少 id")
}
st, err := s.ctx.EntClient.Station.Get(ctx, id)
if err != nil {
return errors.New("工位不存在")
}
if st.IsBuiltin {
return errors.New("内置工位不允许删除")
}
if err := s.ctx.EntClient.Station.DeleteOneID(st.ID).Exec(ctx); err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "station.delete", "", operator, "station", "", "删除工位",
map[string]any{"stationNo": st.StationNo, "name": st.Name})
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})
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("工位不存在或未配置")
}
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)
}
// M4:工位终端按「本工位绑定的工艺流程」驱动。
// 产线定义 = 关联工位(station.flow_id+ 工位号 1→12 顺序,已无独立"工艺路线/路线段"对象。
// 本工位要干哪些步骤,完全取决于 station.flow_id 指向的流程图,工单的 processSeq 只决定走哪些工位。
activeFlowId := st.FlowId
var flow *ent.ProcessFlow
if activeFlowId > 0 {
flow, _ = s.ctx.EntClient.ProcessFlow.Get(ctx, activeFlowId)
}
steps := []*ProcessStepTemplate{}
if flow != nil && flow.Status == "ACTIVE" {
steps = s.flowSteps(ctx, flow.ID)
}
return map[string]any{
"stationNo": st.StationNo,
"stationName": st.Name,
"flowId": activeFlowId,
"flow": flow,
"flowActive": flow != nil && flow.Status == "ACTIVE",
"steps": steps,
"orderNos": orderNos,
}, nil
}
// WorkloadRow 绩效/工作量聚合行(按人/按工位/明细三视图共用)
// 前端按 camelCase 读取,必须显式标注 json tag(否则 Go 默认序列化为 PascalCase 导致列空白)
type WorkloadRow struct {
Operator string `json:"operator"`
StationNo string `json:"stationNo"`
Date string `json:"date"`
FlowId int `json:"flowId"`
ProcessName string `json:"processName"`
DoneCount int `json:"doneCount"`
OkCount int `json:"okCount"`
NgCount int `json:"ngCount"`
// Item M 绩效效率:作业时长取自 workpiece_process.duration_sec
TotalDurationSec int `json:"totalDurationSec"` // 累计作业时长(秒)
AvgDurationSec int `json:"avgDurationSec"` // 平均作业时长(秒)=累计/完成数
Efficiency float64 `json:"efficiency"` // 效率(件/小时)=完成数/(累计时长/3600)
}
// finalizeWorkloadRow 由累计作业时长与完成数派生平均作业时长与效率
func finalizeWorkloadRow(r *WorkloadRow) {
if r.DoneCount > 0 {
r.AvgDurationSec = r.TotalDurationSec / r.DoneCount
}
if r.TotalDurationSec > 0 {
r.Efficiency = float64(r.DoneCount) / (float64(r.TotalDurationSec) / 3600.0)
}
}
// Workload 绩效统计(按人/按工位/明细三视图,各自真分页)。
// orderNo 非空时按工单号模糊过滤(客户第12条「记录查看加一项看工单号查询」)。
func (s *Service) Workload(ctx context.Context, operator, stationNo, orderNo, 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 orderNo != "" {
q = q.Where(workpieceprocess.OrderNoContainsFold(orderNo))
}
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.FlowId}
row, ok := agg[k]
if !ok {
row = &WorkloadRow{Operator: r.Operator, StationNo: r.StationNo, Date: k.date, FlowId: r.FlowId, ProcessName: r.ProcessName}
agg[k] = row
}
row.DoneCount++
row.TotalDurationSec += r.DurationSec
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].FlowId < list[j].FlowId
})
// 三个视图分别聚合(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
o.TotalDurationSec += v.TotalDurationSec
} else {
byOpAgg[opKey] = &WorkloadRow{Operator: v.Operator, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount, TotalDurationSec: v.TotalDurationSec}
}
stKey := v.StationNo
if o, ok := byStAgg[stKey]; ok {
o.DoneCount += v.DoneCount
o.OkCount += v.OkCount
o.NgCount += v.NgCount
o.TotalDurationSec += v.TotalDurationSec
} else {
byStAgg[stKey] = &WorkloadRow{StationNo: v.StationNo, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount, TotalDurationSec: v.TotalDurationSec}
}
}
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 {
finalizeWorkloadRow(v)
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 {
finalizeWorkloadRow(v)
byStList = append(byStList, v)
}
sort.Slice(byStList, func(i, j int) bool { return byStList[i].StationNo < byStList[j].StationNo })
// 明细视图:按 (操作人,工位,日期,工序) 聚合,与前端明细表列(date/doneCount/okCount/ngCount)对齐
for _, v := range list {
finalizeWorkloadRow(v)
}
opTotal, byOpPage := paginate(byOpList, opPage, opSize)
stTotal, byStPage := paginate(byStList, stPage, stSize)
detTotal, detList := paginate(list, 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": detList},
}, nil
}
// ---------- 工艺物料清单(flow_material ----------
// FlowMaterialReq 工艺物料清单条目(保存为整表覆盖)
type FlowMaterialReq struct {
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
ManageMode string `json:"manageMode"` // 1 结构件(批次) / 2 电气件(SN)
UnitQty float64 `json:"unitQty"` // 单台用量
LossRate float64 `json:"lossRate"` // 损耗率(%)
}
// ListFlowMaterials 查某工艺的物料清单(按 ID 升序)
func (s *Service) ListFlowMaterials(ctx context.Context, flowId int) ([]*ent.FlowMaterial, error) {
return s.ctx.EntClient.FlowMaterial.Query().
Where(flowmaterial.FlowId(flowId)).
Order(ent.Asc(flowmaterial.FieldID)).All(ctx)
}
// SaveFlowMaterials 保存工艺物料清单(整表覆盖:先删后插)。
// 物料编码须在 WMS 物料档案存在;WMS 不可达/异常时降级放行并记日志,避免阻塞工艺配置。
func (s *Service) SaveFlowMaterials(ctx context.Context, flowId int, items []FlowMaterialReq, operator string) error {
if flowId <= 0 {
return errors.New("flowId 必填")
}
if _, err := s.ctx.EntClient.ProcessFlow.Get(ctx, flowId); err != nil {
return errors.New("工艺不存在")
}
// WMS 物料存在性校验(缺失拒绝;不可达降级放行)
codes := make([]string, 0, len(items))
seen := map[string]bool{}
for _, it := range items {
if it.MaterialCode == "" {
return errors.New("物料编码不能为空")
}
if seen[it.MaterialCode] {
return errors.New("物料 " + it.MaterialCode + " 重复")
}
seen[it.MaterialCode] = true
codes = append(codes, it.MaterialCode)
}
if len(codes) > 0 && s.ctx.Wms != nil {
if missing, err := s.ctx.Wms.MaterialExists(ctx, codes); err != nil {
s.ctx.EventLog.Write(ctx, "flow.material.wms_skip", "", operator, "process_flow", itoa(flowId),
"WMS 物料校验不可达,已降级放行", map[string]any{"error": err.Error()})
} else if len(missing) > 0 {
return errors.New("以下物料在 WMS 物料档案中不存在:" + strings.Join(missing, "、"))
}
}
tx, err := s.ctx.EntClient.Tx(ctx)
if err != nil {
return err
}
committed := false
defer func() {
if !committed {
_ = tx.Rollback()
}
}()
client := tx.Client()
if _, err := client.FlowMaterial.Delete().Where(flowmaterial.FlowId(flowId)).Exec(ctx); err != nil {
return err
}
for _, it := range items {
if err := client.FlowMaterial.Create().
SetFlowId(flowId).
SetMaterialCode(it.MaterialCode).SetMaterialName(it.MaterialName).
SetSpec(it.Spec).SetUnit(it.Unit).SetManageMode(it.ManageMode).
SetUnitQty(it.UnitQty).SetLossRate(it.LossRate).
Exec(ctx); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
committed = true
s.ctx.EventLog.Write(ctx, "flow.material.save", "", operator, "process_flow", itoa(flowId),
"保存工艺物料清单(整表覆盖)", map[string]any{"flowId": flowId, "count": len(items)})
return nil
}