Files
bj_power/bj_power_wms/internal/handler/agv.go
T
SunYF b6799c9de4 feat: 完成产线与仓储系统多模块迭代升级
本次迭代覆盖MES与WMS核心业务:
1. 新增接驳台托盘传感器读取与AGV对接能力
2. 完善工单排产、备料流程与权限体系拆分
3. 优化看板接口与前端路由、样式
4. 新增操作日志、库存盘点与角色保护逻辑
5. 修复代理地址、BOM保存等已知问题
2026-09-04 14:18:53 +08:00

296 lines
8.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
}