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:
@@ -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 下发工序号到 PLC(Mock/未连接时自动模拟)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user