Files
bj_power/bj_power_wms/internal/handler/agv.go
T

296 lines
8.6 KiB
Go
Raw Normal View History

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"`
// 也可以直接给一个 groupsourceDock + 多目标
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)
})
}