本次提交覆盖多个业务模块的功能完善与体验优化:
1. **鉴权与配置调整**:
- 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
- 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
- 工位配置放开1~12限制,改为仅校验大于0
2. **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3. **功能新增**:
- 新增工位类型、工艺路线与产线点位台账模块
- 添加工艺PDF预览面板、工位终端代理转发接口
- 新增操作日志按操作人列表筛选、工位登出日志记录
- 新增PLC移料指令与产线点位状态管理
4. **业务流程优化**:
- 调整BOM物料删除校验逻辑,优化工单备料计算
- 补充物料图号、检测单号等追溯字段
- 完善工艺流程图与工位绑定关系说明
- 优化前端页面文案与交互细节
5. **代码规范与维护**:
- 新增通用工具函数与前端静态资源
- 整理路由权限与中间件逻辑
- 修复部分接口与配置的不兼容问题
155 lines
5.8 KiB
Go
155 lines
5.8 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"bj_power_mes/ent"
|
||
"bj_power_mes/ent/materialrequest"
|
||
"bj_power_mes/ent/workorder"
|
||
)
|
||
|
||
// GenerateMaterialRequest 按日排产自动算料生成备料单
|
||
// 数量 = 该日所有工单计划数量 × 产品BOM单台用量×(1+损耗率),材料来源=该工单产品的 BOM
|
||
// 生成前校验:BOM 内所有物料编码须在 WMS 物料档案存在,有缺失返回 missing(不生成);
|
||
// WMS 不可达/异常时降级为"全部存在"并记 eventlog,避免 WMS 抖动阻塞产线。
|
||
// 返回值:count=本次生成条数;missing=WMS 中不存在的物料编码(非空时 count=0)。
|
||
func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string, operator string) (int, []string, error) {
|
||
plans, err := s.ListDailyPlans(ctx, "", planDate)
|
||
if err != nil {
|
||
return 0, nil, err
|
||
}
|
||
if len(plans) == 0 {
|
||
return 0, nil, errors.New("该日期没有排产计划")
|
||
}
|
||
type planBom struct {
|
||
plan *ent.DailyPlan
|
||
wo *ent.WorkOrder
|
||
bom []*ent.BomItem
|
||
}
|
||
groups := make([]planBom, 0, len(plans))
|
||
codeSet := map[string]bool{}
|
||
for _, plan := range plans {
|
||
if plan.Status == "CANCELLED" || plan.Status == "DONE" {
|
||
continue // 已取消/已完成的排产不再算料
|
||
}
|
||
// 解析工单 → 产品编码 → 该产品型号的物料清单(固定使用默认清单)
|
||
wo, err := s.ctx.EntClient.WorkOrder.Query().
|
||
Where(workorder.WorkOrderNo(plan.OrderNo)).Only(ctx)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
bom, err := s.ListBom(ctx, wo.ProductCode, bomNameOrDefault(""))
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if len(bom) == 0 {
|
||
continue
|
||
}
|
||
for _, item := range bom {
|
||
if item.MaterialCode != "" {
|
||
codeSet[item.MaterialCode] = true
|
||
}
|
||
}
|
||
groups = append(groups, planBom{plan: plan, wo: wo, bom: bom})
|
||
}
|
||
if len(groups) == 0 {
|
||
return 0, nil, errors.New("该日期排产均无有效 BOM 物料,无法生成备料单")
|
||
}
|
||
// 阶段1:WMS 物料存在性校验(全部存在才生成)
|
||
codes := make([]string, 0, len(codeSet))
|
||
for c := range codeSet {
|
||
codes = append(codes, c)
|
||
}
|
||
missing, wmsErr := s.ctx.Wms.MaterialExists(ctx, codes)
|
||
if wmsErr != nil {
|
||
// 降级:WMS 不可达按"全部存在"放行,记日志供事后核查
|
||
s.ctx.EventLog.Write(ctx, "material.request.wms_skip", "", operator, "material_request", planDate,
|
||
"WMS 物料校验不可达,已降级放行", map[string]any{"planDate": planDate, "error": wmsErr.Error()})
|
||
} else if len(missing) > 0 {
|
||
return 0, missing, nil
|
||
}
|
||
// 阶段2:全部存在 → 算料生成
|
||
created := 0
|
||
// 备料单号基准:一次生成调用只取一次时间戳,序号全局递增,避免同一秒内多物料/多工单重复
|
||
tsBase := time.Now().Format("20060102150405")
|
||
seq := 1
|
||
for _, g := range groups {
|
||
for _, item := range g.bom {
|
||
reqQty := float64(g.plan.PlanQty) * item.UnitQty * (1 + item.LossRate/100)
|
||
if reqQty <= 0 {
|
||
continue
|
||
}
|
||
reqNo := fmt.Sprintf("MR%s%03d", tsBase, seq)
|
||
seq++
|
||
// 送达接驳台:取该排产确认的 dock_codes(可多选,逗号分隔),未确认则为空串待仓管下发时指定
|
||
target := strings.Join(g.plan.DockCodes, ",")
|
||
_ = s.ctx.EntClient.MaterialRequest.Create().
|
||
SetRequestNo(reqNo).SetOrderNo(g.plan.OrderNo).SetPlanDate(g.plan.PlanDate).
|
||
SetMaterialCode(item.MaterialCode).SetMaterialName(item.MaterialName).
|
||
SetUnit(item.Unit).SetManageMode(item.ManageMode).SetReqQty(reqQty).
|
||
SetStatus("PENDING").SetOperator(operator).
|
||
SetTargetDock(target).Exec(ctx)
|
||
created++
|
||
}
|
||
}
|
||
s.ctx.EventLog.Write(ctx, "material.request.generate", "", operator, "material_request", planDate, "按日排产自动生成备料单", map[string]any{"planDate": planDate, "count": created})
|
||
s.notifyDashboard()
|
||
return created, nil, nil
|
||
}
|
||
|
||
// ListMaterialRequests 查询备料单
|
||
func (s *Service) ListMaterialRequests(ctx context.Context, orderNo, planDate, status string) ([]*ent.MaterialRequest, error) {
|
||
return s.listMaterialRequests(ctx, orderNo, planDate, status, "", "", "")
|
||
}
|
||
|
||
// ListMaterialRequestsFiltered 管理页丰富筛选入口
|
||
func (s *Service) ListMaterialRequestsFiltered(ctx context.Context, orderNo, planDate, status, materialCode, materialName, targetDock string) ([]*ent.MaterialRequest, error) {
|
||
return s.listMaterialRequests(ctx, orderNo, planDate, status, materialCode, materialName, targetDock)
|
||
}
|
||
|
||
// listMaterialRequests 备料单查询(2026-09-08 丰富筛选+大小写不敏感)
|
||
// materialCode/materialName/targetDock 可空;keyword 兜底匹配 备料单号/工单号/物料编码
|
||
func (s *Service) listMaterialRequests(ctx context.Context, orderNo, planDate, status, materialCode, materialName, targetDock string) ([]*ent.MaterialRequest, error) {
|
||
q := s.ctx.EntClient.MaterialRequest.Query()
|
||
if orderNo != "" {
|
||
q = q.Where(materialrequest.OrderNoEqualFold(orderNo))
|
||
}
|
||
if planDate != "" {
|
||
q = q.Where(materialrequest.PlanDate(planDate))
|
||
}
|
||
if status != "" {
|
||
q = q.Where(materialrequest.Status(status))
|
||
}
|
||
if materialCode != "" {
|
||
q = q.Where(materialrequest.MaterialCodeContainsFold(materialCode))
|
||
}
|
||
if materialName != "" {
|
||
q = q.Where(materialrequest.MaterialNameContainsFold(materialName))
|
||
}
|
||
if targetDock != "" {
|
||
q = q.Where(materialrequest.TargetDockEqualFold(targetDock))
|
||
}
|
||
return q.Order(ent.Desc(materialrequest.FieldCreatedAt), ent.Desc(materialrequest.FieldID)).All(ctx)
|
||
}
|
||
|
||
|
||
// SetMaterialRequestStatus 推进备料单状态(WMS 下发 AGV DELIVERING / 到位 DONE)
|
||
func (s *Service) SetMaterialRequestStatus(ctx context.Context, requestNo, status string) error {
|
||
n, err := s.ctx.EntClient.MaterialRequest.Update().
|
||
Where(materialrequest.RequestNo(requestNo)).
|
||
SetStatus(status).
|
||
Save(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if n == 0 {
|
||
return errors.New("备料单不存在")
|
||
}
|
||
s.notifyDashboard()
|
||
return nil
|
||
}
|