docs(deployment): 更新部署手册和业务架构说明

- 更新MES和WMS服务端口配置说明
- 详细说明产线工位设备配置(扫码枪+拧紧枪)
- 明确AGV配送和接驳台的模拟模式与真实对接方案
- 更新WMS中AGV配送默认模拟模式说明
- 完善厂内物流(AGV/接驳台)的业务架构约束
- 修订临时文档为详细的项目问题和解决方案规划
This commit is contained in:
SunYF
2026-09-20 14:52:36 +08:00
parent 5a33028003
commit f35e9b5b75
57 changed files with 6096 additions and 1000 deletions
+35 -155
View File
@@ -1,7 +1,6 @@
package handler
import (
"errors"
"net/http"
"strings"
@@ -76,8 +75,8 @@ func dockMaintRowOf(d *ent.Dock) dockMaintRow {
}
}
// listDocksMaintHandler GET /api/docks?dockType=&keyword=&page=&pageSize=
// 接驳台主数据分页查询(真分页在 SQL 层),默认按创建时间倒序
// listDocksMaintHandler GET /api/docks?dockType=&dockCode=&name=&status=&page=&pageSize=
// 接驳台主数据只读分页查询(真分页在 SQL 层),编码精确/名称模糊/状态精确各自独立筛选
func listDocksMaintHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
@@ -85,11 +84,15 @@ func listDocksMaintHandler(ctx *svc.ServiceContext) http.HandlerFunc {
if typ := q.Get("dockType"); typ != "" {
query = query.Where(dock.DockType(typ))
}
if kw := strings.TrimSpace(q.Get("keyword")); kw != "" {
query = query.Where(dock.Or(
dock.DockCodeContainsFold(kw),
dock.NameContainsFold(kw),
))
// 列表页禁止关键字混搜:编码精确、名称模糊、状态精确,各字段独立筛选(需求5、N)
if code := strings.TrimSpace(q.Get("dockCode")); code != "" {
query = query.Where(dock.DockCodeEqualFold(code))
}
if name := strings.TrimSpace(q.Get("name")); name != "" {
query = query.Where(dock.NameContainsFold(name))
}
if status := strings.TrimSpace(q.Get("status")); status != "" {
query = query.Where(dock.StatusEQ(status))
}
total, err := query.Count(ctx0())
if err != nil {
@@ -119,159 +122,36 @@ func listDocksMaintHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// dockReq 接驳台保存请求
type dockReq struct {
ID int64 `json:"id"`
DockCode string `json:"dockCode"`
Name string `json:"name"`
DockType string `json:"dockType"`
// 接驳台为系统预置只读主数据(需求5):不提供新增/修改/删除,仅保留查询。
// 原 createDockHandler / updateDockHandler / deleteDockHandler / validateDock / dockReq 已彻底移除,
// 接驳台数据由 db/seed.go 种子初始化,调整由实施维护种子或数据库完成。
// stationOption 出库目标工位下拉项(需求20:目标工位可下拉选择、不允许手输)。
// 客户口径「出库工位号来自 MES 工位列表」——WMS 侧以接驳台主数据中 line 类型
// (产线接驳台与工位 1:1 绑定)为真源派生工位选项,避免前端写死 1~20。
type stationOption struct {
StationNo int `json:"stationNo"`
Status string `json:"status"`
Name string `json:"name"`
DockCode string `json:"dockCode"`
}
// validateDock 校验接驳台字段:
// - 编码/名称必填,编码全库唯一;
// - 类型仅 line/store/other
// - 产线接驳台必须绑定工位号(>0)且与工位 1:1;库房/其他接驳台不绑定工位(固定 0)。
func validateDock(ctx *svc.ServiceContext, req *dockReq, selfID int64) error {
req.DockCode = strings.TrimSpace(req.DockCode)
req.Name = strings.TrimSpace(req.Name)
if req.DockCode == "" {
return errors.New("接驳台编码必填")
}
if req.Name == "" {
return errors.New("接驳台名称必填")
}
switch req.DockType {
case "line", "store", "other":
default:
return errors.New("接驳台类型仅支持 产线/库房/其他")
}
if req.Status != "ENABLED" && req.Status != "DISABLED" {
return errors.New("接驳台状态仅支持 启用/停用")
}
if req.DockType != "line" {
req.StationNo = 0
} else if req.StationNo <= 0 {
return errors.New("产线接驳台必须绑定工位号")
}
dupCode := ctx.EntClient.Dock.Query().Where(dock.DockCode(req.DockCode))
if selfID > 0 {
dupCode = dupCode.Where(dock.IDNEQ(int(selfID)))
}
if exists, err := dupCode.Exist(ctx0()); err == nil && exists {
return errors.New("接驳台编码已存在:" + req.DockCode)
}
if req.StationNo > 0 {
q := ctx.EntClient.Dock.Query().Where(dock.StationNo(req.StationNo))
if selfID > 0 {
q = q.Where(dock.IDNEQ(int(selfID)))
}
other, err := q.First(ctx0())
if err == nil && other != nil {
return errors.New("该工位已绑定接驳台 " + other.DockCode + ",一个工位只能有一个接驳台")
}
}
return nil
}
// createDockHandler POST /api/docks 新增接驳台
func createDockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
// listStationOptionsHandler GET /api/outbound/stations 返回产线工位下拉选项(无权限包裹,供出库页调用)。
func listStationOptionsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req dockReq
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.DockType == "" {
req.DockType = "line"
}
if req.Status == "" {
req.Status = "ENABLED"
}
if err := validateDock(ctx, &req, 0); err != nil {
fail(w, http.StatusBadRequest, err.Error())
return
}
_, err := ctx.EntClient.Dock.Create().
SetDockCode(req.DockCode).SetName(req.Name).SetDockType(req.DockType).
SetStationNo(req.StationNo).SetHasPallet(false).SetStatus(req.Status).
Save(ctx0())
list, err := ctx.EntClient.Dock.Query().
Where(dock.DockType("line")).
Order(ent.Asc("station_no")).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "新增接驳台失败: "+err.Error())
fail(w, http.StatusInternalServerError, "查询工位失败: "+err.Error())
return
}
ctx.EventLog.Write(ctx0(), "dock.create", r.Header.Get("X-Username"), "dock", req.DockCode,
"新增接驳台", map[string]any{"dockCode": req.DockCode, "dockType": req.DockType, "stationNo": req.StationNo})
ok(w, map[string]any{"message": "接驳台已新增"})
}
}
// updateDockHandler POST /api/docks/update 修改接驳台(编码不可改,是产线/AGV 任务的引用锚点)
func updateDockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req dockReq
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.ID <= 0 {
fail(w, http.StatusBadRequest, "缺少 id")
return
}
old, err := ctx.EntClient.Dock.Get(ctx0(), int(req.ID))
if err != nil {
fail(w, http.StatusBadRequest, "接驳台不存在")
return
}
req.DockCode = old.DockCode
if err := validateDock(ctx, &req, req.ID); err != nil {
fail(w, http.StatusBadRequest, err.Error())
return
}
_, err = old.Update().
SetName(req.Name).SetDockType(req.DockType).
SetStationNo(req.StationNo).SetStatus(req.Status).Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "修改接驳台失败: "+err.Error())
return
}
ctx.EventLog.Write(ctx0(), "dock.update", r.Header.Get("X-Username"), "dock", old.DockCode,
"修改接驳台", map[string]any{"dockCode": old.DockCode, "dockType": req.DockType, "stationNo": req.StationNo})
ok(w, map[string]any{"message": "接驳台已保存"})
}
}
// deleteDockHandler POST /api/docks/delete 删除接驳台
// 正在占用(有托盘)的接驳台不允许删除,避免 AGV/产线任务指向已删除的接驳台。
func deleteDockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID int64 `json:"id"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.ID <= 0 {
fail(w, http.StatusBadRequest, "缺少 id")
return
}
d, err := ctx.EntClient.Dock.Get(ctx0(), int(req.ID))
if err != nil {
fail(w, http.StatusBadRequest, "接驳台不存在")
return
}
if d.HasPallet {
fail(w, http.StatusBadRequest, "接驳台 "+d.DockCode+" 当前有托盘占用,请先处理托盘后再删除")
return
}
if err := ctx.EntClient.Dock.DeleteOneID(d.ID).Exec(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "删除接驳台失败: "+err.Error())
return
}
ctx.EventLog.Write(ctx0(), "dock.delete", r.Header.Get("X-Username"), "dock", d.DockCode,
"删除接驳台", map[string]any{"dockCode": d.DockCode})
ok(w, map[string]any{"message": "接驳台已删除"})
rows := make([]stationOption, 0, len(list))
for _, d := range list {
if d.StationNo <= 0 {
continue
}
rows = append(rows, stationOption{StationNo: d.StationNo, Name: d.Name, DockCode: d.DockCode})
}
ok(w, map[string]any{"list": rows})
}
}