feat: 五项目业务实现并对接完成

- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE
- WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置
- WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页
- 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面
- Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理
- 清理各项目球形磨遗留代码,新增部署手册.md
This commit is contained in:
SunYF
2026-08-27 19:50:57 +08:00
parent c85c3bd6f1
commit a2afd2f6dc
335 changed files with 87495 additions and 7357 deletions
@@ -0,0 +1,182 @@
package production
import (
"context"
"encoding/json"
"fmt"
"time"
"bj_power_mes/ent/agvtask"
"bj_power_mes/internal/sse"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type AgvTaskLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAgvTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AgvTaskLogic {
return &AgvTaskLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// AgvTaskCreate 发起 AGV 搬运任务(库房接驳台 → 产线接驳台)
// AgvRcs.Sim=true 时:本地记录任务并 10 秒后自动置完成,不依赖真实 RCS
func (l *AgvTaskLogic) AgvTaskCreate(req *types.AgvTaskCreateReq) (map[string]any, error) {
if req.DockFrom == "" || req.DockTo == "" {
return nil, errInvalid("dockFrom / dockTo 必填")
}
if req.Priority < 0 || req.Priority > 14 {
req.Priority = 4
}
taskNo := fmt.Sprintf("AGV%s%03d", time.Now().Format("20060102150405"), time.Now().UnixMilli()%1000)
sim := l.svcCtx.Config.AgvRcs.Sim || l.svcCtx.AgvClient == nil
status := "待下发"
respText := ""
rcsTaskID := ""
switch {
case sim:
status = "执行中"
respText = "模拟模式:直接进入执行中,10 秒后自动完成"
default:
raw, err := l.svcCtx.AgvClient.Submit(taskNo, "", req.Priority, req.Task)
if err != nil {
status = "失败"
respText = truncateStr(err.Error(), 300)
} else {
status = "执行中"
respText = string(raw)
var parsed struct {
TaskID any `json:"taskId"`
}
_ = json.Unmarshal(raw, &parsed)
rcsTaskID = fmt.Sprintf("%v", parsed.TaskID)
}
}
create := l.svcCtx.EntClient.AgvTask.Create().
SetTaskNo(taskNo).
SetStatus(status).
SetSim(sim).
SetPriority(req.Priority).
SetDockFrom(req.DockFrom).
SetDockTo(req.DockTo)
if req.OrderNo != "" {
create.SetOrderNo(req.OrderNo)
}
if rcsTaskID != "" && rcsTaskID != "<nil>" {
create.SetRcsTaskID(rcsTaskID)
}
if respText != "" {
create.SetRespText(respText)
}
rec, err := create.Save(l.ctx)
if err != nil {
return nil, err
}
if status == "执行中" && sim {
go func(id int, orderNo string) {
time.Sleep(10 * time.Second)
ctx := context.Background()
_ = l.svcCtx.EntClient.AgvTask.UpdateOneID(id).SetStatus("完成").Exec(ctx)
l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate,
map[string]any{"type": "agv_task_done", "taskNo": rec.TaskNo, "orderNo": orderNo})
}(rec.ID, req.OrderNo)
} else if status == "执行中" {
go l.pollRcs(rec.ID, taskNo)
}
l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate,
map[string]any{"type": "agv_task_created", "taskNo": taskNo, "from": req.DockFrom, "to": req.DockTo})
return map[string]any{"record": rec}, nil
}
// pollRcs 真实模式下轮询 RCS 任务状态直到结束
func (l *AgvTaskLogic) pollRcs(taskID int, taskNo string) {
client := l.svcCtx.AgvClient
if client == nil {
return
}
deadline := time.Now().Add(30 * time.Minute)
for time.Now().Before(deadline) {
time.Sleep(5 * time.Second)
raw, err := client.QueryTask(taskNo)
if err != nil {
continue
}
st := parseRcsStatus(raw)
switch st {
case "finish":
_ = l.svcCtx.EntClient.AgvTask.UpdateOneID(taskID).SetStatus("完成").Exec(context.Background())
l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate,
map[string]any{"type": "agv_task_done", "taskNo": taskNo})
return
case "fail":
_ = l.svcCtx.EntClient.AgvTask.UpdateOneID(taskID).SetStatus("失败").Exec(context.Background())
return
default:
_ = l.svcCtx.EntClient.AgvTask.UpdateOneID(taskID).SetStatus("执行中").Exec(context.Background())
}
}
_ = l.svcCtx.EntClient.AgvTask.UpdateOneID(taskID).SetStatus("失败").SetRespText("轮询超时").Exec(context.Background())
}
// parseRcsStatus 解析 RCS 任务状态字段(字段名因版本而异)
func parseRcsStatus(raw []byte) string {
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
return ""
}
data, _ := m["data"].(map[string]any)
if data == nil {
data = m
}
for _, key := range []string{"status", "taskStatus", "state"} {
if v, ok := data[key].(string); ok {
switch v {
case "FINISHED", "COMPLETED", "finished":
return "finish"
case "FAILED", "CANCELED", "failed":
return "fail"
default:
return v
}
}
}
return ""
}
func truncateStr(s string, n int) string {
if len(s) > n {
return s[:n]
}
return s
}
// AgvTaskList AGV 任务分页查询
func (l *AgvTaskLogic) AgvTaskList(req *types.AgvTaskQueryReq) (map[string]any, error) {
pg := normalizePage(req.Page, req.Size)
q := l.svcCtx.EntClient.AgvTask.Query()
if req.Status != "" {
q = q.Where(agvtask.StatusEQ(req.Status))
}
if req.OrderNo != "" {
q = q.Where(agvtask.OrderNoEQ(req.OrderNo))
}
total, err := q.Count(l.ctx)
if err != nil {
return nil, err
}
list, err := q.Offset(pg.Offset).Limit(pg.Size).All(l.ctx)
if err != nil {
return nil, err
}
return map[string]any{"total": total, "list": list}, nil
}
@@ -0,0 +1,85 @@
package production
import (
"context"
"bj_power_mes/ent"
"bj_power_mes/ent/bomitem"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type BomLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewBomLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BomLogic {
return &BomLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// BomList 查询某产品的 BOM
func (l *BomLogic) BomList(req *types.BomListReq) (*types.BomListReply, error) {
rows, err := l.svcCtx.EntClient.BomItem.Query().
Where(bomitem.ProductCodeEQ(req.ProductCode)).
Order(bomitem.ByCreatedAt()).
All(l.ctx)
if err != nil {
return nil, err
}
items := make([]types.BomItemRow, 0, len(rows))
for _, r := range rows {
items = append(items, types.BomItemRow{
MaterialCode: r.MaterialCode,
MaterialName: r.MaterialName,
UnitQty: r.UnitQty,
IsSuite: r.IsSuite,
SuiteOf: r.SuiteOf,
})
}
return &types.BomListReply{ProductCode: req.ProductCode, Items: items}, nil
}
// BomSave 保存(整表覆盖)某产品的 BOM
func (l *BomLogic) BomSave(req *types.BomSaveReq) error {
if req.ProductCode == "" {
return errInvalid("productCode 必填")
}
client := l.svcCtx.EntClient
tx, err := client.Tx(l.ctx)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
if _, err = tx.BomItem.Delete().Where(bomitem.ProductCodeEQ(req.ProductCode)).Exec(l.ctx); err != nil {
return err
}
bulk := make([]*ent.BomItemCreate, 0, len(req.Items))
for _, it := range req.Items {
if it.MaterialCode == "" {
continue
}
qty := it.UnitQty
if qty <= 0 {
qty = 1
}
bulk = append(bulk, tx.BomItem.Create().
SetProductCode(req.ProductCode).
SetMaterialCode(it.MaterialCode).
SetMaterialName(it.MaterialName).
SetUnitQty(qty).
SetIsSuite(it.IsSuite).
SetSuiteOf(it.SuiteOf),
)
}
if len(bulk) > 0 {
if _, err = tx.BomItem.CreateBulk(bulk...).Save(l.ctx); err != nil {
return err
}
}
return tx.Commit()
}
@@ -0,0 +1,70 @@
package production
import (
"context"
"encoding/json"
"fmt"
xhttp "bj_power_mes/common/httpx"
"bj_power_mes/ent/user"
"bj_power_mes/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
// CurrentUserName 从 JWT 上下文取当前用户名(查不到返回空串)
func CurrentUserName(ctx context.Context, svcCtx *svc.ServiceContext) string {
uid := xhttp.GetUidFromCtx(ctx)
if uid <= 0 {
return ""
}
u, err := svcCtx.EntClient.User.Query().Where(user.IDEQ(int(uid))).Only(ctx)
if err != nil {
return ""
}
return u.Username
}
// stockCheckResult 对齐 WMS /api/internal/stock/check 返回结构
type StockCheckResult struct {
AllOk bool `json:"allOk"`
Items []struct {
MaterialCode string `json:"materialCode"`
RequireQty int `json:"requireQty"`
AvailQty int `json:"availQty"`
BatchAvail int `json:"batchAvail"`
SerialAvail int `json:"serialAvail"`
Enough bool `json:"enough"`
} `json:"items"`
}
func parseCheckResult(raw json.RawMessage) (*StockCheckResult, error) {
var r StockCheckResult
if err := json.Unmarshal(raw, &r); err != nil {
return nil, fmt.Errorf("WMS 检查结果解析失败: %w", err)
}
return &r, nil
}
// errInvalid 参数/业务校验失败
func errInvalid(format string, args ...any) error {
return fmt.Errorf(format, args...)
}
type pageParams struct {
Page int
Size int
Offset int
}
func normalizePage(page, size int) pageParams {
if page < 1 {
page = 1
}
if size < 1 || size > 200 {
size = 20
}
return pageParams{Page: page, Size: size, Offset: (page - 1) * size}
}
var _ = logx.WithContext // 保持依赖引用
@@ -0,0 +1,164 @@
package production
import (
"context"
"encoding/json"
"fmt"
"bj_power_mes/ent/materialrequest"
"bj_power_mes/ent/workorder"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
wmsstock "bj_power_mes/internal/wmsstock"
"github.com/zeromicro/go-zero/core/logx"
)
type MaterialRequestLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewMaterialRequestLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MaterialRequestLogic {
return &MaterialRequestLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// MaterialRequestCreate 备料:按 BOM×工单数量 校验库存(可选锁库)并同步台账
func (l *MaterialRequestLogic) MaterialRequestCreate(req *types.MaterialRequestCreateReq) (map[string]any, error) {
if req.OrderNo == "" {
return nil, errInvalid("orderNo 必填")
}
order, err := l.svcCtx.EntClient.WorkOrder.Query().
Where(workorder.WorkOrderNoEQ(req.OrderNo)).
Only(l.ctx)
if err != nil {
return nil, fmt.Errorf("工单不存在: %s", req.OrderNo)
}
items := req.Items
if len(items) == 0 {
bom := NewBomLogic(l.ctx, l.svcCtx)
list, err := bom.BomList(&types.BomListReq{ProductCode: order.ProductCode})
if err != nil {
return nil, err
}
items = list.Items
}
if len(items) == 0 {
return nil, errInvalid("产品 %s 未维护 BOM,无法备料", order.ProductCode)
}
client := l.svcCtx.WmsClient
if client == nil {
return nil, errInvalid("WMS 客户端未初始化")
}
// 1. 同步台账到 WMS(出库扣减的强约束依据)
ledgerItems := make([]wmsstock.LedgerItem, 0, len(items))
checkItems := make([]wmsstock.CheckItem, 0, len(items))
for _, it := range items {
qty := it.UnitQty * order.Quantity
ledgerItems = append(ledgerItems, wmsstock.LedgerItem{
MaterialCode: it.MaterialCode,
MaterialName: it.MaterialName,
TotalQty: qty,
})
checkItems = append(checkItems, wmsstock.CheckItem{
MaterialCode: it.MaterialCode,
Qty: qty,
})
}
respText := ""
if _, err = client.LedgerSync(req.OrderNo, order.ProductCode, ledgerItems); err != nil {
l.Logger.Errorf("ledger sync failed: %v", err)
respText = "台账同步失败: " + err.Error()
}
// 2. 检查库存
raw, err := client.CheckStock(checkItems)
if err != nil {
return nil, err
}
result, err := parseCheckResult(raw)
if err != nil {
return nil, err
}
status := "已检查"
if !result.AllOk {
status = "库存不足"
} else if req.Lock {
status = "已锁定"
lockItems := make([]wmsstock.LockItem, 0, len(items))
for _, it := range items {
lockItems = append(lockItems, wmsstock.LockItem{
MaterialCode: it.MaterialCode,
Qty: it.UnitQty * order.Quantity,
})
}
if _, err = client.LockStock(req.OrderNo, lockItems); err != nil {
l.Logger.Errorf("lock stock failed: %v", err)
status = "已检查"
respText += "锁库失败: " + err.Error()
}
}
// 3. 落库记录
requestSeq, cerr := l.svcCtx.EntClient.MaterialRequest.Query().
Where(materialrequest.OrderNoEQ(req.OrderNo)).
Count(l.ctx)
if cerr != nil {
requestSeq = 0
}
requestNo := fmt.Sprintf("MR%s%02d", trimOrderNo(req.OrderNo), requestSeq+1)
create := l.svcCtx.EntClient.MaterialRequest.Create().
SetRequestNo(requestNo).
SetOrderNo(req.OrderNo).
SetAllEnough(result.AllOk).
SetStatus(status)
itemsJSON, _ := json.Marshal(result.Items)
var rows []map[string]any
_ = json.Unmarshal(itemsJSON, &rows)
create.SetItems(rows)
if req.Operator != "" {
create.SetOperator(req.Operator)
}
rec, err := create.Save(l.ctx)
if err != nil {
return nil, err
}
out := map[string]any{"record": rec}
if respText != "" {
out["warn"] = respText
}
return out, nil
}
func trimOrderNo(orderNo string) string {
const maxLen = 16
if len(orderNo) > maxLen {
return orderNo[:maxLen]
}
return orderNo
}
// MaterialRequestList 备料单分页查询
func (l *MaterialRequestLogic) MaterialRequestList(req *types.MaterialRequestQueryReq) (map[string]any, error) {
pg := normalizePage(req.Page, req.Size)
q := l.svcCtx.EntClient.MaterialRequest.Query()
if req.OrderNo != "" {
q = q.Where(materialrequest.OrderNoEQ(req.OrderNo))
}
total, err := q.Count(l.ctx)
if err != nil {
return nil, err
}
list, err := q.
Offset(pg.Offset).Limit(pg.Size).
All(l.ctx)
if err != nil {
return nil, err
}
return map[string]any{"total": total, "list": list}, nil
}
@@ -0,0 +1,51 @@
package production
import (
"context"
"bj_power_mes/ent"
"bj_power_mes/ent/plcsendlog"
"bj_power_mes/internal/plcproc"
"bj_power_mes/internal/sse"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type PlcLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewPlcLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PlcLogic {
return &PlcLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// PlcSendProcess 下发工序号到 PLCMock/未连接时自动模拟)
func (l *PlcLogic) PlcSendProcess(req *types.PlcSendProcessReq) (*ent.PlcSendLog, error) {
rec, err := plcproc.SendProcess(l.svcCtx, req.OrderNo, req.CodeStr)
if err != nil {
return nil, err
}
l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate, `{"type":"plc_sent","orderNo":"`+req.OrderNo+`","code":"`+req.CodeStr+`"}`)
return rec, nil
}
// PlcSendLogs 下发日志查询(最近在前,默认 50 条)
func (l *PlcLogic) PlcSendLogs(req *types.PlcSendLogQueryReq) (map[string]any, error) {
limit := req.Limit
if limit < 1 || limit > 200 {
limit = 50
}
q := l.svcCtx.EntClient.PlcSendLog.Query()
if req.OrderNo != "" {
q = q.Where(plcsendlog.OrderNoEQ(req.OrderNo))
}
list, err := q.Limit(limit).All(l.ctx)
if err != nil {
return nil, err
}
return map[string]any{"list": list}, nil
}
@@ -0,0 +1,89 @@
package production
import (
"context"
"bj_power_mes/ent"
"bj_power_mes/ent/processdict"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ProcessDictLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewProcessDictLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ProcessDictLogic {
return &ProcessDictLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func toRow(r *ent.ProcessDict) types.ProcessDictRow {
return types.ProcessDictRow{
Code: r.Code,
Name: r.Name,
Description: r.Description,
SortNum: r.SortNum,
Enabled: r.Enabled,
}
}
// ProcessDictList 工序字典列表(按工序号升序)
func (l *ProcessDictLogic) ProcessDictList() (*types.ProcessDictListReply, error) {
rows, err := l.svcCtx.EntClient.ProcessDict.Query().
Order(processdict.BySortNum(), processdict.ByCode()).
All(l.ctx)
if err != nil {
return nil, err
}
items := make([]types.ProcessDictRow, 0, len(rows))
for _, r := range rows {
items = append(items, toRow(r))
}
return &types.ProcessDictListReply{Items: items}, nil
}
// ProcessDictSave 批量保存(按 code upsert),支持用户自定义 12 道工序描述
func (l *ProcessDictLogic) ProcessDictSave(req *types.ProcessDictSaveReq) error {
client := l.svcCtx.EntClient
for _, it := range req.Items {
if it.Code < 1 || it.Code > 12 {
return errInvalid("工序号必须在 1~12 内")
}
name := it.Name
if name == "" {
return errInvalid("工序 %d 名称不能为空", it.Code)
}
exists, err := client.ProcessDict.Query().
Where(processdict.CodeEQ(it.Code)).
Exist(l.ctx)
if err != nil {
return err
}
if exists {
if _, err = client.ProcessDict.Update().
Where(processdict.CodeEQ(it.Code)).
SetName(name).
SetDescription(it.Description).
SetSortNum(it.SortNum).
SetEnabled(it.Enabled).
Save(l.ctx); err != nil {
return err
}
} else {
if _, err = client.ProcessDict.Create().
SetCode(it.Code).
SetName(name).
SetDescription(it.Description).
SetSortNum(it.SortNum).
SetEnabled(it.Enabled).
Save(l.ctx); err != nil {
return err
}
}
}
return nil
}
@@ -0,0 +1,103 @@
package production
import (
"context"
"strconv"
"bj_power_mes/ent/processdict"
"bj_power_mes/ent/scanrecord"
"bj_power_mes/internal/sse"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ScanLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewScanLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ScanLogic {
return &ScanLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// ScanReport 扫码报工:到料确认 / 工序完成上报 / 暂存退库
func (l *ScanLogic) ScanReport(req *types.ScanReportReq) (map[string]any, error) {
if req.Sn == "" {
return nil, errInvalid("sn 必填(扫码值)")
}
action := req.Action
if action == "" {
action = "process_done"
}
switch action {
case "arrival", "process_done", "temp_store":
default:
return nil, errInvalid("action 仅支持 arrival/process_done/temp_store")
}
create := l.svcCtx.EntClient.ScanRecord.Create().
SetSn(req.Sn).
SetAction(scanrecord.Action(action))
if req.OrderNo != "" {
create.SetOrderNo(req.OrderNo)
}
if req.DockCode != "" {
create.SetDockCode(req.DockCode)
}
if req.StepCode != "" {
create.SetStepCode(req.StepCode)
if name := processNameOf(l.ctx, l.svcCtx, req.StepCode); name != "" {
create.SetStepName(name)
}
}
rec, err := create.Save(l.ctx)
if err != nil {
return nil, err
}
l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate,
map[string]any{
"type": "scan_reported", "orderNo": req.OrderNo, "sn": req.Sn,
"stepCode": req.StepCode, "action": action,
})
return map[string]any{"id": rec.ID, "record": rec}, nil
}
// processNameOf 从工序字典取工序名称
func processNameOf(ctx context.Context, svcCtx *svc.ServiceContext, stepCode string) string {
code, err := strconv.Atoi(stepCode)
if err != nil {
return ""
}
pd, cerr := svcCtx.EntClient.ProcessDict.Query().
Where(processdict.CodeEQ(code)).
Only(ctx)
if cerr != nil {
return ""
}
return pd.Name
}
// ScanQuery 扫码记录分页查询(追溯用)
func (l *ScanLogic) ScanQuery(req *types.ScanQueryReq) (map[string]any, error) {
pg := normalizePage(req.Page, req.Size)
q := l.svcCtx.EntClient.ScanRecord.Query()
if req.OrderNo != "" {
q = q.Where(scanrecord.OrderNoEQ(req.OrderNo))
}
if req.Sn != "" {
q = q.Where(scanrecord.SnEQ(req.Sn))
}
total, err := q.Count(l.ctx)
if err != nil {
return nil, err
}
list, err := q.Offset(pg.Offset).Limit(pg.Size).All(l.ctx)
if err != nil {
return nil, err
}
return map[string]any{"total": total, "list": list}, nil
}
@@ -0,0 +1,116 @@
package production
import (
"context"
"fmt"
"strings"
"bj_power_mes/ent/semiflow"
"bj_power_mes/internal/sse"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type SemiFlowLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSemiFlowLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SemiFlowLogic {
return &SemiFlowLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// SemiInbound 半成品入库暂存(同步 WMS 半成品库,失败仅记录不影响本机操作)
func (l *SemiFlowLogic) SemiInbound(req *types.SemiFlowReq) (map[string]any, error) {
if req.Sn == "" {
return nil, errInvalid("sn 必填")
}
zone := req.ZoneCode
if zone == "" {
zone = "Z02" // 默认半成品暂存区
}
return l.record("inbound", req, zone)
}
// SemiOutbound 半成品出库重上线
func (l *SemiFlowLogic) SemiOutbound(req *types.SemiFlowReq) (map[string]any, error) {
if req.Sn == "" {
return nil, errInvalid("sn 必填")
}
return l.record("outbound", req, req.ZoneCode)
}
func (l *SemiFlowLogic) record(action string, req *types.SemiFlowReq, zone string) (map[string]any, error) {
wmsOK := false
respText := "WMS 未启用(忽略)"
client := l.svcCtx.WmsClient
if client != nil {
var err error
if action == "inbound" {
_, err = client.SemiInbound(req.Sn, req.MaterialCode, req.CompletedProcess, zone, req.OrderNo, req.Operator)
} else {
_, err = client.SemiOutbound(req.Sn, zone, req.Operator)
}
if err == nil {
wmsOK = true
respText = "OK"
} else if isWMSUnreachable(err) {
l.Errorf("wms unreachable (%s), local-only: %v", action, err)
respText = "WMS 不可达,已本地记账"
} else {
// WMS 明确拒绝(如重复入库/不在库)→ 返回错误
return nil, fmt.Errorf("WMS 拒绝%s: %v", map[string]string{"inbound": "入库", "outbound": "出库"}[action], err)
}
}
create := l.svcCtx.EntClient.SemiFlow.Create().
SetSn(req.Sn).
SetAction(semiflow.Action(action)).
SetWmsOk(wmsOK).
SetRespText(respText).
SetZoneCode(zone)
if req.OrderNo != "" {
create.SetOrderNo(req.OrderNo)
}
if req.MaterialCode != "" {
create.SetMaterialCode(req.MaterialCode)
}
if req.CompletedProcess != "" {
create.SetCompletedProcess(req.CompletedProcess)
}
rec, err := create.Save(l.ctx)
if err != nil {
return nil, err
}
l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate,
map[string]any{"type": "semi_flow", "action": action, "sn": req.Sn, "orderNo": req.OrderNo})
return map[string]any{"record": rec}, nil
}
func isWMSUnreachable(err error) bool {
return strings.Contains(err.Error(), "不可达")
}
// SemiFlows 半成品流转分页查询(追溯用)
func (l *SemiFlowLogic) SemiFlows(req *types.SemiFlowQueryReq) (map[string]any, error) {
pg := normalizePage(req.Page, req.Size)
q := l.svcCtx.EntClient.SemiFlow.Query()
if req.Sn != "" {
q = q.Where(semiflow.SnEQ(req.Sn))
}
if req.OrderNo != "" {
q = q.Where(semiflow.OrderNoEQ(req.OrderNo))
}
total, err := q.Count(l.ctx)
if err != nil {
return nil, err
}
list, err := q.Offset(pg.Offset).Limit(pg.Size).All(l.ctx)
if err != nil {
return nil, err
}
return map[string]any{"total": total, "list": list}, nil
}
@@ -0,0 +1,89 @@
package production
import (
"context"
"bj_power_mes/ent/torquerecord"
"bj_power_mes/internal/sse"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type TorqueLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewTorqueLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TorqueLogic {
return &TorqueLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// TorqueReport 拧紧数据上报(工位终端 D 经内部 API 调用,支持批量)
func (l *TorqueLogic) TorqueReport(req *types.TorqueReportReq) (map[string]any, error) {
list := req.Records
if !req.Batch {
list = []types.TorqueReportReq{*req}
}
if len(list) == 0 {
return nil, errInvalid("记录为空")
}
saved := 0
for _, rec := range list {
if rec.Sn == "" {
return nil, errInvalid("sn 必填")
}
result := rec.Result
if result != "NG" {
result = "OK"
}
create := l.svcCtx.EntClient.TorqueRecord.Create().
SetSn(rec.Sn).
SetTorque(rec.Torque).
SetAngle(rec.Angle).
SetResult(torquerecord.Result(result))
if rec.WorkOrderNo != "" {
create.SetWorkOrderNo(rec.WorkOrderNo)
}
if rec.DockCode != "" {
create.SetDockCode(rec.DockCode)
}
if rec.ScrewNo != "" {
create.SetScrewNo(rec.ScrewNo)
}
if rec.Operator != "" {
create.SetOperator(rec.Operator)
}
if _, err := create.Save(l.ctx); err != nil {
return nil, err
}
saved++
}
l.svcCtx.SSEHandler.Emit(sse.EventDashboardUpdate,
map[string]any{"type": "torque_reported", "count": saved, "workOrderNo": req.WorkOrderNo})
return map[string]any{"saved": saved}, nil
}
// TorqueQuery 拧紧记录分页查询(追溯用)
func (l *TorqueLogic) TorqueQuery(req *types.TorqueQueryReq) (map[string]any, error) {
pg := normalizePage(req.Page, req.Size)
q := l.svcCtx.EntClient.TorqueRecord.Query()
if req.Sn != "" {
q = q.Where(torquerecord.SnEQ(req.Sn))
}
if req.OrderNo != "" {
q = q.Where(torquerecord.WorkOrderNoEQ(req.OrderNo))
}
total, err := q.Count(l.ctx)
if err != nil {
return nil, err
}
list, err := q.Offset(pg.Offset).Limit(pg.Size).All(l.ctx)
if err != nil {
return nil, err
}
return map[string]any{"total": total, "list": list}, nil
}
@@ -0,0 +1,157 @@
package production
import (
"context"
"bj_power_mes/ent"
"bj_power_mes/ent/agvtask"
"bj_power_mes/ent/plcsendlog"
"bj_power_mes/ent/predicate"
"bj_power_mes/ent/scanrecord"
"bj_power_mes/ent/semiflow"
"bj_power_mes/ent/torquerecord"
"bj_power_mes/ent/workorder"
"bj_power_mes/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
type TraceLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewTraceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *TraceLogic {
return &TraceLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// Trace 全链路追溯:按 SN(优先)或工单号聚合 扫码/拧紧/半成品流转/PLC 下发/AGV 任务/工单信息
func (l *TraceLogic) Trace(sn, orderNo string) (map[string]any, error) {
if sn == "" && orderNo == "" {
return nil, errInvalid("sn 与 orderNo 至少提供一个")
}
client := l.svcCtx.EntClient
// 由 SN 反查关联工单号列表
orderNos := make([]string, 0)
if orderNo != "" {
orderNos = append(orderNos, orderNo)
}
scans, err := client.ScanRecord.Query().
Where(scanOrSnFilter(sn, orderNos)).
Order(ent.Desc(scanrecord.FieldCreatedAt)).
Limit(200).
All(l.ctx)
if err != nil {
return nil, err
}
for _, s := range scans {
if s.OrderNo != "" && !containsStr0(orderNos, s.OrderNo) {
orderNos = append(orderNos, s.OrderNo)
}
}
torques, err := client.TorqueRecord.Query().
Where(torqueFilter(sn, orderNos)).
Order(ent.Desc(torquerecord.FieldCreatedAt)).
Limit(200).
All(l.ctx)
if err != nil {
return nil, err
}
flows, err := client.SemiFlow.Query().
Where(semiFilter(sn, orderNos)).
Order(ent.Desc(semiflow.FieldCreatedAt)).
Limit(100).
All(l.ctx)
if err != nil {
return nil, err
}
plcLogs := []*ent.PlcSendLog{}
if len(orderNos) > 0 {
plcLogs, _ = client.PlcSendLog.Query().
Where(plcsendlog.OrderNoIn(orderNos...)).
Order(ent.Desc(plcsendlog.FieldID)).
Limit(50).All(l.ctx)
}
agvTasks := []*ent.AgvTask{}
if len(orderNos) > 0 {
agvTasks, _ = client.AgvTask.Query().
Where(agvtask.OrderNoIn(orderNos...)).
Order(ent.Desc(agvtask.FieldID)).
Limit(50).All(l.ctx)
}
var orders []*ent.WorkOrder
if len(orderNos) > 0 {
orders, _ = client.WorkOrder.Query().
Where(workorder.WorkOrderNoIn(orderNos...)).
All(l.ctx)
}
return map[string]any{
"sn": sn,
"orderNo": orderNo,
"orderNos": orderNos,
"scans": scans,
"torques": torques,
"semiFlows": flows,
"plcLogs": plcLogs,
"agvTasks": agvTasks,
"workOrders": orders,
}, nil
}
func scanOrSnFilter(sn string, orderNos []string) predicate.ScanRecord {
predicates := make([]predicate.ScanRecord, 0, 2)
if sn != "" {
predicates = append(predicates, scanrecord.SnEQ(sn))
}
if len(orderNos) > 0 {
predicates = append(predicates, scanrecord.OrderNoIn(orderNos...))
}
if len(predicates) == 1 {
return predicates[0]
}
return scanrecord.Or(predicates...)
}
func torqueFilter(sn string, orderNos []string) predicate.TorqueRecord {
ps := make([]predicate.TorqueRecord, 0, 2)
if sn != "" {
ps = append(ps, torquerecord.SnEQ(sn))
}
if len(orderNos) > 0 {
ps = append(ps, torquerecord.WorkOrderNoIn(orderNos...))
}
if len(ps) == 1 {
return ps[0]
}
return torquerecord.Or(ps...)
}
func semiFilter(sn string, orderNos []string) predicate.SemiFlow {
ps := make([]predicate.SemiFlow, 0, 2)
if sn != "" {
ps = append(ps, semiflow.SnEQ(sn))
}
if len(orderNos) > 0 {
ps = append(ps, semiflow.OrderNoIn(orderNos...))
}
if len(ps) == 1 {
return ps[0]
}
return semiflow.Or(ps...)
}
func containsStr0(list []string, target string) bool {
for _, s := range list {
if s == target {
return true
}
}
return false
}