feat: 完成多模块迭代更新

- 新增过程巡检按步骤上报、数量不符上报、工位退料功能
- 增加工单工程编号三级归属字段
- 新增物料安全库存与缺货预警
- 新增附件管理、退库单管理模块
- 优化生产看板展示逻辑与前端页面文案
- 清理冗余的工艺路线相关代码与备份文件
This commit is contained in:
SunYF
2026-09-15 16:37:39 +08:00
parent 951f3dfecd
commit 4a5e2d7bac
494 changed files with 24702 additions and 139223 deletions
@@ -37,6 +37,11 @@ type InspectionReq struct {
Photo string `json:"photo"` // 拍照文件名
Remark string `json:"remark"` // 文字描述/签字
Operator string `json:"operator"` // 操作人
// P0-4 过程巡检按步骤维度
ProcessCode int `json:"processCode"` // 所属工位号
StepId int `json:"stepId"` // 工艺步骤ID
StepName string `json:"stepName"` // 工艺步骤名称
MeasuredValue string `json:"measuredValue"` // 实测值
}
// CreateInspection 提交巡检记录;ALARM 类型同时触发看板报警(SSE + 清缓存)
@@ -66,6 +71,10 @@ func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, opera
SetPhoto(req.Photo).
SetRemark(req.Remark).
SetOperator(req.Operator).
SetProcessCode(req.ProcessCode).
SetStepId(req.StepId).
SetStepName(req.StepName).
SetMeasuredValue(req.MeasuredValue).
Save(ctx)
if err != nil {
return err
+11 -79
View File
@@ -361,76 +361,10 @@ func (s *Service) StationTask(ctx context.Context, stationNo int) (map[string]an
for _, w := range wos {
orderNos = append(orderNos, w.WorkOrderNo)
}
// M4:工位终端按"工艺路线段"驱动(段=并行工位组,谁空停谁)
// 解析活跃工单的 routeSnapshot,找出本工位所属的路线段,供终端按段执行流程
routeSegments := []map[string]any{}
for _, w := range wos {
if len(w.RouteSnapshot) == 0 {
continue
}
for _, seg := range w.RouteSnapshot {
stations, _ := seg["stations"].([]any)
inSeg := false
for _, sv := range stations {
switch n := sv.(type) {
case float64:
if int(n) == stationNo {
inSeg = true
}
case int:
if n == stationNo {
inSeg = true
}
}
if inSeg {
break
}
}
if !inSeg {
continue
}
flowId := 0
if fv, ok := seg["flowId"].(float64); ok {
flowId = int(fv)
} else if fv, ok := seg["flowId"].(int); ok {
flowId = fv
}
routeSegments = append(routeSegments, map[string]any{
"orderNo": w.WorkOrderNo,
"seq": seg["seq"],
"segmentType": seg["segmentType"],
"flowId": flowId,
"stations": stations,
})
}
}
// 按段驱动:本工位实际执行的工艺流程取自「本工位所属路线段」的 flowId;
// 工单未配置路线段(旧数据/未排路线)时回退到工位绑定的 flow_id。
activeFlowId := 0
currentSegment := map[string]any{}
minSeq := -1
for _, sg := range routeSegments {
seq, _ := sg["seq"].(float64)
seqInt := int(seq)
if sgv, ok := sg["seq"].(int); ok {
seqInt = sgv
}
if minSeq < 0 || seqInt < minSeq {
minSeq, currentSegment = seqInt, sg
}
}
if len(currentSegment) > 0 {
switch v := currentSegment["flowId"].(type) {
case int:
activeFlowId = v
case float64:
activeFlowId = int(v)
}
}
if activeFlowId == 0 {
activeFlowId = st.FlowId
}
// 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)
@@ -440,15 +374,13 @@ func (s *Service) StationTask(ctx context.Context, stationNo int) (map[string]an
steps = s.flowSteps(ctx, flow.ID)
}
return map[string]any{
"stationNo": st.StationNo,
"stationName": st.Name,
"flowId": activeFlowId,
"segment": currentSegment,
"flow": flow,
"flowActive": flow != nil && flow.Status == "ACTIVE",
"steps": steps,
"orderNos": orderNos,
"routeSegments": routeSegments,
"stationNo": st.StationNo,
"stationName": st.Name,
"flowId": activeFlowId,
"flow": flow,
"flowActive": flow != nil && flow.Status == "ACTIVE",
"steps": steps,
"orderNos": orderNos,
}, nil
}
-271
View File
@@ -1,271 +0,0 @@
package logic
import (
"context"
"errors"
"fmt"
"sort"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/processroute"
"bj_power_mes/ent/routesegment"
"bj_power_mes/ent/station"
)
// ---------- 工艺路线(M2:有序并行工位段) ----------
// RouteSegmentReq 路线段请求:一段 = 一个流程图 + 一组并行工位
type RouteSegmentReq struct {
Seq int `json:"seq"`
FlowId int `json:"flowId"`
Stations []int `json:"stations"`
SegmentType string `json:"segmentType"` // LINE / OFFLINE
Remark string `json:"remark"`
}
// RouteReq 工艺路线保存请求
type RouteReq struct {
Id int `json:"id"`
Name string `json:"name"`
ProductTypeId *int `json:"productTypeId"`
Status string `json:"status"`
Remark string `json:"remark"`
Segments []RouteSegmentReq `json:"segments"`
}
// RouteSegmentVO 路线段视图
type RouteSegmentVO struct {
Id int `json:"id"`
RouteId int `json:"routeId"`
Seq int `json:"seq"`
FlowId int `json:"flowId"`
FlowName string `json:"flowName"`
Stations []int `json:"stations"`
SegmentType string `json:"segmentType"`
Remark string `json:"remark"`
}
// RouteVO 工艺路线视图(含段列表)
type RouteVO struct {
Id int `json:"id"`
Name string `json:"name"`
ProductTypeId int `json:"productTypeId"`
ProductName string `json:"productName"`
Status string `json:"status"`
Remark string `json:"remark"`
Segments []*RouteSegmentVO `json:"segments"`
CreatedAt time.Time `json:"createdAt"`
}
// SaveRoute 保存工艺路线(含段)。约束:段序号>=0;LINE 段工位须存在且不跨段重复;OFFLINE 段(0/13 线下位)不校验工位。
func (s *Service) SaveRoute(ctx context.Context, req RouteReq, operator string) error {
if req.Name == "" {
return errors.New("路线名称必填")
}
status := req.Status
if status == "" {
status = "ACTIVE"
}
if status != "ACTIVE" && status != "INACTIVE" {
return errors.New("非法状态")
}
if len(req.Segments) == 0 {
return errors.New("路线至少包含一个段")
}
seenStation := map[int]bool{}
for _, seg := range req.Segments {
if seg.Seq < 0 {
return errors.New("段序号非法(从 0 开始)")
}
if seg.FlowId > 0 {
if _, err := s.ctx.EntClient.ProcessFlow.Get(ctx, seg.FlowId); err != nil {
return errors.New("所选工艺流程不存在")
}
}
stType := seg.SegmentType
if stType == "" {
stType = "LINE"
}
if stType != "LINE" && stType != "OFFLINE" {
return errors.New("段类型仅支持 LINE/OFFLINE")
}
if len(seg.Stations) == 0 {
return errors.New("每段至少包含一个工位")
}
if stType == "LINE" {
for _, no := range seg.Stations {
if no < 1 {
return errors.New("工位号非法")
}
if _, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(no)).First(ctx); err != nil {
return fmt.Errorf("工位 %d 不存在,请先在工位主数据中维护", no)
}
if seenStation[no] {
return fmt.Errorf("工位 %d 在路线中重复出现", no)
}
seenStation[no] = true
}
}
}
var routeID int
if req.Id > 0 {
upd := s.ctx.EntClient.ProcessRoute.UpdateOneID(req.Id).
SetName(req.Name).SetStatus(status).SetRemark(req.Remark)
if req.ProductTypeId != nil {
upd.SetProductTypeId(*req.ProductTypeId)
} else {
upd.ClearProductTypeId()
}
if _, err := upd.Save(ctx); err != nil {
return err
}
routeID = req.Id
} else {
cr := s.ctx.EntClient.ProcessRoute.Create().
SetName(req.Name).SetStatus(status).SetRemark(req.Remark)
if req.ProductTypeId != nil {
cr.SetProductTypeId(*req.ProductTypeId)
}
r, err := cr.Save(ctx)
if err != nil {
return err
}
routeID = r.ID
}
_, _ = s.ctx.EntClient.RouteSegment.Delete().Where(routesegment.RouteId(routeID)).Exec(ctx)
for _, seg := range req.Segments {
stType := seg.SegmentType
if stType == "" {
stType = "LINE"
}
if _, err := s.ctx.EntClient.RouteSegment.Create().
SetRouteId(routeID).SetSeq(seg.Seq).SetFlowId(seg.FlowId).
SetStations(seg.Stations).SetSegmentType(stType).SetRemark(seg.Remark).Save(ctx); err != nil {
return err
}
}
s.ctx.EventLog.Write(ctx, "route.save", "", operator, "process_route", "", "维护工艺路线",
map[string]any{"routeId": routeID, "segments": len(req.Segments)})
return nil
}
func (s *Service) routeVO(ctx context.Context, r *ent.ProcessRoute) (*RouteVO, error) {
segs, err := s.ctx.EntClient.RouteSegment.Query().
Where(routesegment.RouteId(r.ID)).Order(ent.Asc(routesegment.FieldSeq)).All(ctx)
if err != nil {
return nil, err
}
flows, _ := s.ctx.EntClient.ProcessFlow.Query().All(ctx)
flowMap := map[int]string{}
for _, f := range flows {
flowMap[f.ID] = f.Name
}
segVOs := make([]*RouteSegmentVO, 0, len(segs))
for _, seg := range segs {
segVOs = append(segVOs, &RouteSegmentVO{
Id: seg.ID, RouteId: seg.RouteId, Seq: seg.Seq,
FlowId: seg.FlowId, FlowName: flowMap[seg.FlowId],
Stations: seg.Stations, SegmentType: seg.SegmentType, Remark: seg.Remark,
})
}
return &RouteVO{
Id: r.ID, Name: r.Name, ProductTypeId: r.ProductTypeId,
Status: r.Status, Remark: r.Remark, Segments: segVOs, CreatedAt: r.CreatedAt,
}, nil
}
// ListRoutes 查询工艺路线(page>0 真分页;否则全量)
func (s *Service) ListRoutes(ctx context.Context, name, status string, page, pageSize int) (any, error) {
q := s.ctx.EntClient.ProcessRoute.Query()
if name != "" {
q = q.Where(processroute.NameContainsFold(name))
}
if status != "" {
q = q.Where(processroute.Status(status))
}
routes, err := q.Order(ent.Desc(processroute.FieldID)).All(ctx)
if err != nil {
return nil, err
}
pts, _ := s.ctx.EntClient.ProductType.Query().All(ctx)
ptMap := map[int]string{}
for _, p := range pts {
ptMap[p.ID] = p.Name
}
out := make([]*RouteVO, 0, len(routes))
for _, r := range routes {
vo, err := s.routeVO(ctx, r)
if err != nil {
return nil, err
}
vo.ProductName = ptMap[r.ProductTypeId]
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
}
// GetRoute 获取单条路线详情
func (s *Service) GetRoute(ctx context.Context, id int) (*RouteVO, error) {
r, err := s.ctx.EntClient.ProcessRoute.Get(ctx, id)
if err != nil {
return nil, errors.New("工艺路线不存在")
}
return s.routeVO(ctx, r)
}
// GetActiveRouteByProductType 取某产品型号的启用路线(工单建单自动带出)
func (s *Service) GetActiveRouteByProductType(ctx context.Context, productTypeId int) (*RouteVO, error) {
r, err := s.ctx.EntClient.ProcessRoute.Query().
Where(processroute.ProductTypeId(productTypeId), processroute.Status("ACTIVE")).
Order(ent.Desc(processroute.FieldID)).First(ctx)
if err != nil {
return nil, err
}
return s.routeVO(ctx, r)
}
// DeleteRoute 删除路线(先清段)
func (s *Service) DeleteRoute(ctx context.Context, id int, operator string) error {
_, _ = s.ctx.EntClient.RouteSegment.Delete().Where(routesegment.RouteId(id)).Exec(ctx)
s.ctx.EventLog.Write(ctx, "route.delete", "", operator, "process_route", "", "删除工艺路线", map[string]any{"routeId": id})
return s.ctx.EntClient.ProcessRoute.DeleteOneID(id).Exec(ctx)
}
// BuildRouteSnapshot 由路线构建工单段快照,并返回展开的工位顺序(仅 LINE 段,兼容旧工位终端 logic)
func (s *Service) BuildRouteSnapshot(route *RouteVO) ([]map[string]any, []int) {
snapshot := make([]map[string]any, 0, len(route.Segments))
lineStations := []int{}
seen := map[int]bool{}
for _, seg := range route.Segments {
snapshot = append(snapshot, map[string]any{
"seq": seg.Seq,
"flowId": seg.FlowId,
"flowName": seg.FlowName,
"stations": seg.Stations,
"segmentType": seg.SegmentType,
})
if seg.SegmentType == "LINE" {
for _, no := range seg.Stations {
if !seen[no] {
seen[no] = true
lineStations = append(lineStations, no)
}
}
}
}
sort.Ints(lineStations)
return snapshot, lineStations
}
+44 -8
View File
@@ -26,14 +26,14 @@ func (s *Service) Seed(ctx context.Context) error {
codes = []string{
"produce.workorder", "produce.dailyplan", "produce.bom", "produce.material", "produce.scan",
"produce.trace", "produce.torque", "produce.plc", "produce.product",
"produce.processflow", "produce.station", "produce.performance", "produce.route",
"produce.processflow", "produce.station", "produce.performance", "produce.processcard",
// 按钮级权限(块2
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate",
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate",
"produce.qtyreport",
"produce.plc:send", "produce.product:add", "produce.product:edit", "produce.product:delete",
"produce.processflow:add", "produce.processflow:edit", "produce.processflow:delete",
"produce.processflow:upload", "produce.station:edit",
"produce.route:add", "produce.route:edit", "produce.route:delete",
}
} else if r.code == "INSPECTOR" {
codes = []string{"produce.trace", "produce.torque", "produce.performance", "sys.eventlog",
@@ -75,11 +75,12 @@ func (s *Service) Seed(ctx context.Context) error {
{"produce.torque", "拧紧查询", "MENU", "/torque"},
{"produce.processflow", "工艺流程", "MENU", "/process-flow"},
{"produce.station", "关联工位", "MENU", "/station"},
{"produce.route", "工艺路线", "MENU", "/process-route"},
{"produce.performance", "绩效报表", "MENU", "/performance"},
{"produce.scan", "手动报工", "MENU", "/scan"},
{"produce.trace", "工件追溯", "MENU", "/trace"},
{"produce.processcard", "生产流程卡", "MENU", "/process-card"},
{"produce.product", "产品型号", "MENU", "/product-type"},
{"produce.qtyreport", "数量不符上报", "MENU", "/qty-report"},
{"sys.inspect", "巡检终端", "MENU", "/inspect"},
{"sys.eventlog", "操作日志", "MENU", "/event-log"},
// sys.rbac(角色权限管理) 已拆分:账号管理 + 角色管理(对齐 WMS 系统管理菜单)
@@ -113,9 +114,6 @@ func (s *Service) Seed(ctx context.Context) error {
{"produce.processflow:delete", "工艺流程-删除", "BUTTON", "", "produce.processflow"},
{"produce.processflow:upload", "工艺流程-上传图纸", "BUTTON", "", "produce.processflow"},
{"produce.station:edit", "工位-绑定", "BUTTON", "", "produce.station"},
{"produce.route:add", "工艺路线-新增", "BUTTON", "", "produce.route"},
{"produce.route:edit", "工艺路线-编辑", "BUTTON", "", "produce.route"},
{"produce.route:delete", "工艺路线-删除", "BUTTON", "", "produce.route"},
{"sys.account:add", "账号-新增", "BUTTON", "", "sys.account"},
{"sys.account:edit", "账号-编辑", "BUTTON", "", "sys.account"},
{"sys.account:delete", "账号-删除", "BUTTON", "", "sys.account"},
@@ -147,6 +145,9 @@ func (s *Service) Seed(ctx context.Context) error {
// 3.2 旧版权限码迁移(sys.rbac 家族 → sys.account / sys.role,幂等)
migrateLegacySysRbac(ctx, s)
// 3.3 工艺路线模块已整块删除(D1):清掉存量角色里的路线权限码与旧菜单/按钮定义行(幂等)
migrateLegacyRoute(ctx, s)
// 4. 初始化工位与默认工艺流程(工位绑定流程,流程承载步骤)
seedFlowsAndStations(ctx, s)
return nil
@@ -226,3 +227,38 @@ func migrateLegacySysRbac(ctx context.Context, s *Service) {
Where(permission.Code(oldCode)).Exec(ctx)
}
}
// migrateLegacyRoute 清理已删除的「工艺路线」模块残留(幂等,可重复执行):
// 产线定义已简化为「关联工位(station.flow_id) + 工位号 1→12 顺序 + 工单工位组合」,
// 工艺路线/路线段整块删除,故需从存量角色的 permissionCodes 中摘除路线权限码,
// 并删除对应的菜单/按钮权限定义行,避免菜单树里留下失效入口。
func migrateLegacyRoute(ctx context.Context, s *Service) {
legacyCodes := map[string]bool{
"produce.route": true,
"produce.route:add": true,
"produce.route:edit": true,
"produce.route:delete": true,
}
// 1) 角色 permissionCodes 剔除
roleList, _ := s.ctx.EntClient.Role.Query().All(ctx)
for _, rl := range roleList {
kept := make([]string, 0, len(rl.PermissionCodes))
changed := false
for _, c := range rl.PermissionCodes {
if legacyCodes[c] {
changed = true
continue
}
kept = append(kept, c)
}
if changed {
_ = s.ctx.EntClient.Role.UpdateOneID(rl.ID).
SetPermissionCodes(mergeCodes(kept, nil)).Exec(ctx)
}
}
// 2) 删除权限定义行
for code := range legacyCodes {
_, _ = s.ctx.EntClient.Permission.Delete().
Where(permission.Code(code)).Exec(ctx)
}
}
+185
View File
@@ -0,0 +1,185 @@
package logic
import (
"context"
"errors"
"fmt"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/inspectionrecord"
"bj_power_mes/ent/materialqtyreport"
"bj_power_mes/ent/workorder"
)
// ============ 工位终端业务操作(P0-3 / 数量不符 / 退库 / 工序间检验记录) ============
// EmergencyCall 应急呼叫:落预警消息(type=emergency_call),预警中心实时展示 + pad 顶部提示。
// 纯软件方案(客户 L614 已定:发消息给班组长,不依赖安灯硬件)。
func (s *Service) EmergencyCall(ctx context.Context, stationNo int, orderNo, remark, operator string) error {
if stationNo <= 0 {
return errors.New("缺少工位号")
}
content := fmt.Sprintf("工位 %d 发起应急呼叫", stationNo)
if orderNo != "" {
content += "(工单 " + orderNo + ""
}
if remark != "" {
content += "" + remark
}
_, err := s.ctx.EntClient.Alert.Create().
SetRuleId(0).
SetType("emergency_call").
SetTitle("应急呼叫").
SetContent(content).
SetRefType("station").
SetRefId(fmt.Sprintf("%d", stationNo)).
SetReceiver("").
SetStatus("UNREAD").
Save(ctx)
if err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "station.emergency", orderNo, operator, "alert",
fmt.Sprintf("%d", stationNo), "工位应急呼叫", map[string]any{"stationNo": stationNo, "remark": remark})
s.notifyDashboard()
return nil
}
// QtyReportReq 数量不符上报请求(工位收到料数量与下发数量不一致)
type QtyReportReq struct {
StationNo int `json:"stationNo"`
OrderNo string `json:"orderNo"`
Sn string `json:"sn"`
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
PlanQty int `json:"planQty"`
ActualQty int `json:"actualQty"`
Cause string `json:"cause"`
Operator string `json:"operator"`
}
// CreateQtyReport 数量不符上报:落 material_qty_report + 预警中心消息(type=qty_diff)。
func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error {
if req.StationNo <= 0 || req.MaterialCode == "" {
return errors.New("工位号与物料编码必填")
}
diff := req.PlanQty - req.ActualQty
_, err := s.ctx.EntClient.MaterialQtyReport.Create().
SetStationNo(req.StationNo).
SetOrderNo(req.OrderNo).
SetSn(req.Sn).
SetMaterialCode(req.MaterialCode).
SetMaterialName(req.MaterialName).
SetPlanQty(req.PlanQty).
SetActualQty(req.ActualQty).
SetDiffQty(diff).
SetCause(req.Cause).
SetStatus("PENDING").
SetOperator(req.Operator).
Save(ctx)
if err != nil {
return err
}
content := fmt.Sprintf("工位 %d 数量不符:物料 %s 应发 %d / 实收 %d / 差异 %d。原因:%s",
req.StationNo, req.MaterialCode, req.PlanQty, req.ActualQty, diff, req.Cause)
_, _ = s.ctx.EntClient.Alert.Create().
SetRuleId(0).
SetType("qty_diff").
SetTitle("数量不符上报").
SetContent(content).
SetRefType("material_qty_report").
SetRefId(req.MaterialCode).
SetReceiver("").
SetStatus("UNREAD").
Save(ctx)
s.ctx.EventLog.Write(ctx, "station.qty_report", req.OrderNo, req.Operator, "material_qty_report",
req.MaterialCode, "数量不符上报", map[string]any{"stationNo": req.StationNo, "diff": diff})
s.notifyDashboard()
return nil
}
// ListQtyReports 数量不符上报列表
func (s *Service) ListQtyReports(ctx context.Context, status, orderNo, materialCode string) ([]*ent.MaterialQtyReport, error) {
q := s.ctx.EntClient.MaterialQtyReport.Query()
if status != "" {
q = q.Where(materialqtyreport.Status(status))
}
if orderNo != "" {
q = q.Where(materialqtyreport.OrderNoContainsFold(orderNo))
}
if materialCode != "" {
q = q.Where(materialqtyreport.MaterialCodeContainsFold(materialCode))
}
return q.Order(ent.Desc(materialqtyreport.FieldCreatedAt), ent.Desc(materialqtyreport.FieldID)).All(ctx)
}
// StationReturnMaterial 工位退料回库房:落事件日志 + 调 WMS 预建待确认退库单。
// 退库单在 WMS 侧由库管确认收货后库存加回(链路封闭,问题记录 L491)。
func (s *Service) StationReturnMaterial(ctx context.Context, orderNo string, stationNo int, sn, materialCode, materialName, spec, reason, operator, unit string, qty int) error {
if stationNo <= 0 || materialCode == "" || qty <= 0 {
return errors.New("工位号、物料编码、退库数量必填")
}
s.ctx.EventLog.Write(ctx, "station.return_material", orderNo, operator, "return_order",
materialCode, "工位退料回库房", map[string]any{"stationNo": stationNo, "qty": qty, "reason": reason})
// 调 WMS 内部 API 预建退库单(失败降级:记录日志但不阻断工位操作)
if s.ctx.Wms != nil {
if err := s.ctx.Wms.CreateReturnOrder(ctx, orderNo, sn, materialCode, materialName, spec, reason, operator, stationNo, qty, unit); err != nil {
return fmt.Errorf("通知 WMS 建退库单失败:%w", err)
}
}
return nil
}
// ============ 一键生成工序间检验记录(P0-4 ============
// InterProcessInspection 工序间检验记录汇总项
type InterProcessInspection struct {
OrderNo string `json:"orderNo"`
StationNo int `json:"stationNo"`
StepId int `json:"stepId"`
StepName string `json:"stepName"`
Sn string `json:"sn"`
Result string `json:"result"`
Measured string `json:"measuredValue"`
Photo string `json:"photo"`
Operator string `json:"operator"`
CreatedAt time.Time `json:"createdAt"`
}
// GenerateInterProcessInspection 汇总某工单 PROCESS 类巡检记录(按步骤维度)生成工序间检验记录。
// 返回结构化列表,前端/H5 据此渲染打印(PDF/Excel)。
func (s *Service) GenerateInterProcessInspection(ctx context.Context, orderNo string) ([]*InterProcessInspection, error) {
if orderNo == "" {
return nil, errors.New("工单号必填")
}
if _, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(orderNo)).First(ctx); err != nil {
return nil, errors.New("工单不存在:" + orderNo)
}
rows, err := s.ctx.EntClient.InspectionRecord.Query().
Where(
inspectionrecord.Category("PROCESS"),
inspectionrecord.OrderNo(orderNo),
).
Order(ent.Desc(inspectionrecord.FieldCreatedAt), ent.Desc(inspectionrecord.FieldID)).
All(ctx)
if err != nil {
return nil, err
}
out := make([]*InterProcessInspection, 0, len(rows))
for _, r := range rows {
out = append(out, &InterProcessInspection{
OrderNo: r.OrderNo,
StationNo: atoiSafe(r.StationNo),
StepId: r.StepId,
StepName: r.StepName,
Sn: r.Sn,
Result: r.Result,
Measured: r.MeasuredValue,
Photo: r.Photo,
Operator: r.Operator,
CreatedAt: r.CreatedAt,
})
}
return out, nil
}
+17 -29
View File
@@ -4,8 +4,6 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"bj_power_mes/ent"
@@ -19,7 +17,6 @@ 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"`
@@ -27,6 +24,10 @@ type WorkOrderReq struct {
Status string `json:"status"`
PlanStart string `json:"planStart"`
PlanEnd string `json:"planEnd"`
// 工程编号体系(合同 → 工程编号 → 产品序号)
ContractNo string `json:"contractNo"`
ProjectNo string `json:"projectNo"`
ProductSerial string `json:"productSerial"`
}
// CreateWorkOrder 创建工单(状态恒 CREATED,后续用状态流转按钮推进)
@@ -47,38 +48,24 @@ func (s *Service) CreateWorkOrder(ctx context.Context, req WorkOrderReq, operato
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)
// 工位组合=本工单要经过的工位,按工位号升序(传送带不回走)。
// 完整的产线定义 = 关联工位(station.flow_id+ 工位号 1→12 顺序,不再有独立的"工艺路线"对象。
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).
SetProcessSeq(req.ProcessSeq).
SetContractNo(req.ContractNo).
SetProjectNo(req.ProjectNo).
SetProductSerial(req.ProductSerial).
SetStatus(status)
if req.PlanStart != "" {
if t, err := parseDate(req.PlanStart); err == nil {
@@ -116,7 +103,8 @@ func (s *Service) UpdateWorkOrder(ctx context.Context, req WorkOrderReq, operato
}
req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq)
}
u := s.ctx.EntClient.WorkOrder.UpdateOneID(req.Id).SetProcessSeq(req.ProcessSeq)
u := s.ctx.EntClient.WorkOrder.UpdateOneID(req.Id).SetProcessSeq(req.ProcessSeq).
SetContractNo(req.ContractNo).SetProjectNo(req.ProjectNo).SetProductSerial(req.ProductSerial)
if req.Quantity > 0 {
u.SetQuantity(req.Quantity)
}
+49
View File
@@ -4,10 +4,13 @@ import (
"context"
"errors"
"strconv"
"strings"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/associationtrace"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/station"
"bj_power_mes/ent/stepcriterion"
"bj_power_mes/ent/stepdata"
"bj_power_mes/ent/torquerecord"
@@ -57,6 +60,8 @@ type StepDataReq struct {
Name string `json:"name"`
Value float64 `json:"value"`
Text string `json:"text"`
// Checked:该步骤是否已完成「检测确认」。needCheck=true 的步骤必须为 true,服务端强校验,防止绕过前端。
Checked bool `json:"checked"`
}
type OnlineReq struct {
@@ -127,6 +132,11 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
} else if miss != "" {
return errors.New(miss)
}
// 阶段2:检测确认(needCheck)强校验——服务端为准,前端勾选只是辅助提示。
// 本工位绑定工艺流程中标记“需要检测确认”的步骤,报工数据里必须逐条带 checked=true,否则拒绝报工。
if err := s.validateNeedCheck(ctx, req); err != nil {
return err
}
now := time.Now()
proc, err := s.ctx.EntClient.WorkpieceProcess.Create().
SetSn(req.Sn).SetProcessCode(req.ProcessCode).
@@ -162,6 +172,45 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
return nil
}
// validateNeedCheck 检测确认(needCheck)服务端强校验。
// 规则:本工位绑定的工艺流程中,凡标记 needCheck=true 的工艺步骤,
// 报工请求里必须存在对应 stepId 且 checked=true 的步骤数据;缺失则拒绝报工。
// 工位未绑定流程、或流程内没有 needCheck 步骤时不拦截(与现场配置保持一致)。
func (s *Service) validateNeedCheck(ctx context.Context, req ReportProcessReq) error {
if req.StationNo <= 0 {
return nil
}
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx)
if err != nil || st.FlowId <= 0 {
return nil
}
steps, err := s.ctx.EntClient.ProcessStep.Query().
Where(processstep.FlowId(st.FlowId), processstep.NeedCheck(true)).All(ctx)
if err != nil || len(steps) == 0 {
return nil
}
checked := map[int]bool{}
for _, sd := range req.Steps {
if sd.Checked {
checked[sd.StepId] = true
}
}
missing := make([]string, 0, len(steps))
for _, ps := range steps {
if !checked[ps.ID] {
name := ps.Name
if name == "" {
name = "步骤" + itoa(ps.ID)
}
missing = append(missing, name)
}
}
if len(missing) > 0 {
return errors.New("以下步骤需先完成检测确认后方可报工:" + strings.Join(missing, "、"))
}
return nil
}
// writeStepData 写一条步骤考核数据,按标准自动判定是否合格
func (s *Service) writeStepData(ctx context.Context, sn string, processId int, st StepDataReq, operator string) (bool, error) {
ok := true