feat&refactor: 完成多模块功能迭代与配置优化

本次提交覆盖多个业务模块的功能完善与体验优化:
1.  **鉴权与配置调整**:
    - 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
    - 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
    - 工位配置放开1~12限制,改为仅校验大于0
2.  **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3.  **功能新增**:
    - 新增工位类型、工艺路线与产线点位台账模块
    - 添加工艺PDF预览面板、工位终端代理转发接口
    - 新增操作日志按操作人列表筛选、工位登出日志记录
    - 新增PLC移料指令与产线点位状态管理
4.  **业务流程优化**:
    - 调整BOM物料删除校验逻辑,优化工单备料计算
    - 补充物料图号、检测单号等追溯字段
    - 完善工艺流程图与工位绑定关系说明
    - 优化前端页面文案与交互细节
5.  **代码规范与维护**:
    - 新增通用工具函数与前端静态资源
    - 整理路由权限与中间件逻辑
    - 修复部分接口与配置的不兼容问题
This commit is contained in:
SunYF
2026-09-10 16:59:15 +08:00
parent 6d8bb75696
commit 0ab21b1234
202 changed files with 21397 additions and 3172 deletions
+271
View File
@@ -0,0 +1,271 @@
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
}