feat: 完成多模块迭代更新
- 新增过程巡检按步骤上报、数量不符上报、工位退料功能 - 增加工单工程编号三级归属字段 - 新增物料安全库存与缺货预警 - 新增附件管理、退库单管理模块 - 优化生产看板展示逻辑与前端页面文案 - 清理冗余的工艺路线相关代码与备份文件
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user