- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档 - WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景 - MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景 - 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景 - 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况 - 删除MES客户端中的ScanRecord相关代码,精简客户端结构 - 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
623 lines
22 KiB
Go
623 lines
22 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"bj_power_wms/ent"
|
||
"bj_power_wms/ent/inventory"
|
||
"bj_power_wms/ent/zone"
|
||
"bj_power_wms/internal/svc"
|
||
)
|
||
|
||
// 层级常量:区域(level1) → 货架(level2) → 层(level3) → 位置号(level4)
|
||
const (
|
||
zoneLevelRegion = 1
|
||
zoneLevelShelf = 2
|
||
zoneLevelLayer = 3
|
||
zoneLevelPos = 4
|
||
)
|
||
|
||
func zoneLevelName(l int) string {
|
||
switch l {
|
||
case 1:
|
||
return "区域"
|
||
case 2:
|
||
return "货架"
|
||
case 3:
|
||
return "层"
|
||
case 4:
|
||
return "位置号"
|
||
}
|
||
return "节点"
|
||
}
|
||
|
||
// zoneNodeVO 层级节点视图对象(列表/下拉共用)
|
||
type zoneNodeVO struct {
|
||
ID int `json:"id"`
|
||
Level int `json:"level"`
|
||
Code string `json:"code"`
|
||
Name string `json:"name"`
|
||
ParentCode string `json:"parentCode"`
|
||
ZoneCode string `json:"zoneCode"`
|
||
ZoneName string `json:"zoneName"`
|
||
ShelfNo string `json:"shelfNo"`
|
||
LayerNo string `json:"layerNo"`
|
||
PositionNo string `json:"positionNo"`
|
||
Status string `json:"status"`
|
||
Description string `json:"description"`
|
||
CreatedAt int64 `json:"createdAt"`
|
||
// StockQty 本级实时库存总量(需求E/需求7):每次查询实时聚合 inventories(quantity>0 且非出库/报废),不缓存。
|
||
// level1=区域合计、level2=货架合计、level3=层合计、level4=该位置合计。
|
||
StockQty int `json:"stockQty"`
|
||
}
|
||
|
||
func toZoneNodeVO(z *ent.Zone) zoneNodeVO {
|
||
return zoneNodeVO{
|
||
ID: z.ID, Level: z.Level, Code: z.Code, Name: z.Name, ParentCode: z.ParentCode,
|
||
ZoneCode: z.ZoneCode, ZoneName: z.ZoneName, ShelfNo: z.ShelfNo, LayerNo: z.LayerNo,
|
||
PositionNo: z.PositionNo, Status: z.Status, Description: z.Description, CreatedAt: z.CreatedAt,
|
||
}
|
||
}
|
||
|
||
// listZonesHandler 按层级列出节点(4 个 Tab 各自独立分页 + 按上级筛选)
|
||
// 参数:level(必填1~4)、parentCode、zoneCode、shelfNo、layerNo、code、name、status、page、pageSize
|
||
func listZonesHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
level := atoi(r.URL.Query().Get("level"), 0)
|
||
if level < 1 || level > 4 {
|
||
fail(w, http.StatusBadRequest, "level 必须为 1~4")
|
||
return
|
||
}
|
||
page := atoi(r.URL.Query().Get("page"), 1)
|
||
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
|
||
if page <= 0 {
|
||
page = 1
|
||
}
|
||
if pageSize <= 0 {
|
||
pageSize = 20
|
||
}
|
||
if pageSize > 200 {
|
||
pageSize = 200
|
||
}
|
||
|
||
q := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(level))
|
||
if pc := strings.TrimSpace(r.URL.Query().Get("parentCode")); pc != "" {
|
||
q = q.Where(zone.ParentCodeEQ(pc))
|
||
}
|
||
if zc := strings.TrimSpace(r.URL.Query().Get("zoneCode")); zc != "" {
|
||
q = q.Where(zone.ZoneCodeEQ(strings.ToUpper(zc)))
|
||
}
|
||
if sh := strings.TrimSpace(r.URL.Query().Get("shelfNo")); sh != "" {
|
||
q = q.Where(zone.ShelfNoEQ(sh))
|
||
}
|
||
if ly := strings.TrimSpace(r.URL.Query().Get("layerNo")); ly != "" {
|
||
q = q.Where(zone.LayerNoEQ(ly))
|
||
}
|
||
// 列表页禁止关键字混搜:本节点编码、名称各自独立模糊筛选(需求N)
|
||
if code := strings.TrimSpace(r.URL.Query().Get("code")); code != "" {
|
||
q = q.Where(zone.CodeContainsFold(code))
|
||
}
|
||
if name := strings.TrimSpace(r.URL.Query().Get("name")); name != "" {
|
||
q = q.Where(zone.NameContainsFold(name))
|
||
}
|
||
if st := strings.TrimSpace(r.URL.Query().Get("status")); st != "" {
|
||
q = q.Where(zone.StatusEQ(st))
|
||
}
|
||
|
||
total, err := q.Count(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
rows, err := q.Order(ent.Asc("code"), ent.Asc("id")).
|
||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
list := make([]zoneNodeVO, 0, len(rows))
|
||
byZone, byShelf, byLayer, byPos := zoneStockMaps(ctx)
|
||
for _, z := range rows {
|
||
vo := toZoneNodeVO(z)
|
||
zc := strings.ToLower(strings.TrimSpace(z.ZoneCode))
|
||
sh := strings.ToLower(strings.TrimSpace(z.ShelfNo))
|
||
ly := strings.TrimSpace(z.LayerNo)
|
||
switch z.Level {
|
||
case zoneLevelRegion:
|
||
vo.StockQty = byZone[zc]
|
||
case zoneLevelShelf:
|
||
vo.StockQty = byShelf[zc+"|"+sh]
|
||
case zoneLevelLayer:
|
||
vo.StockQty = byLayer[zc+"|"+sh+"|"+ly]
|
||
case zoneLevelPos:
|
||
vo.StockQty = byPos[locKey(z.ZoneCode, z.ShelfNo, z.LayerNo, z.PositionNo)]
|
||
}
|
||
list = append(list, vo)
|
||
}
|
||
ok(w, map[string]any{"total": total, "list": list, "page": page, "pageSize": pageSize})
|
||
}
|
||
}
|
||
|
||
// zoneStockMaps 实时聚合各级库存总量(需求E):一次扫描在库 inventories(quantity>0 且 status 非出库/报废),
|
||
// 分别按 区域 / 区域+货架 / 区域+货架+层 / 完整四级货位 累加,供四级列表展示实时库存。
|
||
// 键归一与 locKey 一致:区域/货架小写,层/位置原样 trim。
|
||
func zoneStockMaps(ctx *svc.ServiceContext) (byZone, byShelf, byLayer, byPos map[string]int) {
|
||
byZone, byShelf, byLayer, byPos = map[string]int{}, map[string]int{}, map[string]int{}, map[string]int{}
|
||
invs, err := ctx.EntClient.Inventory.Query().
|
||
Where(inventory.QuantityGT(0), inventory.StatusNotIn("出库", "报废")).All(ctx0())
|
||
if err != nil {
|
||
return
|
||
}
|
||
for _, iv := range invs {
|
||
zc := strings.ToLower(strings.TrimSpace(iv.ZoneCode))
|
||
sh := strings.ToLower(strings.TrimSpace(iv.ShelfNo))
|
||
ly := strings.TrimSpace(iv.LayerNo)
|
||
byZone[zc] += iv.Quantity
|
||
byShelf[zc+"|"+sh] += iv.Quantity
|
||
byLayer[zc+"|"+sh+"|"+ly] += iv.Quantity
|
||
byPos[locKey(iv.ZoneCode, iv.ShelfNo, iv.LayerNo, iv.PositionNo)] += iv.Quantity
|
||
}
|
||
return
|
||
}
|
||
|
||
// zoneNodeReq 新建层级节点请求
|
||
type zoneNodeReq struct {
|
||
Level int `json:"level"`
|
||
Code string `json:"code"`
|
||
Name string `json:"name"`
|
||
ParentCode string `json:"parentCode"`
|
||
Remark string `json:"remark"`
|
||
}
|
||
|
||
// createZoneNodeHandler 新建层级节点(按 level 解析上级并写冗余路径;建货架自动建默认位置)
|
||
func createZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req zoneNodeReq
|
||
if err := parseJSON(r, &req); err != nil {
|
||
fail(w, http.StatusBadRequest, "参数错误")
|
||
return
|
||
}
|
||
req.Code = strings.TrimSpace(req.Code)
|
||
req.Name = strings.TrimSpace(req.Name)
|
||
req.ParentCode = strings.TrimSpace(req.ParentCode)
|
||
req.Remark = strings.TrimSpace(req.Remark)
|
||
if req.Level < 1 || req.Level > 4 {
|
||
fail(w, http.StatusBadRequest, "level 必须为 1~4")
|
||
return
|
||
}
|
||
if req.Code == "" {
|
||
fail(w, http.StatusBadRequest, "编码必填")
|
||
return
|
||
}
|
||
if req.Level == 1 {
|
||
req.Code = strings.ToUpper(req.Code)
|
||
if req.Name == "" {
|
||
fail(w, http.StatusBadRequest, "区域名称必填")
|
||
return
|
||
}
|
||
}
|
||
|
||
var zoneCode, shelfNo, layerNo, parentCode string
|
||
parentCode = req.ParentCode
|
||
switch req.Level {
|
||
case 1:
|
||
zoneCode = req.Code
|
||
parentCode = ""
|
||
case 2:
|
||
if req.ParentCode == "" {
|
||
fail(w, http.StatusBadRequest, "请选择所属区域")
|
||
return
|
||
}
|
||
reg, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(1), zone.CodeEQ(req.ParentCode)).Only(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "所属区域不存在:"+req.ParentCode)
|
||
return
|
||
}
|
||
zoneCode = reg.ZoneCode
|
||
shelfNo = req.Code
|
||
case 3:
|
||
if req.ParentCode == "" {
|
||
fail(w, http.StatusBadRequest, "请选择所属货架")
|
||
return
|
||
}
|
||
sh, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(2), zone.CodeEQ(req.ParentCode)).Only(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "所属货架不存在:"+req.ParentCode)
|
||
return
|
||
}
|
||
zoneCode = sh.ZoneCode
|
||
shelfNo = sh.ShelfNo
|
||
layerNo = req.Code
|
||
case 4:
|
||
if req.ParentCode == "" {
|
||
fail(w, http.StatusBadRequest, "请选择所属层或货架(默认位置)")
|
||
return
|
||
}
|
||
parent, err := ctx.EntClient.Zone.Query().
|
||
Where(zone.CodeEQ(req.ParentCode)).Where(zone.Or(zone.LevelEQ(3), zone.LevelEQ(2))).Only(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "所属层/货架不存在:"+req.ParentCode)
|
||
return
|
||
}
|
||
zoneCode = parent.ZoneCode
|
||
shelfNo = parent.ShelfNo
|
||
if parent.Level == 3 {
|
||
layerNo = parent.Code
|
||
} else {
|
||
layerNo = "" // 默认位置:无层
|
||
}
|
||
}
|
||
|
||
// 唯一性:同层级 + 同区域 + 同货架 + 同层 + 同编码 不可重复
|
||
dup, _ := ctx.EntClient.Zone.Query().
|
||
Where(zone.LevelEQ(req.Level), zone.ZoneCodeEQ(zoneCode),
|
||
zone.ShelfNoEQ(shelfNo), zone.LayerNoEQ(layerNo), zone.CodeEQ(req.Code)).
|
||
Exist(ctx0())
|
||
if dup {
|
||
fail(w, http.StatusConflict, "该编码已存在:"+req.Code)
|
||
return
|
||
}
|
||
|
||
create := ctx.EntClient.Zone.Create().
|
||
SetLevel(req.Level).SetCode(req.Code).SetParentCode(parentCode).
|
||
SetZoneCode(zoneCode).SetNillableShelfNo(strPtr(shelfNo)).
|
||
SetNillableLayerNo(strPtr(layerNo)).SetNillablePositionNo(strPtr(req.Code)).
|
||
SetStatus("启用").SetNillableDescription(strPtr(req.Remark)).
|
||
SetName(req.Name) // 四级均可有名称(需求E)
|
||
if req.Level == 1 {
|
||
create.SetZoneName(req.Name) // 区域名称冗余,兼容 stock.go 区域名映射
|
||
}
|
||
z, err := create.Save(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
|
||
// 建货架(level2)时自动创建一个「默认位置」(level4,挂在货架下、无层),物料可不细分时挂这里
|
||
if req.Level == 2 {
|
||
_, _ = ctx.EntClient.Zone.Create().
|
||
SetLevel(4).SetCode("默认").SetParentCode(req.Code).
|
||
SetZoneCode(zoneCode).SetShelfNo(shelfNo).SetNillableLayerNo(strPtr("")).
|
||
SetPositionNo("默认").SetName("默认位置").SetStatus("启用").
|
||
Save(ctx0())
|
||
}
|
||
|
||
ctx.EventLog.Write(ctx0(), "zone.create", r.Header.Get("X-Username"), "zone", req.Code,
|
||
"新建"+zoneLevelName(req.Level)+" "+req.Code, nil)
|
||
ok(w, toZoneNodeVO(z))
|
||
}
|
||
}
|
||
|
||
type zoneNodeUpdateReq struct {
|
||
ID int `json:"id"`
|
||
Name string `json:"name"`
|
||
Remark string `json:"remark"`
|
||
Status string `json:"status"`
|
||
}
|
||
|
||
// updateZoneNodeHandler 编辑节点(四级均可改名称/备注/启用停用;区域额外同步 zone_name 冗余)。
|
||
// 需求E:不删只停用,无删除入口;名称与备注全级可维护。
|
||
func updateZoneNodeHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req zoneNodeUpdateReq
|
||
if err := parseJSON(r, &req); err != nil {
|
||
fail(w, http.StatusBadRequest, "参数错误")
|
||
return
|
||
}
|
||
if req.ID <= 0 {
|
||
fail(w, http.StatusBadRequest, "id 必填")
|
||
return
|
||
}
|
||
z, err := ctx.EntClient.Zone.Get(ctx0(), req.ID)
|
||
if err != nil {
|
||
fail(w, http.StatusNotFound, "节点不存在")
|
||
return
|
||
}
|
||
status := req.Status
|
||
if status == "" {
|
||
status = z.Status
|
||
}
|
||
if status != "启用" && status != "停用" {
|
||
fail(w, http.StatusBadRequest, "状态取值非法")
|
||
return
|
||
}
|
||
upd := ctx.EntClient.Zone.UpdateOneID(req.ID).SetStatus(status).
|
||
SetName(strings.TrimSpace(req.Name)).
|
||
SetNillableDescription(strPtr(strings.TrimSpace(req.Remark)))
|
||
if z.Level == 1 {
|
||
upd.SetZoneName(strings.TrimSpace(req.Name)) // 区域名称冗余同步
|
||
}
|
||
if _, err := upd.Save(ctx0()); err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
ctx.EventLog.Write(ctx0(), "zone.update", r.Header.Get("X-Username"), "zone", z.Code,
|
||
"编辑"+zoneLevelName(z.Level)+" "+z.Code, nil)
|
||
ok(w, map[string]any{"id": req.ID})
|
||
}
|
||
}
|
||
|
||
// listZonesForPicker 库位下拉(兼容 + 层级两用):
|
||
// - 不带 level:返回所有启用位置号(level4)扁平结构(兼容 Outbound/Stocktake/Inventory 旧调用)
|
||
// - 带 level:返回该层级节点(用于入库页区域→货架→层→位置 级联下拉)
|
||
//
|
||
// locKey 四级货位比较键:区域/货架忽略大小写(存量数据存在 z01/Z01 混用),层/位置原样。
|
||
func locKey(zoneCode, shelfNo, layerNo, positionNo string) string {
|
||
return strings.ToLower(strings.TrimSpace(zoneCode)) + "|" +
|
||
strings.ToLower(strings.TrimSpace(shelfNo)) + "|" +
|
||
strings.TrimSpace(layerNo) + "|" + strings.TrimSpace(positionNo)
|
||
}
|
||
|
||
// vacantZoneHandler 同区空位推荐:返回指定区域(不传 zoneCode 则不限区域)内「未被库存占用」的位置节点(level=4)。
|
||
// 用途:入库选中物料后,若该物料无历史库位(新物料),自动推荐同区空位,替代人工逐个挑选货架/层/位置。
|
||
// 占用判定:inventories 中 quantity>0 且 status 非「出库/报废」的记录,其四级货位视为已占用。
|
||
func vacantZoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
wantZone := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("zoneCode")))
|
||
limit := atoi(r.URL.Query().Get("limit"), 20)
|
||
if limit <= 0 || limit > 200 {
|
||
limit = 20
|
||
}
|
||
rows, err := ctx.EntClient.Zone.Query().
|
||
Where(zone.LevelEQ(zoneLevelPos), zone.StatusEQ("启用")).
|
||
Order(ent.Asc("zone_code"), ent.Asc("shelf_no"), ent.Asc("layer_no"), ent.Asc("code")).
|
||
All(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
occupied := map[string]bool{}
|
||
if invs, ierr := ctx.EntClient.Inventory.Query().
|
||
Where(inventory.QuantityGT(0), inventory.StatusNotIn("出库", "报废")).
|
||
All(ctx0()); ierr == nil {
|
||
for _, iv := range invs {
|
||
occupied[locKey(iv.ZoneCode, iv.ShelfNo, iv.LayerNo, iv.PositionNo)] = true
|
||
}
|
||
}
|
||
list := make([]map[string]any, 0, limit)
|
||
for _, z := range rows {
|
||
if wantZone != "" && strings.ToLower(z.ZoneCode) != wantZone {
|
||
continue
|
||
}
|
||
if occupied[locKey(z.ZoneCode, z.ShelfNo, z.LayerNo, z.PositionNo)] {
|
||
continue
|
||
}
|
||
list = append(list, map[string]any{
|
||
"zoneCode": z.ZoneCode, "zoneName": z.ZoneName, "shelfNo": z.ShelfNo,
|
||
"layerNo": z.LayerNo, "positionNo": z.PositionNo, "name": z.Name, "code": z.Code,
|
||
})
|
||
if len(list) >= limit {
|
||
break
|
||
}
|
||
}
|
||
ok(w, list)
|
||
}
|
||
}
|
||
|
||
func listZonesForPicker(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
level := atoi(r.URL.Query().Get("level"), 0)
|
||
status := strings.TrimSpace(r.URL.Query().Get("status"))
|
||
if level == 0 {
|
||
q := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(4))
|
||
if status == "" {
|
||
q = q.Where(zone.StatusEQ("启用"))
|
||
} else {
|
||
q = q.Where(zone.StatusEQ(status))
|
||
}
|
||
rows, err := q.Order(ent.Asc("code")).All(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
list := make([]map[string]any, 0, len(rows))
|
||
for _, z := range rows {
|
||
list = append(list, map[string]any{
|
||
"zoneCode": z.ZoneCode, "zoneName": z.ZoneName, "shelfNo": z.ShelfNo,
|
||
"layerNo": z.LayerNo, "positionNo": z.PositionNo, "name": z.Name, "code": z.Code,
|
||
})
|
||
}
|
||
ok(w, list)
|
||
return
|
||
}
|
||
q := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(level))
|
||
if pc := strings.TrimSpace(r.URL.Query().Get("parentCode")); pc != "" {
|
||
q = q.Where(zone.ParentCodeEQ(pc))
|
||
}
|
||
if zc := strings.TrimSpace(r.URL.Query().Get("zoneCode")); zc != "" {
|
||
q = q.Where(zone.ZoneCodeEQ(strings.ToUpper(zc)))
|
||
}
|
||
if sh := strings.TrimSpace(r.URL.Query().Get("shelfNo")); sh != "" {
|
||
q = q.Where(zone.ShelfNoEQ(sh))
|
||
}
|
||
if ly := strings.TrimSpace(r.URL.Query().Get("layerNo")); ly != "" {
|
||
q = q.Where(zone.LayerNoEQ(ly))
|
||
}
|
||
if status == "" {
|
||
q = q.Where(zone.StatusEQ("启用"))
|
||
} else {
|
||
q = q.Where(zone.StatusEQ(status))
|
||
}
|
||
rows, err := q.Order(ent.Asc("code")).All(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
list := make([]map[string]any, 0, len(rows))
|
||
for _, z := range rows {
|
||
list = append(list, map[string]any{
|
||
"id": z.ID, "level": z.Level, "code": z.Code, "name": z.Name,
|
||
"parentCode": z.ParentCode, "zoneCode": z.ZoneCode, "shelfNo": z.ShelfNo, "layerNo": z.LayerNo,
|
||
})
|
||
}
|
||
ok(w, list)
|
||
}
|
||
}
|
||
|
||
type zoneBatchGenReq struct {
|
||
ZoneCode string `json:"zoneCode"`
|
||
ShelfNo string `json:"shelfNo"`
|
||
LayerStart int `json:"layerStart"`
|
||
LayerEnd int `json:"layerEnd"`
|
||
PosStart int `json:"posStart"`
|
||
PosEnd int `json:"posEnd"`
|
||
Level int `json:"level"` // 3=批量生成层(level3) 4=批量生成位置号(level4,默认,同时确保层存在)
|
||
Preview bool `json:"preview"`
|
||
}
|
||
|
||
// batchGenerateHandler 批量生成库位节点:选区域→货架→范围,先预览后提交。
|
||
// level=3:只生成「层」节点(按层范围);level=4(默认):生成「位置号」节点(层×位置),并自动确保层节点存在。
|
||
// 已存在的节点跳过不重复建。
|
||
func batchGenerateHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req zoneBatchGenReq
|
||
if err := parseJSON(r, &req); err != nil {
|
||
fail(w, http.StatusBadRequest, "参数错误")
|
||
return
|
||
}
|
||
req.ZoneCode = strings.ToUpper(strings.TrimSpace(req.ZoneCode))
|
||
req.ShelfNo = strings.TrimSpace(req.ShelfNo)
|
||
if req.ZoneCode == "" || req.ShelfNo == "" {
|
||
fail(w, http.StatusBadRequest, "请选择区域与货架")
|
||
return
|
||
}
|
||
level := req.Level
|
||
if level != 3 {
|
||
level = 4
|
||
}
|
||
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart {
|
||
fail(w, http.StatusBadRequest, "层起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||
return
|
||
}
|
||
if level == 4 && (req.PosStart < 0 || req.PosEnd < req.PosStart) {
|
||
fail(w, http.StatusBadRequest, "位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||
return
|
||
}
|
||
sh, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(2), zone.ZoneCodeEQ(req.ZoneCode), zone.CodeEQ(req.ShelfNo)).Only(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "货架不存在:"+req.ZoneCode+"/"+req.ShelfNo)
|
||
return
|
||
}
|
||
zoneCode := sh.ZoneCode
|
||
|
||
layers := make([]int, 0)
|
||
for l := req.LayerStart; l <= req.LayerEnd; l++ {
|
||
layers = append(layers, l)
|
||
}
|
||
|
||
// ---------- level=3:批量生成层节点 ----------
|
||
if level == 3 {
|
||
if len(layers) > 2000 {
|
||
fail(w, http.StatusBadRequest, "一次最多生成 2000 个层,请缩小范围")
|
||
return
|
||
}
|
||
type genLayer struct {
|
||
LayerNo string `json:"layerNo"`
|
||
LayerName string `json:"layerName"`
|
||
Exists bool `json:"exists"`
|
||
}
|
||
layerList := make([]genLayer, 0, len(layers))
|
||
for _, l := range layers {
|
||
ls := strconv.Itoa(l)
|
||
exists, _ := ctx.EntClient.Zone.Query().
|
||
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(ls)).Exist(ctx0())
|
||
layerList = append(layerList, genLayer{LayerNo: ls, LayerName: req.ShelfNo + "架" + ls + "层", Exists: exists})
|
||
}
|
||
if req.Preview {
|
||
ok(w, map[string]any{"preview": true, "level": 3, "list": layerList, "total": len(layerList)})
|
||
return
|
||
}
|
||
createdL, skippedL := 0, 0
|
||
for _, it := range layerList {
|
||
if it.Exists {
|
||
skippedL++
|
||
continue
|
||
}
|
||
if _, err := ctx.EntClient.Zone.Create().
|
||
SetLevel(3).SetCode(it.LayerNo).SetParentCode(req.ShelfNo).
|
||
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(it.LayerNo).
|
||
SetName(it.LayerName).SetStatus("启用").Save(ctx0()); err != nil {
|
||
fail(w, http.StatusInternalServerError, "创建层失败: "+err.Error())
|
||
return
|
||
}
|
||
createdL++
|
||
}
|
||
ctx.EventLog.Write(ctx0(), "zone.batch", r.Header.Get("X-Username"), "zone", req.ShelfNo,
|
||
"批量生成层 "+req.ZoneCode+"/"+req.ShelfNo+" 层"+strconv.Itoa(req.LayerStart)+"-"+strconv.Itoa(req.LayerEnd)+
|
||
":新建"+strconv.Itoa(createdL)+" 跳过"+strconv.Itoa(skippedL), nil)
|
||
ok(w, map[string]any{"preview": false, "level": 3, "created": createdL, "skipped": skippedL, "total": len(layers)})
|
||
return
|
||
}
|
||
|
||
// ---------- level=4:批量生成位置号 ----------
|
||
poss := make([]int, 0)
|
||
for p := req.PosStart; p <= req.PosEnd; p++ {
|
||
poss = append(poss, p)
|
||
}
|
||
if len(layers)*len(poss) > 2000 {
|
||
fail(w, http.StatusBadRequest, "一次最多生成 2000 个位置,请缩小范围")
|
||
return
|
||
}
|
||
|
||
// 预览:返回将生成的位置清单(含是否已存在)
|
||
type genItem struct {
|
||
LayerNo string `json:"layerNo"`
|
||
PositionNo string `json:"positionNo"`
|
||
PositionName string `json:"positionName"`
|
||
Exists bool `json:"exists"`
|
||
}
|
||
preview := make([]genItem, 0, len(layers)*len(poss))
|
||
for _, l := range layers {
|
||
ls := strconv.Itoa(l)
|
||
for _, p := range poss {
|
||
ps := strconv.Itoa(p)
|
||
exists, _ := ctx.EntClient.Zone.Query().
|
||
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
|
||
zone.LayerNoEQ(ls), zone.CodeEQ(ps)).Exist(ctx0())
|
||
preview = append(preview, genItem{LayerNo: ls, PositionNo: ps, PositionName: req.ShelfNo + "架" + ls + "层" + ps + "位", Exists: exists})
|
||
}
|
||
}
|
||
if req.Preview {
|
||
ok(w, map[string]any{"preview": true, "list": preview, "total": len(preview)})
|
||
return
|
||
}
|
||
|
||
// 提交:确保层节点存在 + 创建不存在的位置号
|
||
created, skipped := 0, 0
|
||
for _, l := range layers {
|
||
ls := strconv.Itoa(l)
|
||
if exist, _ := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(ls)).Exist(ctx0()); !exist {
|
||
if _, err := ctx.EntClient.Zone.Create().
|
||
SetLevel(3).SetCode(ls).SetParentCode(req.ShelfNo).
|
||
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(ls).SetStatus("启用").Save(ctx0()); err != nil {
|
||
fail(w, http.StatusInternalServerError, "创建层失败: "+err.Error())
|
||
return
|
||
}
|
||
}
|
||
for _, p := range poss {
|
||
ps := strconv.Itoa(p)
|
||
if exist, _ := ctx.EntClient.Zone.Query().
|
||
Where(zone.LevelEQ(4), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo),
|
||
zone.LayerNoEQ(ls), zone.CodeEQ(ps)).Exist(ctx0()); exist {
|
||
skipped++
|
||
continue
|
||
}
|
||
if _, err := ctx.EntClient.Zone.Create().
|
||
SetLevel(4).SetCode(ps).SetParentCode(ls).
|
||
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(ls).SetPositionNo(ps).
|
||
SetName(req.ShelfNo + "架" + ls + "层" + ps + "位").SetStatus("启用").Save(ctx0()); err != nil {
|
||
fail(w, http.StatusInternalServerError, "创建位置失败: "+err.Error())
|
||
return
|
||
}
|
||
created++
|
||
}
|
||
}
|
||
ctx.EventLog.Write(ctx0(), "zone.batch", r.Header.Get("X-Username"), "zone", req.ShelfNo,
|
||
"批量生成位置 "+req.ZoneCode+"/"+req.ShelfNo+" 层"+strconv.Itoa(req.LayerStart)+"-"+strconv.Itoa(req.LayerEnd)+
|
||
" 位"+strconv.Itoa(req.PosStart)+"-"+strconv.Itoa(req.PosEnd)+":新建"+strconv.Itoa(created)+" 跳过"+strconv.Itoa(skipped), nil)
|
||
ok(w, map[string]any{"preview": false, "created": created, "skipped": skipped, "total": len(layers) * len(poss)})
|
||
}
|
||
}
|