Files
bj_power/bj_power_mes/internal/logic/torque.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

345 lines
12 KiB
Go

package logic
import (
"context"
"errors"
"sort"
"strconv"
"strings"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/station"
"bj_power_mes/ent/stepcriterion"
"bj_power_mes/ent/torqueauditlog"
"bj_power_mes/ent/torquerecord"
"bj_power_mes/ent/workpieceprocess"
)
type TorqueReq struct {
Sn string `json:"sn"`
WorkOrder string `json:"workOrder"`
WorkOrderNo string `json:"workOrderNo"`
StationNo string `json:"stationNo"`
ScrewNo string `json:"screwNo"`
Strain float64 `json:"strain"` // 扭矩
Torque float64 `json:"torque"`
Angle float64 `json:"angle"`
Result string `json:"result"`
Operator string `json:"operator"`
Time time.Time `json:"time"`
}
// ReportTorque 接收拧紧数据(工位终端/内部接口上报),入库并做扭矩 RANGE 判定写 step_data
func (s *Service) ReportTorque(ctx context.Context, req TorqueReq, operator string) error {
if req.Sn == "" {
return errors.New("sn 不能为空")
}
if req.Strain == 0 {
req.Strain = req.Torque
}
result := req.Result
if result == "" {
result = "OK"
}
workOrderNo := req.WorkOrder
if workOrderNo == "" {
workOrderNo = req.WorkOrderNo
}
if req.Time.IsZero() {
req.Time = time.Now()
}
_, err := s.ctx.EntClient.TorqueRecord.Create().
SetWorkOrderNo(workOrderNo).SetSn(req.Sn).SetScrewNo(req.ScrewNo).
SetStrain(req.Strain).SetTorque(req.Strain).SetAngle(req.Angle).
SetResult(result).SetOperator(req.Operator).SetStationNo(req.StationNo).
SetTime(req.Time).Save(ctx)
if err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "torque.report", workOrderNo, operator, "torque_record", req.Sn, "接收拧紧数据上报", map[string]any{"strain": req.Strain, "angle": req.Angle, "result": result})
s.evaluateTorqueCriterion(ctx, req.Sn, req.StationNo, req.Strain, req.Angle, operator)
s.notifyDashboard()
return nil
}
// evaluateTorqueCriterion 依据 step_criterion RANGE 自动判定 OK/NG 写入 step_data
func (s *Service) evaluateTorqueCriterion(ctx context.Context, sn, stationNo string, strain, angle float64, operator string) {
q := s.ctx.EntClient.StepCriterion.Query().Where(stepcriterion.Name("扭矩"))
crits, _ := q.All(ctx)
ng := false
for _, c := range crits {
if c.Logic != "RANGE" {
continue
}
ok := true
if c.Min != nil && strain < *c.Min {
ok = false
}
if c.Max != nil && strain > *c.Max {
ok = false
}
_, _ = s.ctx.EntClient.StepData.Create().
SetSn(sn).SetStepId(c.StepId).SetCriterionId(c.ID).
SetCriterionName(c.Name).SetCriterionLogic(c.Logic).
SetValue(strain).SetIsOK(ok).SetOperator(operator).Save(ctx)
if !ok {
ng = true
}
}
if ng {
s.Evaluate(ctx, "torque_ng", 1, "拧紧扭矩不合格", "SN="+sn+" 工位"+stationNo, "torque_record", sn)
}
}
// ListTorqueRecords 查询拧紧数据(四条件复合取交集:工单号/SN/日期区间/工位)
func (s *Service) ListTorqueRecords(ctx context.Context, sn, workOrderNo, stationNo string, from, to time.Time) ([]*ent.TorqueRecord, error) {
q := s.ctx.EntClient.TorqueRecord.Query()
if sn != "" {
// 大小写不敏感(2026-09-08 全项目搜索规范)
q = q.Where(torquerecord.SnEqualFold(sn))
}
if workOrderNo != "" {
q = q.Where(torquerecord.WorkOrderNoEqualFold(workOrderNo))
}
if stationNo != "" {
q = q.Where(torquerecord.StationNoEqualFold(stationNo))
}
if !from.IsZero() {
q = q.Where(torquerecord.TimeGTE(from))
}
if !to.IsZero() {
q = q.Where(torquerecord.TimeLTE(to))
}
return q.Order(ent.Desc(torquerecord.FieldTime)).Limit(500).All(ctx)
}
// requiredScrews 按工位号取该工位绑定工艺流程中 isTorque=true 的步骤数 = 应拧数量。
// 工位未绑定流程或流程无扭矩步骤时返回 0(表示无工艺基准,前端标注为「未配置」)。
func (s *Service) requiredScrews(ctx context.Context, stationNo string) int {
no, err := strconv.Atoi(strings.TrimSpace(stationNo))
if err != nil || no <= 0 {
return 0
}
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(no)).First(ctx)
if err != nil || st.FlowId <= 0 {
return 0
}
cnt, _ := s.ctx.EntClient.ProcessStep.Query().
Where(processstep.FlowId(st.FlowId), processstep.IsTorque(true)).Count(ctx)
return cnt
}
// TorqueGroupVO 拧紧一级汇总行(按 工单号+SN+工位 分组)
type TorqueGroupVO struct {
WorkOrderNo string `json:"workOrderNo"`
Sn string `json:"sn"`
StationNo string `json:"stationNo"`
Required int `json:"required"` // 应拧数量(工艺流程 isTorque 步骤数)
Actual int `json:"actual"` // 实际记录数
OkCount int `json:"okCount"` // 合格颗数
NgCount int `json:"ngCount"` // 不合格颗数
Satisfied bool `json:"satisfied"` // 是否满足:应拧>0 且 实拧>=应拧 且 无不合格
Audited bool `json:"audited"` // 是否已全部审核签字
Operator string `json:"operator"` // 最近操作人
LastTime time.Time `json:"lastTime"` // 最近采集时间
}
// ListTorqueGroups 拧紧一级汇总列表:按 工单号+SN+工位 分组,附「应拧 vs 实拧」满足判定,后端真分页。
func (s *Service) ListTorqueGroups(ctx context.Context, sn, workOrderNo, stationNo string, from, to time.Time, page, pageSize int) (map[string]any, error) {
recs, err := s.ListTorqueRecords(ctx, sn, workOrderNo, stationNo, from, to)
if err != nil {
return nil, err
}
type gkey struct{ wo, sn, st string }
agg := map[gkey]*TorqueGroupVO{}
reqCache := map[string]int{}
for _, r := range recs {
k := gkey{r.WorkOrderNo, r.Sn, r.StationNo}
g, ok := agg[k]
if !ok {
g = &TorqueGroupVO{WorkOrderNo: r.WorkOrderNo, Sn: r.Sn, StationNo: r.StationNo}
agg[k] = g
}
g.Actual++
if r.Result == "OK" {
g.OkCount++
} else {
g.NgCount++
}
if r.Time.After(g.LastTime) {
g.LastTime = r.Time
g.Operator = r.Operator
}
}
list := make([]*TorqueGroupVO, 0, len(agg))
for _, g := range agg {
req, ok := reqCache[g.StationNo]
if !ok {
req = s.requiredScrews(ctx, g.StationNo)
reqCache[g.StationNo] = req
}
g.Required = req
g.Satisfied = req > 0 && g.Actual >= req && g.NgCount == 0
list = append(list, g)
}
// Audited 需基于分组内全部记录:任一条未审核即整组未审核
auditOK := map[gkey]bool{}
for _, r := range recs {
k := gkey{r.WorkOrderNo, r.Sn, r.StationNo}
if _, seen := auditOK[k]; !seen {
auditOK[k] = true
}
if r.AuditBy == "" {
auditOK[k] = false
}
}
for _, g := range list {
g.Audited = auditOK[gkey{g.WorkOrderNo, g.Sn, g.StationNo}]
}
sort.Slice(list, func(i, j int) bool { return list[i].LastTime.After(list[j].LastTime) })
total := len(list)
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
start := (page - 1) * pageSize
if start > total {
start = total
}
end := start + pageSize
if end > total {
end = total
}
return map[string]any{"total": total, "list": list[start:end], "page": page, "pageSize": pageSize}, nil
}
// TorqueScrewStat 单工件单工位「应拧 vs 实拧」统计(二级明细抽屉顶部展示)
func (s *Service) TorqueScrewStat(ctx context.Context, sn, stationNo string) (*TorqueGroupVO, error) {
if strings.TrimSpace(sn) == "" {
return nil, errors.New("sn 不能为空")
}
q := s.ctx.EntClient.TorqueRecord.Query().Where(torquerecord.SnEqualFold(sn))
if stationNo != "" {
q = q.Where(torquerecord.StationNoEqualFold(stationNo))
}
recs, _ := q.All(ctx)
vo := &TorqueGroupVO{Sn: sn, StationNo: stationNo, Required: s.requiredScrews(ctx, stationNo)}
allAudited := len(recs) > 0
for _, r := range recs {
vo.Actual++
if r.WorkOrderNo != "" && vo.WorkOrderNo == "" {
vo.WorkOrderNo = r.WorkOrderNo
}
if r.Result == "OK" {
vo.OkCount++
} else {
vo.NgCount++
}
if r.AuditBy == "" {
allAudited = false
}
if r.Time.After(vo.LastTime) {
vo.LastTime = r.Time
vo.Operator = r.Operator
}
}
vo.Satisfied = vo.Required > 0 && vo.Actual >= vo.Required && vo.NgCount == 0
vo.Audited = allAudited
return vo, nil
}
// AuditTorqueRecord 拧紧记录审核签字:标记审核人/时间并写留痕
func (s *Service) AuditTorqueRecord(ctx context.Context, recordId int, remark, operator string) error {
rec, err := s.ctx.EntClient.TorqueRecord.Get(ctx, recordId)
if err != nil {
return errors.New("拧紧记录不存在")
}
if rec.AuditBy != "" {
return errors.New("该记录已审核,不可重复审核")
}
_, err = rec.Update().SetAuditBy(operator).SetAuditAt(time.Now()).Save(ctx)
if err != nil {
return err
}
_, _ = s.ctx.EntClient.TorqueAuditLog.Create().
SetTorqueRecordId(rec.ID).SetAction("审核").SetOperator(operator).
SetRemark(remark).SetOldVal(rec.AuditBy).SetNewVal(operator).Save(ctx)
return nil
}
// ListTorqueAuditLog 查询某拧紧记录的修改留痕
func (s *Service) ListTorqueAuditLog(ctx context.Context, recordId int) ([]*ent.TorqueAuditLog, error) {
return s.ctx.EntClient.TorqueAuditLog.Query().
Where(torqueauditlog.TorqueRecordId(recordId)).
Order(ent.Desc(torqueauditlog.FieldCreatedAt)).All(ctx)
}
// 计算一块工件某工序的扭矩步骤是否已报
func (s *Service) hasWorkpieceProcess(ctx context.Context, sn string, processCode int) bool {
cnt, _ := s.ctx.EntClient.WorkpieceProcess.Query().
Where(workpieceprocess.Sn(sn), workpieceprocess.ProcessCode(processCode)).Count(ctx)
return cnt > 0
}
// ProcessStepTemplate 下发给前端渲染的参数采集模板
type ProcessStepTemplate struct {
ID int `json:"id"`
Seq int `json:"seq"`
Name string `json:"name"`
CollectType string `json:"collectType"` // AUTO(拧紧枪自动)/MANUAL(手填)/NONE
IsTorque bool `json:"isTorque"`
NeedCheck bool `json:"needCheck"`
Remark string `json:"remark"`
Attachment []map[string]string `json:"attachment"` // 本步骤图纸/附件 [{name,url}],按工序分发到工位终端
Criteria []CriterionVO `json:"criteria"`
}
// CriterionVO 考核标准视图
type CriterionVO struct {
ID int `json:"id"`
Name string `json:"name"`
Unit string `json:"unit"`
Logic string `json:"logic"` // GE/LE/GT/LT/RANGE/EQUAL/NONE
Target float64 `json:"target"`
Min *float64 `json:"min"`
Max *float64 `json:"max"`
}
// ListStationSteps 按工位号加载该工位绑定且「启用」的工艺流程的工序步骤(含考核标准)。
// 链路:工位号 → station.flow_id → 工艺流程(绑定关系在「关联工位」页维护,流程可绑任意工位)。
// 手动报工页:选工位 → 自动带出该工位的工序步骤。
func (s *Service) ListStationSteps(ctx context.Context, stationNo int) ([]*ProcessStepTemplate, error) {
if stationNo < 1 {
return []*ProcessStepTemplate{}, nil
}
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx)
if err != nil || st.FlowId <= 0 {
// 该工位未绑定工艺流程:返回空模板(前端提示)
return []*ProcessStepTemplate{}, nil
}
flow, err := s.ctx.EntClient.ProcessFlow.Get(ctx, st.FlowId)
if err != nil || flow.Status != "ACTIVE" {
return []*ProcessStepTemplate{}, nil
}
return s.flowSteps(ctx, flow.ID), nil
}
// StepItemReq 一次保存的一个步骤(含其考核标准)
type StepItemReq struct {
ID int `json:"id"`
Seq int `json:"seq"`
Name string `json:"name"`
CollectType string `json:"collectType"`
IsTorque bool `json:"isTorque"`
NeedCheck bool `json:"needCheck"`
Remark string `json:"remark"`
Attachment []map[string]string `json:"attachment"` // 本步骤图纸/附件 [{name,url}]
Criteria []CriterionVO `json:"criteria"`
}