feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统 - 为BomItem实体添加relatedStandard字段及相关CRUD方法 - 为InspectionRecord实体添加reportNo、materialCode、materialName等字段 - 更新ent schema确保新字段的验证和默认值设置 - 添加必要的数据库迁移和字段映射逻辑
This commit is contained in:
@@ -106,6 +106,25 @@ type ReportProcessReq struct {
|
||||
StationNo int `json:"stationNo"`
|
||||
Steps []StepDataReq `json:"steps"`
|
||||
Binds []BindItemReq `json:"binds"` // 装机绑定(本工序装配物料:结构件批次/电气件SN),随报工原子提交
|
||||
// 作业时长采集(方案 M / P0-3):工位终端在「上料到本工位」时采 startedAt、点「报工」时采 endedAt,
|
||||
// 两端时间戳同源于终端时钟(ISO8601),离线重传也不失真;缺失则回退服务端当前时刻(时长=0)。
|
||||
StartedAt string `json:"startedAt"`
|
||||
EndedAt string `json:"endedAt"`
|
||||
}
|
||||
|
||||
// parseClientTime 解析工位终端上报的 ISO8601 时间戳(兼容带/不带毫秒、Z/时区偏移)。
|
||||
func parseClientTime(s string) (time.Time, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return time.Time{}, errors.New("empty time")
|
||||
}
|
||||
layouts := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02T15:04:05", "2006-01-02 15:04:05"}
|
||||
for _, l := range layouts {
|
||||
if t, err := time.Parse(l, s); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, errors.New("unrecognized time format: " + s)
|
||||
}
|
||||
|
||||
// ReportProcess 工位报工:先应用装机绑定并做强校验(齐套才放行),再写报工实绩 + 步骤考核。
|
||||
@@ -119,52 +138,94 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
|
||||
if err != nil {
|
||||
return errors.New("工件未进线登记")
|
||||
}
|
||||
// 报工是"装机绑定→校验齐套→写实绩→写步骤考核→回写结果/工件状态"的多步写,必须整体事务化(方案 U3):
|
||||
// 任一步失败全部回滚,杜绝"实绩已建但工件状态没推进/结果没落"的半成品脏数据;
|
||||
// 校验(齐套/needCheck)也走同一 tx client,才能读到本事务内刚写入的绑定。
|
||||
tx, err := s.ctx.EntClient.Tx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
client := tx.Client()
|
||||
// 阶段1:装机绑定(随报工提交)→ 落库后强校验齐套
|
||||
stationNo := ""
|
||||
if req.StationNo > 0 {
|
||||
stationNo = itoa(req.StationNo)
|
||||
}
|
||||
if _, err := s.applyBinds(ctx, req.Sn, wp.OrderNo, req.ProcessCode, stationNo, operator, req.Binds); err != nil {
|
||||
if _, err := s.applyBinds(ctx, client, req.Sn, wp.OrderNo, req.ProcessCode, stationNo, operator, req.Binds); err != nil {
|
||||
return err
|
||||
}
|
||||
if miss, err := s.validateStationBinds(ctx, req.Sn, req.ProcessCode); err != nil {
|
||||
if miss, err := s.validateStationBinds(ctx, client, req.Sn, req.ProcessCode); err != nil {
|
||||
return err
|
||||
} else if miss != "" {
|
||||
return errors.New(miss)
|
||||
}
|
||||
// 阶段2:检测确认(needCheck)强校验——服务端为准,前端勾选只是辅助提示。
|
||||
// 本工位绑定工艺流程中标记“需要检测确认”的步骤,报工数据里必须逐条带 checked=true,否则拒绝报工。
|
||||
if err := s.validateNeedCheck(ctx, req); err != nil {
|
||||
if err := s.validateNeedCheck(ctx, client, req); err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
proc, err := s.ctx.EntClient.WorkpieceProcess.Create().
|
||||
// 作业时长(方案 M / P0-3):startedAt=进工位(上料)时刻、endedAt=报工时刻,均由工位终端采集;
|
||||
// 缺失或非法则回退到服务端当前时刻(durationSec=0),时钟回拨导致负数则时长归零。
|
||||
startedAt, endedAt := now, now
|
||||
if t, e := parseClientTime(req.StartedAt); e == nil {
|
||||
startedAt = t
|
||||
}
|
||||
if t, e := parseClientTime(req.EndedAt); e == nil {
|
||||
endedAt = t
|
||||
}
|
||||
durationSec := int(endedAt.Sub(startedAt).Seconds())
|
||||
if durationSec < 0 {
|
||||
durationSec = 0
|
||||
endedAt = startedAt
|
||||
}
|
||||
proc, err := client.WorkpieceProcess.Create().
|
||||
SetSn(req.Sn).SetProcessCode(req.ProcessCode).
|
||||
SetStationNo(itoa(req.StationNo)).SetOrderNo(wp.OrderNo).
|
||||
SetProcessName(processName(req.ProcessCode)).
|
||||
SetStatus("DONE").SetOperator(operator).SetResult("OK").
|
||||
SetStartedAt(now).SetEndedAt(now).Save(ctx)
|
||||
SetStartedAt(startedAt).SetEndedAt(endedAt).SetDurationSec(durationSec).Save(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allOK := true
|
||||
for _, st := range req.Steps {
|
||||
if ok, e := s.writeStepData(ctx, req.Sn, proc.ID, st, operator); e == nil && !ok {
|
||||
ok, e := s.writeStepData(ctx, client, req.Sn, proc.ID, st, operator)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if !ok {
|
||||
allOK = false
|
||||
}
|
||||
}
|
||||
if bad, _ := s.ctx.EntClient.StepData.Query().
|
||||
Where(stepdata.ProcessId(proc.ID), stepdata.IsOK(false)).Exist(ctx); bad {
|
||||
if bad, e := client.StepData.Query().
|
||||
Where(stepdata.ProcessId(proc.ID), stepdata.IsOK(false)).Exist(ctx); e != nil {
|
||||
return e
|
||||
} else if bad {
|
||||
allOK = false
|
||||
}
|
||||
result := "OK"
|
||||
if !allOK {
|
||||
result = "NG"
|
||||
}
|
||||
_, _ = s.ctx.EntClient.WorkpieceProcess.UpdateOneID(proc.ID).SetResult(result).Save(ctx)
|
||||
_, _ = s.ctx.EntClient.Workpiece.UpdateOneID(wp.ID).
|
||||
SetCurrentProcess(req.ProcessCode).SetStatus("PROCESSING").Save(ctx)
|
||||
if _, err := client.WorkpieceProcess.UpdateOneID(proc.ID).SetResult(result).Save(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := client.Workpiece.UpdateOneID(wp.ID).
|
||||
SetCurrentProcess(req.ProcessCode).SetStatus("PROCESSING").Save(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
committed = true
|
||||
|
||||
s.ctx.EventLog.Write(ctx, "workpiece.process.report", wp.OrderNo, operator, "workpiece", req.Sn,
|
||||
"工位报工", map[string]any{"processCode": req.ProcessCode, "result": result})
|
||||
@@ -176,15 +237,15 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
|
||||
// 规则:本工位绑定的工艺流程中,凡标记 needCheck=true 的工艺步骤,
|
||||
// 报工请求里必须存在对应 stepId 且 checked=true 的步骤数据;缺失则拒绝报工。
|
||||
// 工位未绑定流程、或流程内没有 needCheck 步骤时不拦截(与现场配置保持一致)。
|
||||
func (s *Service) validateNeedCheck(ctx context.Context, req ReportProcessReq) error {
|
||||
func (s *Service) validateNeedCheck(ctx context.Context, client *ent.Client, req ReportProcessReq) error {
|
||||
if req.StationNo <= 0 {
|
||||
return nil
|
||||
}
|
||||
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx)
|
||||
st, err := client.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx)
|
||||
if err != nil || st.FlowId <= 0 {
|
||||
return nil
|
||||
}
|
||||
steps, err := s.ctx.EntClient.ProcessStep.Query().
|
||||
steps, err := client.ProcessStep.Query().
|
||||
Where(processstep.FlowId(st.FlowId), processstep.NeedCheck(true)).All(ctx)
|
||||
if err != nil || len(steps) == 0 {
|
||||
return nil
|
||||
@@ -212,9 +273,12 @@ func (s *Service) validateNeedCheck(ctx context.Context, req ReportProcessReq) e
|
||||
}
|
||||
|
||||
// writeStepData 写一条步骤考核数据,按标准自动判定是否合格
|
||||
func (s *Service) writeStepData(ctx context.Context, sn string, processId int, st StepDataReq, operator string) (bool, error) {
|
||||
func (s *Service) writeStepData(ctx context.Context, client *ent.Client, sn string, processId int, st StepDataReq, operator string) (bool, error) {
|
||||
ok := true
|
||||
crits, _ := s.ctx.EntClient.StepCriterion.Query().Where(stepcriterion.StepId(st.StepId)).All(ctx)
|
||||
crits, err := client.StepCriterion.Query().Where(stepcriterion.StepId(st.StepId)).All(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, c := range crits {
|
||||
if !evalLogic(c, st.Value) {
|
||||
ok = false
|
||||
@@ -227,7 +291,7 @@ func (s *Service) writeStepData(ctx context.Context, sn string, processId int, s
|
||||
clogic = crits[0].Logic
|
||||
cid = crits[0].ID
|
||||
}
|
||||
_, err := s.ctx.EntClient.StepData.Create().
|
||||
_, err = client.StepData.Create().
|
||||
SetSn(sn).SetProcessId(processId).SetStepId(st.StepId).
|
||||
SetStepName(st.Name).SetCriterionId(cid).SetCriterionName(cname).
|
||||
SetCriterionLogic(clogic).SetValue(st.Value).SetValueText(st.Text).
|
||||
@@ -249,7 +313,7 @@ func (s *Service) DoneWorkpiece(ctx context.Context, req DoneReq, operator strin
|
||||
return errors.New("工件不存在")
|
||||
}
|
||||
// 兜底强校验:工艺路线中每个工序的装配物料都须绑齐(BOM 未配置工序时自动放行)
|
||||
if miss, err := s.validateAllProcessBinds(ctx, req.Sn); err != nil {
|
||||
if miss, err := s.validateAllProcessBinds(ctx, s.ctx.EntClient, req.Sn); err != nil {
|
||||
return err
|
||||
} else if miss != "" {
|
||||
return errors.New("完工前必须绑齐全部装配物料:" + miss)
|
||||
@@ -337,3 +401,105 @@ func (s *Service) ListWorkpieces(ctx context.Context, sn, orderNo string) ([]*en
|
||||
}
|
||||
return q.Order(ent.Desc(workpiece.FieldID)).Limit(500).All(ctx)
|
||||
}
|
||||
|
||||
// InProcessItem 在制品(已进线未完工)视图项。跨天工件继续停留,日排产不重复排。
|
||||
type InProcessItem struct {
|
||||
Sn string `json:"sn"`
|
||||
OrderNo string `json:"orderNo"`
|
||||
ProductCode string `json:"productCode"`
|
||||
ProductName string `json:"productName"`
|
||||
CurrentStation int `json:"currentStation"` // 0=上线位, 13=下线位, 其余为物理工位
|
||||
CurrentStationName string `json:"currentStationName"` // 工位显示名(上线位/下线位/装配X)
|
||||
Status string `json:"status"`
|
||||
OnlineAt string `json:"onlineAt"`
|
||||
DwellSec int64 `json:"dwellSec"` // 停留时长(秒),自进线起算
|
||||
DoneSteps int `json:"doneSteps"`
|
||||
TotalSteps int `json:"totalSteps"`
|
||||
}
|
||||
|
||||
// WorkpiecesInProcess 列出停在各工位、尚未完工的在制品(ONLINE/PROCESSING/REPAIR)。
|
||||
// stationNo<=0 返回全部;stationNo>0 仅返回当前停在该工位(0=上线位, 13=下线位)的工件。
|
||||
// 已进线未完工的工件跨天继续,日排产仅排「未开工数量」,不会重复排产。
|
||||
func (s *Service) WorkpiecesInProcess(ctx context.Context, stationNo int) ([]InProcessItem, error) {
|
||||
list, err := s.ctx.EntClient.Workpiece.Query().
|
||||
Where(workpiece.Or(
|
||||
workpiece.Status("ONLINE"),
|
||||
workpiece.Status("PROCESSING"),
|
||||
workpiece.Status("REPAIR"),
|
||||
)).
|
||||
Order(ent.Desc(workpiece.FieldOnlineAt)).All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]InProcessItem, 0, len(list))
|
||||
for _, wp := range list {
|
||||
seq := ParseProcessSeq(wp.ProcessSeq)
|
||||
if len(seq) == 0 {
|
||||
seq = ParseProcessSeq(FullProcessSeq)
|
||||
}
|
||||
maxStation := 0
|
||||
for _, c := range seq {
|
||||
if c > maxStation {
|
||||
maxStation = c
|
||||
}
|
||||
}
|
||||
// 推算当前物理工位:ONLINE 在 上线位(0);PROCESSING 且已到末端 → 下线位(13)
|
||||
cur := 0
|
||||
if wp.Status == "PROCESSING" || wp.Status == "REPAIR" {
|
||||
cp := wp.CurrentProcess
|
||||
if cp <= 0 {
|
||||
cp = maxStation
|
||||
}
|
||||
if cp >= maxStation && wp.Status == "PROCESSING" {
|
||||
cur = 13
|
||||
} else {
|
||||
cur = cp
|
||||
}
|
||||
}
|
||||
if stationNo > 0 && cur != stationNo {
|
||||
continue
|
||||
}
|
||||
dwell := int64(0)
|
||||
var onlineAt string
|
||||
if wp.OnlineAt != nil {
|
||||
dwell = int64(time.Since(*wp.OnlineAt).Seconds())
|
||||
onlineAt = wp.OnlineAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
done := 0
|
||||
if procs, e := s.ctx.EntClient.WorkpieceProcess.Query().
|
||||
Where(workpieceprocess.Sn(wp.Sn), workpieceprocess.Status("DONE")).Count(ctx); e == nil {
|
||||
done = procs
|
||||
}
|
||||
code, name := "", ""
|
||||
if wp.WorkOrderId > 0 {
|
||||
if wo, e := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId); e == nil {
|
||||
code, name = wo.ProductCode, wo.ProductName
|
||||
}
|
||||
}
|
||||
if code == "" && wp.OrderNo != "" {
|
||||
if wo, e := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(wp.OrderNo)).First(ctx); e == nil {
|
||||
code, name = wo.ProductCode, wo.ProductName
|
||||
}
|
||||
}
|
||||
stName := stationDisplayName("", cur)
|
||||
if cur == 0 {
|
||||
stName = "上线位(虚拟)"
|
||||
} else if cur == 13 {
|
||||
stName = "下线位(虚拟)"
|
||||
}
|
||||
out = append(out, InProcessItem{
|
||||
Sn: wp.Sn,
|
||||
OrderNo: wp.OrderNo,
|
||||
ProductCode: code,
|
||||
ProductName: name,
|
||||
CurrentStation: cur,
|
||||
CurrentStationName: stName,
|
||||
Status: wp.Status,
|
||||
OnlineAt: onlineAt,
|
||||
DwellSec: dwell,
|
||||
DoneSteps: done,
|
||||
TotalSteps: len(seq),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user