296 lines
8.6 KiB
Go
296 lines
8.6 KiB
Go
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)
|
|||
|
|
})
|
|||
|
|
}
|