package handler import ( "encoding/json" "net/http" "strings" "time" "bj_power_wms/ent" "bj_power_wms/ent/agvtask" "bj_power_wms/ent/dock" "bj_power_wms/internal/svc" "github.com/google/uuid" ) // ---------- AGV 配送接口 ---------- // submitAgvRequest 仓管下发 AGV:至少选一批待发料行,按 target_dock 聚合生成任务 type submitAgvRequest struct { // rows: 待发料行(每行含 requestNo/物料/数量/targetDock),来自 MES 备料单 Rows []agvSubmitRow `json:"rows"` // 也可以直接给一个 group:sourceDock + 多目标 SourceDock string `json:"sourceDock"` } type agvSubmitRow struct { RequestNo string `json:"requestNo"` MaterialCode string `json:"materialCode"` MaterialName string `json:"materialName"` Unit string `json:"unit"` Qty float64 `json:"qty"` TargetDock string `json:"targetDock"` } func submitAgvHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req submitAgvRequest if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } if len(req.Rows) == 0 { fail(w, http.StatusBadRequest, "请至少选择一批待发料") return } source := req.SourceDock if source == "" { source = "DOCK21" } operator := r.Header.Get("X-Username") // 按 target_dock 聚合 groups := map[string][]agvSubmitRow{} for _, row := range req.Rows { if row.TargetDock == "" { continue } groups[row.TargetDock] = append(groups[row.TargetDock], row) } if len(groups) == 0 { fail(w, http.StatusBadRequest, "所选料行缺少目标接驳台(targetDock)") return } created := 0 failed := []string{} for target, rows := range groups { // 1. 生成我方任务号并落库(PENDING→下发) taskNo := "AGV" + strings.ToUpper(uuid.NewString()[:10]) mats, _ := json.Marshal(rows) t, err := ctx.EntClient.AgvTask.Create(). SetTaskNo(taskNo). SetSourceDock(source). SetTargetDock(target). SetMaterialsJSON(string(mats)). SetStatus("PENDING"). SetOperator(operator). SetDispatchedAt(0). SetArrivedAt(0). Save(ctx0()) if err != nil { failed = append(failed, target) continue } // 2. 调海康 RCS 下发 robCode, aerr := ctx.Agv.SubmitTask(ctx0(), source, target, "") if aerr != nil { ctx.EntClient.AgvTask.UpdateOneID(t.ID). SetStatus("FAILED").SetRemark("RCS下发失败: "+aerr.Error()). Exec(ctx0()) failed = append(failed, target) continue } ctx.EntClient.AgvTask.UpdateOneID(t.ID). SetHikTaskCode(robCode). SetStatus("DISPATCHED"). SetDispatchedAt(time.Now().Unix()). Exec(ctx0()) // 3. 回写 MES 备料单 → DELIVERING for _, row := range rows { _ = ctx.Mes.MarkStatus(ctx0(), row.RequestNo, "DELIVERING") } // 4. 记录事件日志 ctx.EventLog.Write(ctx0(), "agv.dispatch", operator, "agv_task", taskNo, "下发AGV搬运 "+source+"→"+target, map[string]any{"rows": len(rows)}) created++ } ok(w, map[string]any{"created": created, "failed": failed}) } } func listAgvTasksHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() query := ctx.EntClient.AgvTask.Query() if st := q.Get("status"); st != "" { query = query.Where(agvtask.Status(st)) } if dock2 := q.Get("targetDock"); dock2 != "" { query = query.Where(agvtask.TargetDock(dock2)) } page := atoi(q.Get("page"), 1) size := atoi(q.Get("pageSize"), 20) if size < 1 || size > 200 { size = 20 } total, err := query.Clone().Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := query. Order(ent.Desc("created_at")). Offset((page - 1) * size).Limit(size). All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]any{"list": list, "total": total, "page": page, "pageSize": size}) } } // refreshAgvHandler 轮询海康 RCS 更新各任务状态 func refreshAgvHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 取所有未终态(DISPATCHED/DELIVERING)且已下发的任务 list, err := ctx.EntClient.AgvTask.Query(). Where( agvtask.StatusIn("DISPATCHED", "DELIVERING"), agvtask.HikTaskCodeNEQ(""), ).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } changed := 0 for _, t := range list { st, qerr := ctx.Agv.QueryTask(ctx0(), t.HikTaskCode) if qerr != nil { continue } // 海康状态映射到我们的状态 our := mapHikStatus(st) if our == "DONE" { ctx.EntClient.AgvTask.UpdateOneID(t.ID). SetStatus("DONE").SetArrivedAt(time.Now().Unix()). Exec(ctx0()) // 到位后回写 MES 备料单 → DONE mats := []agvSubmitRow{} _ = json.Unmarshal([]byte(t.MaterialsJSON), &mats) for _, row := range mats { _ = ctx.Mes.MarkStatus(ctx0(), row.RequestNo, "DONE") } ctx.EventLog.Write(ctx0(), "agv.arrive", r.Header.Get("X-Username"), "agv_task", t.TaskNo, "AGV已到位 "+t.TargetDock, nil) changed++ } else if our != "" && t.Status != our && our == "DELIVERING" { ctx.EntClient.AgvTask.UpdateOneID(t.ID).SetStatus("DELIVERING").Exec(ctx0()) changed++ } } ok(w, map[string]any{"changed": changed}) } } // mapHikStatus 海康 taskStatus → 我方状态 // 海康: QUEUE/WAIT/EXECUTING/MANUALED/FINISHED/CANCELLED func mapHikStatus(s string) string { switch s { case "EXECUTING", "DELIVERING": return "DELIVERING" case "FINISHED", "MANUALED": return "DONE" case "CANCELLED": return "CANCELLED" default: // QUEUE / WAIT / 未知 return "DISPATCHED" } } func listDocksHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() query := ctx.EntClient.Dock.Query() if typ := q.Get("dockType"); typ != "" { query = query.Where(dock.DockType(typ)) } list, err := query.Order(ent.Asc("id")).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } // 显式构造,避免 ent omitempty 吞掉 hasPallet=false 导致前端看不到空闲状态 type dockView struct { ID int64 `json:"id"` DockCode string `json:"dockCode"` Name string `json:"name"` DockType string `json:"dockType"` StationNo int `json:"stationNo"` Status string `json:"status"` HasPallet bool `json:"hasPallet"` PalletRef string `json:"palletRef"` } views := make([]dockView, 0, len(list)) for _, d := range list { views = append(views, dockView{ ID: int64(d.ID), DockCode: d.DockCode, Name: d.Name, DockType: d.DockType, StationNo: d.StationNo, Status: d.Status, HasPallet: d.HasPallet, PalletRef: d.PalletRef, }) } ok(w, map[string]any{"list": views}) } } // listAgvMaterialsHandler 仓管下发 AGV 时拉取 MES 备料单(含 target_dock),本地分页。 // MES 内部接口全量返回备料单行;备料单行数可控,内存分页足够。 func listAgvMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() status := q.Get("status") all, err := ctx.Mes.ListMaterialRequests(ctx0(), status) if err != nil { fail(w, http.StatusInternalServerError, "拉取备料单失败: "+err.Error()) return } page := atoi(q.Get("page"), 1) size := atoi(q.Get("pageSize"), 20) if size < 1 || size > 200 { size = 20 } total := len(all) start := (page - 1) * size if start > total { start = total } end := start + size if end > total { end = total } ok(w, map[string]any{"list": all[start:end], "total": total, "page": page, "pageSize": size}) } } // syncDockPalletHandler 内部接口:MES 同步某接驳台是否有托盘(PLC 托盘传感器)。 func syncDockPalletHandler(ctx *svc.ServiceContext) http.HandlerFunc { return wrapInternal(ctx)(func(w http.ResponseWriter, r *http.Request) { var req struct { DockCode string `json:"dockCode"` HasPallet bool `json:"hasPallet"` PalletRef string `json:"palletRef"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } upd := ctx.EntClient.Dock.Update(). Where(dock.DockCode(req.DockCode)). SetHasPallet(req.HasPallet) if req.PalletRef != "" { upd = upd.SetPalletRef(req.PalletRef) } if err := upd.Exec(ctx0()); err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, nil) }) }