chore: 批量完成项目多模块迭代优化
1. 废弃半成品库存表并统一库存主表 2. 修复列表排序与搜索大小写不敏感问题 3. 新增用户最近登录时间、工单BOM名称字段 4. 完善角色管理、工位选择器功能 5. 增加拧紧数据补录、成品自动回流WMS能力 6. 统一导出时间范围校验规则 7. 丰富物料/备料单筛选条件 8. 调整菜单与权限命名适配产品型号业务
This commit is contained in:
@@ -89,6 +89,12 @@ func (s *Service) Login(ctx context.Context, req LoginReq) (LoginResp, error) {
|
||||
if bcrypt.CompareHashAndPassword([]byte(usr.Password), []byte(req.Password)) != nil {
|
||||
return resp, errors.New("用户名或密码错误")
|
||||
}
|
||||
// 记录最近登录时间(仅后台登录;工位终端 StationLogin 不计入)
|
||||
now := time.Now().Unix()
|
||||
if err := s.ctx.EntClient.User.UpdateOneID(usr.ID).SetLastLoginAt(now).Exec(ctx); err != nil {
|
||||
// 不阻塞登录,仅记日志
|
||||
}
|
||||
usr.LastLoginAt = &now
|
||||
roleEntity, _ := s.ctx.EntClient.Role.Get(ctx, usr.RoleId)
|
||||
return s.issueTokens(usr, roleEntity)
|
||||
}
|
||||
@@ -445,6 +451,7 @@ type UserVO struct {
|
||||
Status string `json:"status"`
|
||||
CanLoginWorkstation bool `json:"canLoginWorkstation"`
|
||||
Stations []int `json:"stations"` // 允许登录/操作的工位号(空=全部工位)
|
||||
LastLoginAt *int64 `json:"lastLoginAt"` // 最近登录时间(unix秒),null=从未登录
|
||||
}
|
||||
|
||||
func (s *Service) ListUsers(ctx context.Context) ([]UserVO, error) {
|
||||
@@ -462,6 +469,7 @@ func (s *Service) ListUsers(ctx context.Context) ([]UserVO, error) {
|
||||
Status: u.Status,
|
||||
CanLoginWorkstation: u.CanLoginWorkstation,
|
||||
Stations: s.UserStations(ctx, u.ID),
|
||||
LastLoginAt: u.LastLoginAt,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -2,14 +2,20 @@ package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/bomitem"
|
||||
"bj_power_mes/ent/workorder"
|
||||
)
|
||||
|
||||
// 默认BOMName 同一型号多份 BOM 并存时的缺省名称(历史数据/未指定时统一回填此值)
|
||||
const DefaultBOMName = "默认"
|
||||
|
||||
type BomItemReq struct {
|
||||
ProductCode string `json:"productCode"`
|
||||
BomName string `json:"bomName"`
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
Spec string `json:"spec"`
|
||||
@@ -22,14 +28,25 @@ type BomItemReq struct {
|
||||
SerialSns []string `json:"serialSns"`
|
||||
}
|
||||
|
||||
// SaveBom 保存产品 BOM(覆盖式:按 产品编码+物料编码 做 upsert)
|
||||
func (s *Service) SaveBom(ctx context.Context, productCode string, items []BomItemReq, operator string) error {
|
||||
// bomNameOrDefault 空 BOM 名回退为「默认」(兼容旧前端/旧数据)
|
||||
func bomNameOrDefault(name string) string {
|
||||
if name == "" {
|
||||
return DefaultBOMName
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// SaveBom 保存产品 BOM(覆盖式 upsert:唯一键 = 产品编码+BOM名称+物料编码)。
|
||||
// 同一产品型号可并存多份 BOM(不同厂家供货/含半成品等不同料单结构),按 bomName 区分,
|
||||
// 工单建单时选定一份;备料/装机绑定均按工单锁定的 bomName 计算。
|
||||
func (s *Service) SaveBom(ctx context.Context, productCode, bomName string, items []BomItemReq, operator string) error {
|
||||
bomName = bomNameOrDefault(bomName)
|
||||
for _, it := range items {
|
||||
if it.ManageMode == "" {
|
||||
it.ManageMode = "2"
|
||||
}
|
||||
exist, err := s.ctx.EntClient.BomItem.Query().
|
||||
Where(bomitem.ProductCode(productCode), bomitem.MaterialCode(it.MaterialCode)).First(ctx)
|
||||
Where(bomitem.ProductCode(productCode), bomitem.BomName(bomName), bomitem.MaterialCode(it.MaterialCode)).First(ctx)
|
||||
if err == nil && exist != nil {
|
||||
u := s.ctx.EntClient.BomItem.UpdateOneID(exist.ID).
|
||||
SetMaterialName(it.MaterialName).SetSpec(it.Spec).SetUnit(it.Unit).
|
||||
@@ -49,7 +66,7 @@ func (s *Service) SaveBom(ctx context.Context, productCode string, items []BomIt
|
||||
pc = *it.ProcessCode
|
||||
}
|
||||
b := s.ctx.EntClient.BomItem.Create().
|
||||
SetProductCode(productCode).SetMaterialCode(it.MaterialCode).
|
||||
SetProductCode(productCode).SetBomName(bomName).SetMaterialCode(it.MaterialCode).
|
||||
SetMaterialName(it.MaterialName).SetSpec(it.Spec).SetUnit(it.Unit).
|
||||
SetManageMode(it.ManageMode).SetProcessCode(pc).
|
||||
SetUnitQty(it.UnitQty).SetLossRate(it.LossRate)
|
||||
@@ -61,14 +78,70 @@ func (s *Service) SaveBom(ctx context.Context, productCode string, items []BomIt
|
||||
}
|
||||
}
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "bom.save", "", operator, "product_bom", productCode, "保存产品BOM", map[string]any{"count": len(items)})
|
||||
s.ctx.EventLog.Write(ctx, "bom.save", "", operator, "product_bom", productCode, "保存产品BOM", map[string]any{"bomName": bomName, "count": len(items)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListBom(ctx context.Context, productCode string) ([]*ent.BomItem, error) {
|
||||
// DeleteBomItem 移除 BOM 中的一条物料(按行主键)。
|
||||
// 安全校验:该 (产品编码+BOM名称) 下任一工单已生成备料单时禁止移除,避免已锁定料单被改动。
|
||||
func (s *Service) DeleteBomItem(ctx context.Context, id int, operator string) error {
|
||||
item, err := s.ctx.EntClient.BomItem.Get(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("BOM 物料不存在")
|
||||
}
|
||||
used, err := s.ctx.EntClient.WorkOrder.Query().
|
||||
Where(workorder.ProductCode(item.ProductCode), workorder.BomName(item.BomName)).Exist(ctx)
|
||||
if err == nil && used {
|
||||
// 该型号+BOM 已被工单选用过(有工单绑定),谨慎起见禁止直接移除
|
||||
return fmt.Errorf("该 BOM 已被「%s」型号的工单选用,移除物料会影响备料/追溯,请先评估相关工单", item.ProductCode)
|
||||
}
|
||||
if err := s.ctx.EntClient.BomItem.DeleteOneID(id).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "bom.item.delete", "", operator, "product_bom", item.ProductCode,
|
||||
"移除BOM物料 "+item.MaterialCode, map[string]any{"bomName": item.BomName})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListBom 查询 BOM(可按产品编码 + BOM 名称过滤;bomName 传 "ALL" 或空且指定了产品时返回全部)
|
||||
func (s *Service) ListBom(ctx context.Context, productCode, bomName string) ([]*ent.BomItem, error) {
|
||||
q := s.ctx.EntClient.BomItem.Query()
|
||||
if productCode != "" {
|
||||
q = q.Where(bomitem.ProductCode(productCode))
|
||||
}
|
||||
return q.Order(ent.Asc(bomitem.FieldID)).All(ctx)
|
||||
}
|
||||
if bomName != "" && bomName != "ALL" {
|
||||
q = q.Where(bomitem.BomName(bomName))
|
||||
}
|
||||
return q.Order(ent.Asc(bomitem.FieldProductCode), ent.Asc(bomitem.FieldBomName), ent.Asc(bomitem.FieldID)).All(ctx)
|
||||
}
|
||||
|
||||
// BomNameInfo 型号下的一份 BOM 概要(名称 + 物料数)
|
||||
type BomNameInfo struct {
|
||||
BomName string `json:"bomName"`
|
||||
ItemCount int `json:"itemCount"`
|
||||
}
|
||||
|
||||
// ListBomNames 列出某产品型号下并存的所有 BOM 名称(工单建单选 BOM 下拉数据源)
|
||||
func (s *Service) ListBomNames(ctx context.Context, productCode string) ([]*BomNameInfo, error) {
|
||||
if productCode == "" {
|
||||
return []*BomNameInfo{}, nil
|
||||
}
|
||||
items, err := s.ctx.EntClient.BomItem.Query().
|
||||
Where(bomitem.ProductCode(productCode)).All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
order := []string{}
|
||||
count := map[string]int{}
|
||||
for _, it := range items {
|
||||
if _, ok := count[it.BomName]; !ok {
|
||||
order = append(order, it.BomName)
|
||||
}
|
||||
count[it.BomName]++
|
||||
}
|
||||
out := make([]*BomNameInfo, 0, len(order))
|
||||
for _, name := range order {
|
||||
out = append(out, &BomNameInfo{BomName: name, ItemCount: count[name]})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -4,16 +4,31 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/inspectionrecord"
|
||||
)
|
||||
|
||||
// FlexStr 兼容客户端把数字字段传成 number 或 string(前端 stationNo 用数字,
|
||||
// 标准 JSON 反序列化进 string 会报错 → 400 请求体解析失败)
|
||||
type FlexStr string
|
||||
|
||||
func (f *FlexStr) UnmarshalJSON(b []byte) error {
|
||||
s := strings.TrimSpace(string(b))
|
||||
s = strings.Trim(s, `"`)
|
||||
if s == "null" {
|
||||
s = ""
|
||||
}
|
||||
*f = FlexStr(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InspectionReq 巡检记录请求(PAD 巡检终端,块8)
|
||||
type InspectionReq struct {
|
||||
Category string `json:"category"` // CHECKIN/POINT/PROCESS/DONE/ALARM
|
||||
StationNo string `json:"stationNo"`
|
||||
StationNo FlexStr `json:"stationNo"`
|
||||
Shift string `json:"shift"` // 早/中/晚
|
||||
OrderNo string `json:"orderNo"` // 工单号
|
||||
Sn string `json:"sn"` // 工件SN
|
||||
@@ -42,7 +57,7 @@ func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, opera
|
||||
}
|
||||
_, err := s.ctx.EntClient.InspectionRecord.Create().
|
||||
SetCategory(category).
|
||||
SetStationNo(req.StationNo).
|
||||
SetStationNo(string(req.StationNo)).
|
||||
SetShift(req.Shift).
|
||||
SetOrderNo(req.OrderNo).
|
||||
SetSn(req.Sn).
|
||||
@@ -56,7 +71,7 @@ func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, opera
|
||||
return err
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "inspection."+category, req.OrderNo, req.Operator, "inspection_record",
|
||||
req.Sn, "巡检终端提交", map[string]any{"category": category, "stationNo": req.StationNo, "result": result})
|
||||
req.Sn, "巡检终端提交", map[string]any{"category": category, "stationNo": string(req.StationNo), "result": result})
|
||||
|
||||
if category == "ALARM" {
|
||||
// 触发看板报警:SSE 推送 + 使 Redis 看板报警缓存失效
|
||||
@@ -64,7 +79,7 @@ func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, opera
|
||||
"level": "CRITICAL",
|
||||
"type": "inspection_alarm",
|
||||
"message": "巡检异常上报:" + req.Remark,
|
||||
"station": req.StationNo,
|
||||
"station": string(req.StationNo),
|
||||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
if s.ctx.SSE != nil {
|
||||
@@ -78,12 +93,25 @@ func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, opera
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListInspections 查询巡检记录
|
||||
// ListInspections 查询巡检记录(不分页,供内部/导出用,封顶 1000)
|
||||
func (s *Service) ListInspections(ctx context.Context, category, operator, from, to string) ([]*ent.InspectionRecord, error) {
|
||||
q := s.ctx.EntClient.InspectionRecord.Query().Order(ent.Desc(inspectionrecord.FieldID))
|
||||
rows, _, err := s.ListInspectionsPaged(ctx, category, operator, from, to, "", "", 1, 1000)
|
||||
return rows, err
|
||||
}
|
||||
|
||||
// ListInspectionsPaged 分页查询巡检记录,返回 {rows, total};排序 id desc 最新置顶
|
||||
// orderNo/stationNo:2026-09-08 补充筛选(大小写不敏感)
|
||||
func (s *Service) ListInspectionsPaged(ctx context.Context, category, operator, from, to, orderNo, stationNo string, page, pageSize int) ([]*ent.InspectionRecord, int, error) {
|
||||
q := s.ctx.EntClient.InspectionRecord.Query()
|
||||
if category != "" {
|
||||
q = q.Where(inspectionrecord.Category(category))
|
||||
}
|
||||
if orderNo != "" {
|
||||
q = q.Where(inspectionrecord.OrderNoEqualFold(orderNo))
|
||||
}
|
||||
if stationNo != "" {
|
||||
q = q.Where(inspectionrecord.StationNoEqualFold(stationNo))
|
||||
}
|
||||
if operator != "" {
|
||||
q = q.Where(inspectionrecord.Operator(operator))
|
||||
}
|
||||
@@ -97,7 +125,13 @@ func (s *Service) ListInspections(ctx context.Context, category, operator, from,
|
||||
q = q.Where(inspectionrecord.CreatedAtLT(t.Add(24 * time.Hour)))
|
||||
}
|
||||
}
|
||||
return q.Limit(1000).All(ctx)
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows, err := q.Order(ent.Desc(inspectionrecord.FieldID)).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
func oneOf(v string, items ...string) bool {
|
||||
|
||||
@@ -36,13 +36,13 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
|
||||
if plan.Status == "CANCELLED" || plan.Status == "DONE" {
|
||||
continue // 已取消/已完成的排产不再算料
|
||||
}
|
||||
// 解析工单 → 产品编码 → 该产品 BOM
|
||||
// 解析工单 → 产品编码 → 该工单锁定的 BOM(建单时选定的 bomName)
|
||||
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)
|
||||
bom, err := s.ListBom(ctx, wo.ProductCode, bomNameOrDefault(wo.BomName))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -103,9 +103,20 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
|
||||
|
||||
// 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.OrderNo(orderNo))
|
||||
q = q.Where(materialrequest.OrderNoEqualFold(orderNo))
|
||||
}
|
||||
if planDate != "" {
|
||||
q = q.Where(materialrequest.PlanDate(planDate))
|
||||
@@ -113,9 +124,19 @@ func (s *Service) ListMaterialRequests(ctx context.Context, orderNo, planDate, s
|
||||
if status != "" {
|
||||
q = q.Where(materialrequest.Status(status))
|
||||
}
|
||||
return q.Order(materialrequest.ByID()).All(ctx)
|
||||
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().
|
||||
|
||||
@@ -46,7 +46,8 @@ type FlowVO struct {
|
||||
}
|
||||
|
||||
// ListFlows 查询工艺流程(可按工位号过滤:命中绑定该工位或主工序号的流程)
|
||||
func (s *Service) ListFlows(ctx context.Context, stationNo int) ([]*FlowVO, error) {
|
||||
// page>0 时真分页返回 map{total,list,page,pageSize}(管理页用);page=0 全量数组(工位终端兼容)
|
||||
func (s *Service) ListFlows(ctx context.Context, stationNo, page, pageSize int) (any, error) {
|
||||
q := s.ctx.EntClient.ProcessFlow.Query().
|
||||
Order(ent.Asc(processflow.FieldProcessCode), ent.Asc(processflow.FieldID))
|
||||
flows, err := q.All(ctx)
|
||||
@@ -85,6 +86,18 @@ func (s *Service) ListFlows(ctx context.Context, stationNo int) ([]*FlowVO, erro
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -95,8 +108,10 @@ func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) er
|
||||
if req.Name == "" {
|
||||
return errors.New("流程名称必填")
|
||||
}
|
||||
if req.StationNo < 1 || req.StationNo > 12 {
|
||||
return errors.New("请选择主工序编号(1~12)")
|
||||
// 工序编号与物理工位解耦:工序是流程步骤编号(1~999,流程内唯一即可),
|
||||
// 工位对应关系由「关联工位」页维护(2026-09-08)
|
||||
if req.StationNo < 1 || req.StationNo > 999 {
|
||||
return errors.New("主工序编号应为 1~999 的整数")
|
||||
}
|
||||
// 解析绑定工位列表:显式 stations 优先,否则回退到 StationNo(兼容旧调用)
|
||||
bindStations := req.Stations
|
||||
@@ -122,21 +137,8 @@ func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) er
|
||||
if status != "ACTIVE" && status != "INACTIVE" {
|
||||
return errors.New("非法状态")
|
||||
}
|
||||
// 校验:绑定工位不得已被其他"启用"流程占用(一个工位只能绑一个启用流程)
|
||||
bound, err := s.ctx.EntClient.Station.Query().
|
||||
Where(station.StationNoIn(stations...), station.FlowIdGT(0)).All(ctx)
|
||||
if err == nil {
|
||||
flowID := req.Id
|
||||
for _, st := range bound {
|
||||
if flowID > 0 && st.FlowId == flowID {
|
||||
continue // 绑定的是本流程自身,允许
|
||||
}
|
||||
other, gErr := s.ctx.EntClient.ProcessFlow.Get(ctx, st.FlowId)
|
||||
if gErr == nil && other.Status == "ACTIVE" {
|
||||
return fmt.Errorf("工位 %d 已被启用流程「%s」绑定,请先在流程/工位中解除绑定", st.StationNo, other.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 绑定语义(2026-09-08 定稿):直接更换——一个工位只有一条关联数据,
|
||||
// 保存时所选工位一律改绑到本流程(覆盖旧绑定),不再报"已被启用流程绑定"冲突。
|
||||
var flowID int
|
||||
if req.Id > 0 {
|
||||
upd := s.ctx.EntClient.ProcessFlow.UpdateOneID(req.Id).
|
||||
@@ -395,7 +397,8 @@ type WorkloadRow struct {
|
||||
NgCount int `json:"ngCount"`
|
||||
}
|
||||
|
||||
func (s *Service) Workload(ctx context.Context, operator, stationNo, from, to string) (map[string]any, error) {
|
||||
func (s *Service) Workload(ctx context.Context, operator, stationNo, from, to string,
|
||||
opPage, opSize, stPage, stSize, detPage, detSize int) (map[string]any, error) {
|
||||
q := s.ctx.EntClient.WorkpieceProcess.Query()
|
||||
if operator != "" {
|
||||
q = q.Where(workpieceprocess.Operator(operator))
|
||||
@@ -449,5 +452,67 @@ func (s *Service) Workload(ctx context.Context, operator, stationNo, from, to st
|
||||
}
|
||||
return list[i].ProcessCode < list[j].ProcessCode
|
||||
})
|
||||
return map[string]any{"rows": list, "detail": rows}, nil
|
||||
|
||||
// 三个视图分别聚合(2026-09-08 绩效报表改造:按人/按工位/明细,各自真分页)
|
||||
byOpAgg := map[string]*WorkloadRow{}
|
||||
byStAgg := map[string]*WorkloadRow{}
|
||||
for _, v := range list {
|
||||
opKey := v.Operator
|
||||
if o, ok := byOpAgg[opKey]; ok {
|
||||
o.DoneCount += v.DoneCount; o.OkCount += v.OkCount; o.NgCount += v.NgCount
|
||||
} else {
|
||||
byOpAgg[opKey] = &WorkloadRow{Operator: v.Operator, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount}
|
||||
}
|
||||
stKey := v.StationNo
|
||||
if o, ok := byStAgg[stKey]; ok {
|
||||
o.DoneCount += v.DoneCount; o.OkCount += v.OkCount; o.NgCount += v.NgCount
|
||||
} else {
|
||||
byStAgg[stKey] = &WorkloadRow{StationNo: v.StationNo, DoneCount: v.DoneCount, OkCount: v.OkCount, NgCount: v.NgCount}
|
||||
}
|
||||
}
|
||||
paginateRows := func(all []*ent.WorkpieceProcess, page, size int) (int, []*ent.WorkpieceProcess) {
|
||||
total := len(all)
|
||||
start := (page - 1) * size
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + size
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
return total, all[start:end]
|
||||
}
|
||||
paginate := func(all []*WorkloadRow, page, size int) (int, []*WorkloadRow) {
|
||||
total := len(all)
|
||||
start := (page - 1) * size
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + size
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
return total, all[start:end]
|
||||
}
|
||||
|
||||
byOpList := make([]*WorkloadRow, 0, len(byOpAgg))
|
||||
for _, v := range byOpAgg {
|
||||
byOpList = append(byOpList, v)
|
||||
}
|
||||
sort.Slice(byOpList, func(i, j int) bool { return byOpList[i].Operator < byOpList[j].Operator })
|
||||
byStList := make([]*WorkloadRow, 0, len(byStAgg))
|
||||
for _, v := range byStAgg {
|
||||
byStList = append(byStList, v)
|
||||
}
|
||||
sort.Slice(byStList, func(i, j int) bool { return byStList[i].StationNo < byStList[j].StationNo })
|
||||
|
||||
opTotal, byOpPage := paginate(byOpList, opPage, opSize)
|
||||
stTotal, byStPage := paginate(byStList, stPage, stSize)
|
||||
detTotal, detRowsPage := paginateRows(rows, detPage, detSize)
|
||||
|
||||
return map[string]any{
|
||||
"byOperator": map[string]any{"total": opTotal, "list": byOpPage},
|
||||
"byStation": map[string]any{"total": stTotal, "list": byStPage},
|
||||
"detail": map[string]any{"total": detTotal, "list": detRowsPage},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -3,11 +3,20 @@ package logic
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/bomitem"
|
||||
"bj_power_mes/ent/producttype"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/internal/wmsclient"
|
||||
)
|
||||
|
||||
// 产品型号(成品档案):编码主数据单一来源 = WMS(materials item_type=3)。
|
||||
// MES 本页改为实时代理 WMS;WMS 不可达时降级读本地 product_types 只读缓存(仅查询,
|
||||
// 写操作必须 WMS 可用),前端以 wmsOnline 标识提示。
|
||||
|
||||
type ProductTypeReq struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -22,25 +31,115 @@ func (s *Service) CreateProductType(ctx context.Context, req ProductTypeReq) err
|
||||
if !req.IsActive {
|
||||
active = true
|
||||
}
|
||||
return s.ctx.EntClient.ProductType.Create().
|
||||
SetName(req.Name).SetCode(req.Code).SetCategory(req.Category).
|
||||
SetRemark(req.Remark).SetIsActive(active).Exec(ctx)
|
||||
if err := s.ctx.Wms.CreateProductType(ctx, req.Code, req.Name, req.Category, req.Remark, active); err != nil {
|
||||
return errors.New("WMS 不可达或拒绝写入(编码主数据以 WMS 为单一来源):" + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateProductType(ctx context.Context, req ProductTypeReq) error {
|
||||
if req.Id == 0 {
|
||||
return errors.New("缺少 id")
|
||||
}
|
||||
return s.ctx.EntClient.ProductType.UpdateOneID(req.Id).
|
||||
SetName(req.Name).SetCode(req.Code).SetCategory(req.Category).
|
||||
SetRemark(req.Remark).SetIsActive(req.IsActive).Exec(ctx)
|
||||
if err := s.ctx.Wms.UpdateProductType(ctx, req.Id, req.Name, req.Category, req.Remark, req.IsActive); err != nil {
|
||||
return errors.New("WMS 不可达或拒绝写入:" + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteProductType 删除保护:被工单/BOM 引用过的型号禁删(MES 侧引用校验,
|
||||
// WMS 侧库存引用由 WMS 内部接口再校验一层)
|
||||
func (s *Service) DeleteProductType(ctx context.Context, id int) error {
|
||||
return s.ctx.EntClient.ProductType.DeleteOneID(id).Exec(ctx)
|
||||
row, err := s.productTypeByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
woCnt, _ := s.ctx.EntClient.WorkOrder.Query().Where(workorder.ProductCode(row.Code)).Count(ctx)
|
||||
if woCnt > 0 {
|
||||
return fmt.Errorf("该产品型号已被 %d 张工单引用,不可删除;建议停用(下架)", woCnt)
|
||||
}
|
||||
bomCnt, _ := s.ctx.EntClient.BomItem.Query().Where(bomitem.ProductCode(row.Code)).Count(ctx)
|
||||
if bomCnt > 0 {
|
||||
return fmt.Errorf("该产品型号在物料清单中有 %d 条配置,不可删除;建议停用(下架)", bomCnt)
|
||||
}
|
||||
if err := s.ctx.Wms.DeleteProductType(ctx, id); err != nil {
|
||||
return errors.New("WMS 不可达或拒绝删除:" + err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ListProductTypes(ctx context.Context) ([]*ent.ProductType, error) {
|
||||
return s.ctx.EntClient.ProductType.Query().
|
||||
Order(ent.Desc(producttype.FieldID)).All(ctx)
|
||||
}
|
||||
// ListProductTypes 全量列表(下拉/兼容形态,id asc 新建在底部)。
|
||||
// degraded=false 表示 WMS 不可达、数据来自本地只读缓存。
|
||||
func (s *Service) ListProductTypes(ctx context.Context) ([]wmsclient.ProductTypeRow, bool, error) {
|
||||
list, err := s.ctx.Wms.ProductTypes(ctx, "", "")
|
||||
if err == nil {
|
||||
return list, true, nil
|
||||
}
|
||||
fallback, ferr := s.localProductTypes(ctx, "")
|
||||
if ferr != nil {
|
||||
return nil, false, errors.New("WMS 不可达且本地缓存读取失败")
|
||||
}
|
||||
return fallback, false, nil
|
||||
}
|
||||
|
||||
// ListProductTypesPage 分页+搜索(管理页真分页)
|
||||
func (s *Service) ListProductTypesPage(ctx context.Context, keyword, isActive string, page, pageSize int) (int64, []wmsclient.ProductTypeRow, bool, error) {
|
||||
total, list, err := s.ctx.Wms.ProductTypePage(ctx, keyword, isActive, page, pageSize)
|
||||
if err == nil {
|
||||
return total, list, true, nil
|
||||
}
|
||||
fallback, ferr := s.localProductTypes(ctx, keyword)
|
||||
if ferr != nil {
|
||||
return 0, nil, false, errors.New("WMS 不可达且本地缓存读取失败")
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start > len(fallback) {
|
||||
start = len(fallback)
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(fallback) {
|
||||
end = len(fallback)
|
||||
}
|
||||
return int64(len(fallback)), fallback[start:end], false, nil
|
||||
}
|
||||
|
||||
func (s *Service) productTypeByID(ctx context.Context, id int) (*wmsclient.ProductTypeRow, error) {
|
||||
list, err := s.ctx.Wms.ProductTypes(ctx, "", "")
|
||||
if err == nil {
|
||||
for i := range list {
|
||||
if list[i].ID == id {
|
||||
return &list[i], nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("产品型号不存在")
|
||||
}
|
||||
// 降级:本地缓存
|
||||
pt, err := s.ctx.EntClient.ProductType.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errors.New("产品型号不存在(WMS 不可达,无法校验引用)")
|
||||
}
|
||||
return &wmsclient.ProductTypeRow{ID: pt.ID, Code: pt.Code, Name: pt.Name}, nil
|
||||
}
|
||||
|
||||
func (s *Service) localProductTypes(ctx context.Context, keyword string) ([]wmsclient.ProductTypeRow, error) {
|
||||
list, err := s.ctx.EntClient.ProductType.Query().Order(ent.Asc(producttype.FieldID)).All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows := make([]wmsclient.ProductTypeRow, 0, len(list))
|
||||
kw := strings.ToLower(strings.TrimSpace(keyword))
|
||||
for _, p := range list {
|
||||
if kw != "" &&
|
||||
!strings.Contains(strings.ToLower(p.Code), kw) &&
|
||||
!strings.Contains(strings.ToLower(p.Name), kw) &&
|
||||
!strings.Contains(strings.ToLower(p.Category), kw) {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, wmsclient.ProductTypeRow{
|
||||
ID: p.ID, Code: p.Code, Name: p.Name, Category: p.Category,
|
||||
Remark: p.Remark, IsActive: p.IsActive,
|
||||
CreatedAt: p.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
@@ -92,13 +92,22 @@ func (s *Service) bumpDailyPlanCompleted(ctx context.Context, orderNo string, t
|
||||
}
|
||||
|
||||
// ListScanRecords 查询扫码报工记录
|
||||
func (s *Service) ListScanRecords(ctx context.Context, sn, orderNo string) ([]*ent.ScanRecord, error) {
|
||||
func (s *Service) ListScanRecords(ctx context.Context, sn, orderNo string, page, pageSize int) ([]*ent.ScanRecord, int, error) {
|
||||
q := s.ctx.EntClient.ScanRecord.Query()
|
||||
if sn != "" {
|
||||
q = q.Where(scanrecord.Sn(sn))
|
||||
}
|
||||
if orderNo != "" {
|
||||
q = q.Where(scanrecord.OrderNo(orderNo))
|
||||
q = q.Where(scanrecord.OrderNoEqualFold(orderNo))
|
||||
}
|
||||
return q.Order(ent.Desc(scanrecord.FieldID)).Limit(1000).All(ctx)
|
||||
if sn != "" {
|
||||
q = q.Where(scanrecord.SnEqualFold(sn))
|
||||
}
|
||||
total, err := q.Count(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows, err := q.Order(ent.Desc(scanrecord.FieldID)).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx)
|
||||
return rows, total, err
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.performance", "绩效报表", "MENU", "/performance"},
|
||||
{"produce.scan", "手动报工", "MENU", "/scan"},
|
||||
{"produce.trace", "工件追溯", "MENU", "/trace"},
|
||||
{"produce.product", "产品类型", "MENU", "/product-type"},
|
||||
{"produce.product", "产品型号", "MENU", "/product-type"},
|
||||
{"sys.inspect", "巡检终端", "MENU", "/inspect"},
|
||||
{"sys.eventlog", "操作日志", "MENU", "/event-log"},
|
||||
// sys.rbac(角色权限管理) 已拆分:账号管理 + 角色管理(对齐 WMS 系统管理菜单)
|
||||
@@ -106,9 +106,9 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.bom:edit", "物料清单-维护", "BUTTON", "", "produce.bom"},
|
||||
{"produce.material:generate", "备料单-生成", "BUTTON", "", "produce.material"},
|
||||
{"produce.plc:send", "工位组合-下发", "BUTTON", "", "produce.plc"},
|
||||
{"produce.product:add", "产品类型-新增", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:edit", "产品类型-编辑", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:delete", "产品类型-删除", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:add", "产品型号-新增", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:edit", "产品型号-编辑", "BUTTON", "", "produce.product"},
|
||||
{"produce.product:delete", "产品型号-删除", "BUTTON", "", "produce.product"},
|
||||
{"produce.processflow:add", "工艺流程-新增", "BUTTON", "", "produce.processflow"},
|
||||
{"produce.processflow:edit", "工艺流程-编辑", "BUTTON", "", "produce.processflow"},
|
||||
{"produce.processflow:delete", "工艺流程-删除", "BUTTON", "", "produce.processflow"},
|
||||
|
||||
@@ -85,14 +85,40 @@ func (s *Service) evaluateTorqueCriterion(ctx context.Context, sn, stationNo str
|
||||
func (s *Service) ListTorqueRecords(ctx context.Context, sn, workOrderNo string, from, to time.Time) ([]*ent.TorqueRecord, error) {
|
||||
q := s.ctx.EntClient.TorqueRecord.Query()
|
||||
if sn != "" {
|
||||
q = q.Where(torquerecord.Sn(sn))
|
||||
// 大小写不敏感(2026-09-08 全项目搜索规范)
|
||||
q = q.Where(torquerecord.SnEqualFold(sn))
|
||||
}
|
||||
if workOrderNo != "" {
|
||||
q = q.Where(torquerecord.WorkOrderNo(workOrderNo))
|
||||
q = q.Where(torquerecord.WorkOrderNoEqualFold(workOrderNo))
|
||||
}
|
||||
return q.Order(ent.Desc(torquerecord.FieldTime)).Limit(500).All(ctx)
|
||||
}
|
||||
|
||||
// AddTorqueRecord 拧紧数据补录(管理端"拧紧查询-拧紧补录"tab 用,定位为设备漏传/手工修正)
|
||||
func (s *Service) AddTorqueRecord(ctx context.Context, sn, workOrderNo, screwNo, stationNo string, strain, angle float64, result, reason, operator string) error {
|
||||
if sn == "" || workOrderNo == "" {
|
||||
return errors.New("工单号与 SN 必填")
|
||||
}
|
||||
if reason == "" {
|
||||
return errors.New("补录原因必填(如:设备漏传/手工修正)")
|
||||
}
|
||||
if result == "" {
|
||||
result = "OK"
|
||||
}
|
||||
now := time.Now()
|
||||
_, err := s.ctx.EntClient.TorqueRecord.Create().
|
||||
SetSn(sn).SetWorkOrderNo(workOrderNo).SetScrewNo(screwNo).SetStationNo(stationNo).
|
||||
SetStrain(strain).SetTorque(strain).SetAngle(angle).
|
||||
SetResult(result).SetOperator(operator+"(补录)").SetTime(now).
|
||||
Save(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "torque.manual.add", workOrderNo, operator, "torque_record", sn,
|
||||
"拧紧数据补录 "+workOrderNo+" "+sn, map[string]any{"reason": reason, "result": result, "screwNo": screwNo, "stationNo": stationNo})
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算一块工件某工序的扭矩步骤是否已报
|
||||
func (s *Service) hasWorkpieceProcess(ctx context.Context, sn string, processCode int) bool {
|
||||
cnt, _ := s.ctx.EntClient.WorkpieceProcess.Query().
|
||||
|
||||
@@ -18,6 +18,7 @@ type WorkOrderReq struct {
|
||||
WorkOrderNo string `json:"workOrderNo"`
|
||||
ProductTypeId int `json:"productTypeId"`
|
||||
ProductCode string `json:"productCode"`
|
||||
BomName string `json:"bomName"` // 建单选定的 BOM 名称(同型号多份并存;空=「默认」)
|
||||
ProductName string `json:"productName"`
|
||||
Quantity int `json:"quantity"`
|
||||
ProcessSeq string `json:"processSeq"`
|
||||
@@ -26,13 +27,34 @@ type WorkOrderReq struct {
|
||||
PlanEnd string `json:"planEnd"`
|
||||
}
|
||||
|
||||
// validateBomName 校验该产品型号下是否存在指定名称的 BOM(型号下无任何 BOM 时放行,允许先建单后补 BOM)
|
||||
func (s *Service) validateBomName(ctx context.Context, productCode, bomName string) error {
|
||||
if productCode == "" {
|
||||
return nil
|
||||
}
|
||||
names, err := s.ListBomNames(ctx, productCode)
|
||||
if err != nil {
|
||||
return nil // 查询异常不阻塞建单(与 WMS 降级策略同思路)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
target := bomNameOrDefault(bomName)
|
||||
for _, n := range names {
|
||||
if n.BomName == target {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("产品型号 %s 下不存在名为「%s」的BOM,请先在物料清单中创建", productCode, target)
|
||||
}
|
||||
|
||||
// CreateWorkOrder 创建工单(状态恒 CREATED,后续用状态流转按钮推进)
|
||||
func (s *Service) CreateWorkOrder(ctx context.Context, req WorkOrderReq, operator string) error {
|
||||
if req.WorkOrderNo == "" {
|
||||
return errors.New("工单号不能为空")
|
||||
}
|
||||
if req.ProductTypeId == 0 {
|
||||
return errors.New("请选择产品类型")
|
||||
return errors.New("请选择产品型号")
|
||||
}
|
||||
if req.Quantity <= 0 {
|
||||
req.Quantity = 1
|
||||
@@ -50,10 +72,14 @@ func (s *Service) CreateWorkOrder(ctx context.Context, req WorkOrderReq, operato
|
||||
return err
|
||||
}
|
||||
req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq)
|
||||
if err := s.validateBomName(ctx, req.ProductCode, req.BomName); err != nil {
|
||||
return err
|
||||
}
|
||||
b := s.ctx.EntClient.WorkOrder.Create().
|
||||
SetWorkOrderNo(req.WorkOrderNo).
|
||||
SetProductTypeId(req.ProductTypeId).
|
||||
SetProductCode(req.ProductCode).
|
||||
SetBomName(bomNameOrDefault(req.BomName)).
|
||||
SetProductName(req.ProductName).
|
||||
SetQuantity(req.Quantity).
|
||||
SetProcessSeq(req.ProcessSeq).
|
||||
@@ -101,6 +127,12 @@ func (s *Service) UpdateWorkOrder(ctx context.Context, req WorkOrderReq, operato
|
||||
if req.ProductCode != "" {
|
||||
u.SetProductCode(req.ProductCode)
|
||||
}
|
||||
if req.BomName != "" {
|
||||
if err := s.validateBomName(ctx, req.ProductCode, req.BomName); err != nil {
|
||||
return err
|
||||
}
|
||||
u.SetBomName(bomNameOrDefault(req.BomName))
|
||||
}
|
||||
if req.ProductName != "" {
|
||||
u.SetProductName(req.ProductName)
|
||||
}
|
||||
@@ -140,7 +172,7 @@ func (s *Service) ListWorkOrders(ctx context.Context, orderNo, status, productCo
|
||||
if productName != "" {
|
||||
q = q.Where(workorder.ProductNameContains(productName))
|
||||
}
|
||||
return q.Order(ent.Desc(workorder.FieldID)).All(ctx)
|
||||
return q.Order(ent.Desc(workorder.FieldCreatedAt), ent.Desc(workorder.FieldID)).All(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) GetWorkOrder(ctx context.Context, id int) (*ent.WorkOrder, error) {
|
||||
|
||||
@@ -226,10 +226,32 @@ func (s *Service) DoneWorkpiece(ctx context.Context, req DoneReq, operator strin
|
||||
s.bumpWorkOrderProgress(ctx, wp.OrderNo, req.Sn)
|
||||
s.bumpDailyPlanCompleted(ctx, wp.OrderNo, now)
|
||||
s.ctx.EventLog.Write(ctx, "workpiece.done", wp.OrderNo, operator, "workpiece", req.Sn, "完工+关联追溯", map[string]any{"batchItems": req.BatchItems, "serialItems": req.SerialItems})
|
||||
// 成品完工自动回流 WMS(统一库存主表 category=3):
|
||||
// 成品 SN = 工件 SN(进线登记即系统发号,全程唯一,保证对号可追溯)。
|
||||
// WMS 不可达/校验失败时降级:记 eventlog 不阻塞完工,与备料 WMS 校验降级策略同模式。
|
||||
s.retainFinishedToWms(ctx, wp, operator)
|
||||
s.notifyDashboard()
|
||||
return nil
|
||||
}
|
||||
|
||||
// retainFinishedToWms 完工品回流 WMS 库存。失败仅记日志(wms.finished.retain_failed),
|
||||
// 不回滚完工状态——仓管可在 WMS 侧人工补录,避免 WMS 抖动卡死产线。
|
||||
func (s *Service) retainFinishedToWms(ctx context.Context, wp *ent.Workpiece, operator string) {
|
||||
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
|
||||
if err != nil || wo == nil {
|
||||
s.ctx.EventLog.Write(ctx, "wms.finished.retain_failed", wp.OrderNo, operator, "workpiece", wp.Sn,
|
||||
"成品回流WMS失败:工单不存在", map[string]any{"error": "work order not found"})
|
||||
return
|
||||
}
|
||||
if wmsErr := s.ctx.Wms.FinishedInbound(ctx, wo.ProductCode, wo.ProductName, wp.Sn, wp.OrderNo, operator); wmsErr != nil {
|
||||
s.ctx.EventLog.Write(ctx, "wms.finished.retain_failed", wp.OrderNo, operator, "workpiece", wp.Sn,
|
||||
"成品回流WMS失败(库存未入,请人工补录)", map[string]any{"error": wmsErr.Error(), "productCode": wo.ProductCode})
|
||||
return
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "wms.finished.retain", wp.OrderNo, operator, "workpiece", wp.Sn,
|
||||
"成品完工已回流WMS库存", map[string]any{"productCode": wo.ProductCode})
|
||||
}
|
||||
|
||||
// Trace 追溯查询(SN 维度):工序时间线/操作人/步骤数据/物料批次SN/拧紧数据
|
||||
func (s *Service) Trace(ctx context.Context, sn string) (map[string]any, error) {
|
||||
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(sn)).First(ctx)
|
||||
|
||||
@@ -81,26 +81,27 @@ type bindTarget struct {
|
||||
UnitQty float64
|
||||
}
|
||||
|
||||
// workpieceProduct 解析工件所属产品编码(sn → workpiece → workOrder → productCode)
|
||||
func (s *Service) workpieceProduct(ctx context.Context, sn string) (*ent.Workpiece, string, error) {
|
||||
// workpieceProduct 解析工件所属产品编码与工单锁定的 BOM 名称(sn → workpiece → workOrder)
|
||||
func (s *Service) workpieceProduct(ctx context.Context, sn string) (*ent.Workpiece, *ent.WorkOrder, error) {
|
||||
wp, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.Sn(sn)).First(ctx)
|
||||
if err != nil {
|
||||
return nil, "", errors.New("工件未进线登记")
|
||||
return nil, nil, errors.New("工件未进线登记")
|
||||
}
|
||||
if wp.WorkOrderId <= 0 {
|
||||
return wp, "", errors.New("工件未关联工单")
|
||||
return wp, nil, errors.New("工件未关联工单")
|
||||
}
|
||||
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, wp.WorkOrderId)
|
||||
if err != nil || wo == nil {
|
||||
return wp, "", errors.New("工件所属工单不存在")
|
||||
return wp, nil, errors.New("工件所属工单不存在")
|
||||
}
|
||||
return wp, wo.ProductCode, nil
|
||||
return wp, wo, nil
|
||||
}
|
||||
|
||||
// stationBindTargets 取某产品在某工序(processCode>0)要求装配的物料清单
|
||||
func (s *Service) stationBindTargets(ctx context.Context, productCode string, processCode int) ([]bindTarget, error) {
|
||||
// stationBindTargets 取某工单(产品+BOM)在某工序(processCode>0)要求装配的物料清单。
|
||||
// 一型号多份 BOM 并存,必须按工单锁定的 bomName 过滤,否则不同厂家料单互相串。
|
||||
func (s *Service) stationBindTargets(ctx context.Context, productCode, bomName string, processCode int) ([]bindTarget, error) {
|
||||
items, err := s.ctx.EntClient.BomItem.Query().
|
||||
Where(bomitem.ProductCode(productCode), bomitem.ProcessCode(processCode)).All(ctx)
|
||||
Where(bomitem.ProductCode(productCode), bomitem.BomName(bomNameOrDefault(bomName)), bomitem.ProcessCode(processCode)).All(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,17 +124,18 @@ func (s *Service) applyBinds(ctx context.Context, sn, orderNo string, processCod
|
||||
if len(items) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
wp, productCode, err := s.workpieceProduct(ctx, sn)
|
||||
wp, wo, err := s.workpieceProduct(ctx, sn)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
productCode := wo.ProductCode
|
||||
if wp.Status == "DONE" || wp.Status == "SCRAPPED" {
|
||||
return 0, errors.New("已完工/报废工件不能再绑定物料")
|
||||
}
|
||||
// 产品 BOM 全量索引:materialCode → target(校验料合法性 + 取物料快照)
|
||||
// 工单锁定 BOM 的全量索引:materialCode → target(校验料合法性 + 取物料快照)
|
||||
bomMap := map[string]*ent.BomItem{}
|
||||
allBom, _ := s.ctx.EntClient.BomItem.Query().
|
||||
Where(bomitem.ProductCode(productCode)).All(ctx)
|
||||
Where(bomitem.ProductCode(productCode), bomitem.BomName(bomNameOrDefault(wo.BomName))).All(ctx)
|
||||
for _, b := range allBom {
|
||||
bomMap[b.MaterialCode] = b
|
||||
}
|
||||
@@ -186,11 +188,11 @@ func (s *Service) applyBinds(ctx context.Context, sn, orderNo string, processCod
|
||||
// 返回缺料明细(空串=齐套)。判定:精密件(SN) 绑定的不同SN数 ≥ 单台用量;结构件(批次) 至少绑定1个批次号。
|
||||
// 若该产品该工序未配置任何装配物料(processCode未配置),视为不启用绑定校验,放行。
|
||||
func (s *Service) validateStationBinds(ctx context.Context, sn string, processCode int) (string, error) {
|
||||
_, productCode, err := s.workpieceProduct(ctx, sn)
|
||||
_, wo, err := s.workpieceProduct(ctx, sn)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
targets, err := s.stationBindTargets(ctx, productCode, processCode)
|
||||
targets, err := s.stationBindTargets(ctx, wo.ProductCode, wo.BomName, processCode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -310,11 +312,11 @@ type StationBindPanel struct {
|
||||
|
||||
// StationBindPanelData 供工位终端/手动报工页在报工前拉取:本工件本工序该装什么、已装什么
|
||||
func (s *Service) StationBindPanelData(ctx context.Context, sn string, processCode int) (*StationBindPanel, error) {
|
||||
_, productCode, err := s.workpieceProduct(ctx, sn)
|
||||
_, wo, err := s.workpieceProduct(ctx, sn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets, err := s.stationBindTargets(ctx, productCode, processCode)
|
||||
targets, err := s.stationBindTargets(ctx, wo.ProductCode, wo.BomName, processCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user