- 将文档从代码审计任务转换为端到端业务回归测试链路 - 重新组织文档结构为链路总览、示例数据基线和详细步骤 - 添加完整的业务场景清单涵盖WMS/MES/工位终端三大系统 - 定义统一的测试数据包括产品型号、物料清单、工位派工等 - 提供详细的步骤断言和数量等式验证方法 - 建立贯穿全链路的数据流向和状态变更追踪体系
617 lines
23 KiB
Go
617 lines
23 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 新建层级节点请求。
|
||
// 父级以完整路径给出(区域→货架→层),后端据此精确定位父级,避免仅凭 code 跨父级歧义。
|
||
type zoneNodeReq struct {
|
||
Level int `json:"level"`
|
||
Code string `json:"code"`
|
||
Name string `json:"name"`
|
||
ZoneCode string `json:"zoneCode"` // 所属区域编码(level>=2 必填)
|
||
ShelfNo string `json:"shelfNo"` // 所属货架号(level>=3 必填)
|
||
LayerNo string `json:"layerNo"` // 所属层号(level=4 必填)
|
||
Remark string `json:"remark"`
|
||
}
|
||
|
||
// createZoneNodeHandler 新建层级节点(按完整路径 区域→货架→层 精确定位父级并写冗余路径)。
|
||
// 父级必须存在且不能为空;位置号(level4)必须挂在已存在的层下(不再有“无层默认位置”)。
|
||
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.ZoneCode = strings.ToUpper(strings.TrimSpace(req.ZoneCode))
|
||
req.ShelfNo = strings.TrimSpace(req.ShelfNo)
|
||
req.LayerNo = strings.TrimSpace(req.LayerNo)
|
||
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
|
||
}
|
||
}
|
||
|
||
// 逐级解析父级:父级必须已存在,路径(区域+货架+层)唯一确定,避免仅凭 code 跨父级歧义。
|
||
var zoneCode, shelfNo, layerNo, parentCode string
|
||
switch req.Level {
|
||
case 1:
|
||
zoneCode = req.Code
|
||
parentCode = ""
|
||
case 2:
|
||
if req.ZoneCode == "" {
|
||
fail(w, http.StatusBadRequest, "请选择所属区域")
|
||
return
|
||
}
|
||
reg, err := ctx.EntClient.Zone.Query().Where(zone.LevelEQ(1), zone.CodeEQ(req.ZoneCode)).Only(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "所属区域不存在:"+req.ZoneCode)
|
||
return
|
||
}
|
||
zoneCode = reg.ZoneCode
|
||
shelfNo = req.Code
|
||
parentCode = reg.Code
|
||
case 3:
|
||
if req.ZoneCode == "" || req.ShelfNo == "" {
|
||
fail(w, http.StatusBadRequest, "请选择所属区域与货架")
|
||
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
|
||
shelfNo = sh.ShelfNo
|
||
layerNo = req.Code
|
||
parentCode = sh.Code
|
||
case 4:
|
||
if req.ZoneCode == "" || req.ShelfNo == "" || req.LayerNo == "" {
|
||
fail(w, http.StatusBadRequest, "请选择所属层")
|
||
return
|
||
}
|
||
ly, err := ctx.EntClient.Zone.Query().
|
||
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(req.ZoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(req.LayerNo)).Only(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "所属层不存在:"+req.ZoneCode+"/"+req.ShelfNo+"/"+req.LayerNo)
|
||
return
|
||
}
|
||
zoneCode = ly.ZoneCode
|
||
shelfNo = ly.ShelfNo
|
||
layerNo = ly.Code
|
||
parentCode = ly.Code
|
||
}
|
||
|
||
// 唯一性:同层级 + 同区域 + 同货架 + 同层 + 同编码 不可重复
|
||
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
|
||
}
|
||
|
||
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"` // level=3 批量生成层:层范围起
|
||
LayerEnd int `json:"layerEnd"` // level=3 批量生成层:层范围止
|
||
LayerNo string `json:"layerNo"` // level=4 批量生成位置号:指定的已存在层(只在该层下生成位置,不生成父级)
|
||
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
|
||
}
|
||
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
|
||
|
||
// ---------- level=3:批量生成层节点(父级货架必须已存在,只生成层自己) ----------
|
||
if level == 3 {
|
||
if req.LayerStart < 0 || req.LayerEnd < req.LayerStart {
|
||
fail(w, http.StatusBadRequest, "层起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||
return
|
||
}
|
||
layers := make([]int, 0)
|
||
for l := req.LayerStart; l <= req.LayerEnd; l++ {
|
||
layers = append(layers, l)
|
||
}
|
||
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:批量生成位置号 ----------
|
||
// 只允许在「已存在的具体层」下批量生成位置号(批量生成只生成自己,不创建父级层)。
|
||
layerNo := strings.TrimSpace(req.LayerNo)
|
||
if layerNo == "" {
|
||
fail(w, http.StatusBadRequest, "请选择具体的层")
|
||
return
|
||
}
|
||
if req.PosStart < 0 || req.PosEnd < req.PosStart {
|
||
fail(w, http.StatusBadRequest, "位置起止不合法(起始需 ≤ 结束,且 ≥ 0)")
|
||
return
|
||
}
|
||
if exist, _ := ctx.EntClient.Zone.Query().
|
||
Where(zone.LevelEQ(3), zone.ZoneCodeEQ(zoneCode), zone.ShelfNoEQ(req.ShelfNo), zone.CodeEQ(layerNo)).Exist(ctx0()); !exist {
|
||
fail(w, http.StatusBadRequest, "层不存在:"+req.ShelfNo+"架"+layerNo+"层,请先在「层」页签创建该层后再生成位置号")
|
||
return
|
||
}
|
||
poss := make([]int, 0)
|
||
for p := req.PosStart; p <= req.PosEnd; p++ {
|
||
poss = append(poss, p)
|
||
}
|
||
if 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(poss))
|
||
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(layerNo), zone.CodeEQ(ps)).Exist(ctx0())
|
||
preview = append(preview, genItem{LayerNo: layerNo, PositionNo: ps, PositionName: req.ShelfNo + "架" + layerNo + "层" + ps + "位", Exists: exists})
|
||
}
|
||
if req.Preview {
|
||
ok(w, map[string]any{"preview": true, "list": preview, "total": len(preview)})
|
||
return
|
||
}
|
||
|
||
// 提交:仅在选定层下创建不存在的位置号(层必须已存在,不再自动创建父级层)
|
||
created, skipped := 0, 0
|
||
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(layerNo), zone.CodeEQ(ps)).Exist(ctx0()); exist {
|
||
skipped++
|
||
continue
|
||
}
|
||
if _, err := ctx.EntClient.Zone.Create().
|
||
SetLevel(4).SetCode(ps).SetParentCode(layerNo).
|
||
SetZoneCode(zoneCode).SetShelfNo(req.ShelfNo).SetLayerNo(layerNo).SetPositionNo(ps).
|
||
SetName(req.ShelfNo + "架" + layerNo + "层" + 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+" 层"+layerNo+
|
||
" 位"+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(poss)})
|
||
}
|
||
}
|