本次迭代覆盖MES与WMS核心业务: 1. 新增接驳台托盘传感器读取与AGV对接能力 2. 完善工单排产、备料流程与权限体系拆分 3. 优化看板接口与前端路由、样式 4. 新增操作日志、库存盘点与角色保护逻辑 5. 修复代理地址、BOM保存等已知问题
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)
|
||
})
|
||
} |