2026-08-31 08:06:55 +08:00
|
|
|
|
package logic
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"errors"
|
2026-09-08 11:44:04 +08:00
|
|
|
|
"strings"
|
2026-08-31 08:06:55 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"bj_power_mes/ent"
|
2026-09-17 12:49:07 +08:00
|
|
|
|
"bj_power_mes/ent/bomitem"
|
2026-08-31 08:06:55 +08:00
|
|
|
|
"bj_power_mes/ent/inspectionrecord"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-08 11:44:04 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 08:06:55 +08:00
|
|
|
|
// InspectionReq 巡检记录请求(PAD 巡检终端,块8)
|
|
|
|
|
|
type InspectionReq struct {
|
|
|
|
|
|
Category string `json:"category"` // CHECKIN/POINT/PROCESS/DONE/ALARM
|
2026-09-08 11:44:04 +08:00
|
|
|
|
StationNo FlexStr `json:"stationNo"`
|
2026-08-31 08:06:55 +08:00
|
|
|
|
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"` // 操作人
|
2026-09-15 16:37:39 +08:00
|
|
|
|
// P0-4 过程巡检按步骤维度
|
|
|
|
|
|
ProcessCode int `json:"processCode"` // 所属工位号
|
|
|
|
|
|
StepId int `json:"stepId"` // 工艺步骤ID
|
|
|
|
|
|
StepName string `json:"stepName"` // 工艺步骤名称
|
|
|
|
|
|
MeasuredValue string `json:"measuredValue"` // 实测值
|
2026-09-17 12:49:07 +08:00
|
|
|
|
// 质量检验(过程检/完工检/来料检)扩展字段
|
|
|
|
|
|
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"` // 处置说明
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CreateInspection 提交巡检记录;ALARM 类型同时触发看板报警(SSE + 清缓存)
|
|
|
|
|
|
func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, operator string) error {
|
|
|
|
|
|
if req.Category == "" {
|
|
|
|
|
|
return errors.New("记录类型必填")
|
|
|
|
|
|
}
|
|
|
|
|
|
category := req.Category
|
2026-09-17 12:49:07 +08:00
|
|
|
|
// PROCESS 为 PAD 巡检终端「过程巡检」专用;MES 车间质检「过程检」用 QC_PROCESS 隔离,
|
|
|
|
|
|
// 避免同枚举导致两个列表互混、统计口径乱(P1-8)。
|
|
|
|
|
|
if !oneOf(category, "CHECKIN", "POINT", "PROCESS", "DONE", "ALARM", "INCOMING", "FINAL", "QC_PROCESS") {
|
2026-08-31 08:06:55 +08:00
|
|
|
|
return errors.New("记录类型不合法")
|
|
|
|
|
|
}
|
|
|
|
|
|
result := req.Result
|
|
|
|
|
|
if result == "" {
|
|
|
|
|
|
result = "OK"
|
|
|
|
|
|
}
|
|
|
|
|
|
if req.Operator == "" {
|
|
|
|
|
|
req.Operator = operator
|
|
|
|
|
|
}
|
2026-09-17 12:49:07 +08:00
|
|
|
|
create := s.ctx.EntClient.InspectionRecord.Create().
|
2026-08-31 08:06:55 +08:00
|
|
|
|
SetCategory(category).
|
2026-09-08 11:44:04 +08:00
|
|
|
|
SetStationNo(string(req.StationNo)).
|
2026-08-31 08:06:55 +08:00
|
|
|
|
SetShift(req.Shift).
|
|
|
|
|
|
SetOrderNo(req.OrderNo).
|
|
|
|
|
|
SetSn(req.Sn).
|
|
|
|
|
|
SetResult(result).
|
|
|
|
|
|
SetItems(req.Items).
|
|
|
|
|
|
SetPhoto(req.Photo).
|
|
|
|
|
|
SetRemark(req.Remark).
|
|
|
|
|
|
SetOperator(req.Operator).
|
2026-09-15 16:37:39 +08:00
|
|
|
|
SetProcessCode(req.ProcessCode).
|
|
|
|
|
|
SetStepId(req.StepId).
|
|
|
|
|
|
SetStepName(req.StepName).
|
2026-09-17 12:49:07 +08:00
|
|
|
|
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)
|
2026-08-31 08:06:55 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
s.ctx.EventLog.Write(ctx, "inspection."+category, req.OrderNo, req.Operator, "inspection_record",
|
2026-09-08 11:44:04 +08:00
|
|
|
|
req.Sn, "巡检终端提交", map[string]any{"category": category, "stationNo": string(req.StationNo), "result": result})
|
2026-08-31 08:06:55 +08:00
|
|
|
|
|
|
|
|
|
|
if category == "ALARM" {
|
|
|
|
|
|
// 触发看板报警:SSE 推送 + 使 Redis 看板报警缓存失效
|
|
|
|
|
|
alarmJSON, _ := json.Marshal(map[string]any{
|
|
|
|
|
|
"level": "CRITICAL",
|
|
|
|
|
|
"type": "inspection_alarm",
|
|
|
|
|
|
"message": "巡检异常上报:" + req.Remark,
|
2026-09-08 11:44:04 +08:00
|
|
|
|
"station": string(req.StationNo),
|
2026-08-31 08:06:55 +08:00
|
|
|
|
"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")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-04 17:31:18 +08:00
|
|
|
|
s.notifyDashboard()
|
2026-08-31 08:06:55 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-08 11:44:04 +08:00
|
|
|
|
// ListInspections 查询巡检记录(不分页,供内部/导出用,封顶 1000)
|
2026-08-31 08:06:55 +08:00
|
|
|
|
func (s *Service) ListInspections(ctx context.Context, category, operator, from, to string) ([]*ent.InspectionRecord, error) {
|
2026-09-17 12:49:07 +08:00
|
|
|
|
rows, _, err := s.ListInspectionsPaged(ctx, category, operator, from, to, "", "", "", "", "", 1, 1000)
|
2026-09-08 11:44:04 +08:00
|
|
|
|
return rows, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListInspectionsPaged 分页查询巡检记录,返回 {rows, total};排序 id desc 最新置顶
|
|
|
|
|
|
// orderNo/stationNo:2026-09-08 补充筛选(大小写不敏感)
|
2026-09-17 12:49:07 +08:00
|
|
|
|
// 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) {
|
2026-09-08 11:44:04 +08:00
|
|
|
|
q := s.ctx.EntClient.InspectionRecord.Query()
|
2026-08-31 08:06:55 +08:00
|
|
|
|
if category != "" {
|
|
|
|
|
|
q = q.Where(inspectionrecord.Category(category))
|
|
|
|
|
|
}
|
2026-09-08 11:44:04 +08:00
|
|
|
|
if orderNo != "" {
|
|
|
|
|
|
q = q.Where(inspectionrecord.OrderNoEqualFold(orderNo))
|
|
|
|
|
|
}
|
|
|
|
|
|
if stationNo != "" {
|
|
|
|
|
|
q = q.Where(inspectionrecord.StationNoEqualFold(stationNo))
|
|
|
|
|
|
}
|
2026-09-17 12:49:07 +08:00
|
|
|
|
if materialCode != "" {
|
|
|
|
|
|
q = q.Where(inspectionrecord.MaterialCodeContainsFold(materialCode))
|
|
|
|
|
|
}
|
|
|
|
|
|
if materialName != "" {
|
|
|
|
|
|
q = q.Where(inspectionrecord.MaterialNameContainsFold(materialName))
|
|
|
|
|
|
}
|
|
|
|
|
|
if reportNo != "" {
|
|
|
|
|
|
q = q.Where(inspectionrecord.ReportNoContainsFold(reportNo))
|
|
|
|
|
|
}
|
2026-08-31 08:06:55 +08:00
|
|
|
|
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)))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-08 11:44:04 +08:00
|
|
|
|
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
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func oneOf(v string, items ...string) bool {
|
|
|
|
|
|
for _, it := range items {
|
|
|
|
|
|
if v == it {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-09-17 12:49:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
}
|