Files
bj_power/bj_power_wms/internal/handler/agv.go
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

371 lines
12 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/ent/outboundorder"
"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 备料单,避免三处状态互斗)。
if t.RelatedOutboundNo != "" {
ctx.EntClient.OutboundOrder.Update().
Where(outboundorder.OutboundNoEQ(t.RelatedOutboundNo)).
SetStatus("已送达").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"
}
}
// cancelAgvHandler 取消 AGV 任务(软取消)。
// 已下发(DISPATCHED/DELIVERING)的调海康 RCS task/cancel;本地任务置 CANCELLED
// 并把关联的 MES 备料单行从 DELIVERING 退回待发料状态。
func cancelAgvHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
TaskNo string `json:"taskNo"`
Reason string `json:"reason"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误: "+err.Error())
return
}
if req.TaskNo == "" {
fail(w, http.StatusBadRequest, "缺少任务号")
return
}
t, err := ctx.EntClient.AgvTask.Query().Where(agvtask.TaskNo(req.TaskNo)).First(ctx0())
if err != nil {
fail(w, http.StatusNotFound, "AGV 任务不存在")
return
}
if t.Status == "DONE" || t.Status == "CANCELLED" {
fail(w, http.StatusBadRequest, "任务已结束,无法取消")
return
}
operator := r.Header.Get("X-Username")
// 已下发的先通知 RCS 取消
if t.HikTaskCode != "" && (t.Status == "DISPATCHED" || t.Status == "DELIVERING") {
if cerr := ctx.Agv.CancelTask(ctx0(), t.HikTaskCode, "CANCEL", req.Reason); cerr != nil {
fail(w, http.StatusBadGateway, "RCS 取消失败: "+cerr.Error())
return
}
}
if err := ctx.EntClient.AgvTask.UpdateOneID(t.ID).
SetStatus("CANCELLED").SetRemark(req.Reason).Exec(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
// 关联备料单行退回待发料
mats := []agvSubmitRow{}
_ = json.Unmarshal([]byte(t.MaterialsJSON), &mats)
for _, row := range mats {
_ = ctx.Mes.MarkStatus(ctx0(), row.RequestNo, "WAIT_SEND")
}
ctx.EventLog.Write(ctx0(), "agv.cancel", operator, "agv_task", t.TaskNo,
"取消AGV搬运 "+t.SourceDock+"→"+t.TargetDock, map[string]any{"reason": req.Reason})
ok(w, nil)
}
}
// agvModeHandler 返回 AGV 对接模式(mock / 真实 RCS),供前端提示当前是否在模拟。
func agvModeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
mock := ctx.Agv.Disable()
ok(w, map[string]any{
"mock": mock,
"tip": func() string {
if mock {
return "当前为模拟模式(未接真实海康 RCS):任务会立即标记为已到位。现场对接请在配置中关闭 EnableMock 并填写 RCS 地址/appKey/appSecret。"
}
return "已对接真实海康 RCS。"
}(),
})
}
}
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)
})
}