Files
bj_power/bj_power_mes/internal/logic/inspection.go
T
SunYF 1cc93795ce feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段
- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
2026-09-17 12:49:07 +08:00

249 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logic
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/bomitem"
"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 FlexStr `json:"stationNo"`
Shift string `json:"shift"` // 早/中/晚
OrderNo string `json:"orderNo"` // 工单号
Sn string `json:"sn"` // 工件SN
Result string `json:"result"` // OK/NG
Items []string `json:"items"` // 点检项/异常类型
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"` // 实测值
// 质量检验(过程检/完工检/来料检)扩展字段
ReportNo string `json:"reportNo"` // 报检单号
MaterialCode string `json:"materialCode"` // 物料编码(图号)
MaterialName string `json:"materialName"` // 物料名称
Manufacturer string `json:"manufacturer"` // 生产厂家
InspectionNo string `json:"inspectionNo"` // 检验单号
AttachmentIds string `json:"attachmentIds"` // 附件ID,逗号分隔
DisposalType string `json:"disposalType"` // 处置类型 退货/返修/退换
DisposalRemark string `json:"disposalRemark"` // 处置说明
}
// CreateInspection 提交巡检记录;ALARM 类型同时触发看板报警(SSE + 清缓存)
func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, operator string) error {
if req.Category == "" {
return errors.New("记录类型必填")
}
category := req.Category
// PROCESS 为 PAD 巡检终端「过程巡检」专用;MES 车间质检「过程检」用 QC_PROCESS 隔离,
// 避免同枚举导致两个列表互混、统计口径乱(P1-8)。
if !oneOf(category, "CHECKIN", "POINT", "PROCESS", "DONE", "ALARM", "INCOMING", "FINAL", "QC_PROCESS") {
return errors.New("记录类型不合法")
}
result := req.Result
if result == "" {
result = "OK"
}
if req.Operator == "" {
req.Operator = operator
}
create := s.ctx.EntClient.InspectionRecord.Create().
SetCategory(category).
SetStationNo(string(req.StationNo)).
SetShift(req.Shift).
SetOrderNo(req.OrderNo).
SetSn(req.Sn).
SetResult(result).
SetItems(req.Items).
SetPhoto(req.Photo).
SetRemark(req.Remark).
SetOperator(req.Operator).
SetProcessCode(req.ProcessCode).
SetStepId(req.StepId).
SetStepName(req.StepName).
SetMeasuredValue(req.MeasuredValue)
if req.ReportNo != "" {
create.SetReportNo(req.ReportNo)
}
if req.MaterialCode != "" {
create.SetMaterialCode(req.MaterialCode)
}
if req.MaterialName != "" {
create.SetMaterialName(req.MaterialName)
}
if req.Manufacturer != "" {
create.SetManufacturer(req.Manufacturer)
}
if req.InspectionNo != "" {
create.SetInspectionNo(req.InspectionNo)
}
if req.AttachmentIds != "" {
create.SetAttachmentIds(req.AttachmentIds)
}
if req.DisposalType != "" {
create.SetDisposalType(req.DisposalType)
}
if req.DisposalRemark != "" {
create.SetDisposalRemark(req.DisposalRemark)
}
_, err := create.Save(ctx)
if err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "inspection."+category, req.OrderNo, req.Operator, "inspection_record",
req.Sn, "巡检终端提交", map[string]any{"category": category, "stationNo": string(req.StationNo), "result": result})
if category == "ALARM" {
// 触发看板报警:SSE 推送 + 使 Redis 看板报警缓存失效
alarmJSON, _ := json.Marshal(map[string]any{
"level": "CRITICAL",
"type": "inspection_alarm",
"message": "巡检异常上报:" + req.Remark,
"station": string(req.StationNo),
"time": time.Now().Format("2006-01-02 15:04:05"),
})
if s.ctx.SSE != nil {
s.ctx.SSE.Publish("dashboard.alarm", string(alarmJSON))
}
if s.ctx.RedisClient != nil {
_, _ = s.ctx.RedisClient.Del("dashboard:alarms")
}
}
s.notifyDashboard()
return nil
}
// ListInspections 查询巡检记录(不分页,供内部/导出用,封顶 1000)
func (s *Service) ListInspections(ctx context.Context, category, operator, from, to string) ([]*ent.InspectionRecord, error) {
rows, _, err := s.ListInspectionsPaged(ctx, category, operator, from, to, "", "", "", "", "", 1, 1000)
return rows, err
}
// ListInspectionsPaged 分页查询巡检记录,返回 {rows, total};排序 id desc 最新置顶
// orderNo/stationNo2026-09-08 补充筛选(大小写不敏感)
// materialCode/materialName/reportNo:质量检验模糊查询(Item H,每框内模糊、多框取交集)
func (s *Service) ListInspectionsPaged(ctx context.Context, category, operator, from, to, orderNo, stationNo, materialCode, materialName, reportNo 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 materialCode != "" {
q = q.Where(inspectionrecord.MaterialCodeContainsFold(materialCode))
}
if materialName != "" {
q = q.Where(inspectionrecord.MaterialNameContainsFold(materialName))
}
if reportNo != "" {
q = q.Where(inspectionrecord.ReportNoContainsFold(reportNo))
}
if operator != "" {
q = q.Where(inspectionrecord.Operator(operator))
}
if from != "" {
if f, err := time.Parse("2006-01-02", from); err == nil {
q = q.Where(inspectionrecord.CreatedAtGTE(f))
}
}
if to != "" {
if t, err := time.Parse("2006-01-02", to); err == nil {
q = q.Where(inspectionrecord.CreatedAtLT(t.Add(24 * time.Hour)))
}
}
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 {
for _, it := range items {
if v == it {
return true
}
}
return false
}
// GetInspectionRecord 按 id 查询一条检验记录(质量检验处置/不合格单用)
func (s *Service) GetInspectionRecord(ctx context.Context, id int) (*ent.InspectionRecord, error) {
if id <= 0 {
return nil, errors.New("缺少 id")
}
rec, err := s.ctx.EntClient.InspectionRecord.Get(ctx, id)
if err != nil {
return nil, errors.New("检验记录不存在")
}
return rec, nil
}
// SetInspectionDisposal 质量检验处置:区分 退货/返修/退换,写处置说明
func (s *Service) SetInspectionDisposal(ctx context.Context, id int, disposalType, disposalRemark, operator string) error {
if id <= 0 {
return errors.New("缺少 id")
}
if !oneOf(disposalType, "退货", "返修", "退换") {
return errors.New("处置类型仅支持 退货/返修/退换")
}
rec, err := s.ctx.EntClient.InspectionRecord.Get(ctx, id)
if err != nil {
return errors.New("检验记录不存在")
}
_, err = s.ctx.EntClient.InspectionRecord.UpdateOneID(id).
SetDisposalType(disposalType).SetDisposalRemark(disposalRemark).Save(ctx)
if err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "inspection.disposal", rec.OrderNo, operator, "inspection_record", rec.Sn,
"质量检验处置 "+disposalType, map[string]any{"id": id, "remark": disposalRemark})
return nil
}
// QueryBomMaterial 按物料编码或名称/图号模糊检索 BOM 料信息(质量检验"选物料自动带出 BOM"用)
func (s *Service) QueryBomMaterial(ctx context.Context, keyword string) ([]*ent.BomItem, error) {
if keyword == "" {
return []*ent.BomItem{}, nil
}
q := s.ctx.EntClient.BomItem.Query()
q = q.Where(
bomitem.Or(
bomitem.MaterialCodeContainsFold(keyword),
bomitem.MaterialNameContainsFold(keyword),
),
)
return q.Order(ent.Asc(bomitem.FieldProductCode), ent.Asc(bomitem.FieldBomName), ent.Asc(bomitem.FieldID)).Limit(200).All(ctx)
}