Files
bj_power/bj_power_wms/internal/handler/zone.go
T

623 lines
22 KiB
Go
Raw Normal View History

2026-09-18 18:54:54 +08:00
package handler
2026-08-28 15:06:01 +08:00
import (
"net/http"
"strconv"
2026-09-18 18:54:54 +08:00
"strings"
2026-08-28 15:06:01 +08:00
"bj_power_wms/ent"
"bj_power_wms/ent/inventory"
2026-08-28 15:06:01 +08:00
"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 "节点"
2026-09-18 18:54:54 +08:00
}
// 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):每次查询实时聚合 inventoriesquantity>0 且非出库/报废),不缓存。
// level1=区域合计、level2=货架合计、level3=层合计、level4=该位置合计。
StockQty int `json:"stockQty"`
2026-09-18 18:54:54 +08:00
}
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,
}
2026-09-18 18:54:54 +08:00
}
// listZonesHandler 按层级列出节点(4 个 Tab 各自独立分页 + 按上级筛选)
// 参数:level(必填1~4)、parentCode、zoneCode、shelfNo、layerNo、code、name、status、page、pageSize
func listZonesHandler(ctx *svc.ServiceContext) http.HandlerFunc {
2026-08-28 15:06:01 +08:00
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")
2026-08-28 15:06:01 +08:00
return
}
page := atoi(r.URL.Query().Get("page"), 1)
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
if page <= 0 {
page = 1
2026-08-28 15:06:01 +08:00
}
if pageSize <= 0 {
pageSize = 20
2026-09-18 18:54:54 +08:00
}
if pageSize > 200 {
pageSize = 200
2026-09-18 18:54:54 +08:00
}
2026-08-28 15:06:01 +08:00
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())
2026-08-28 15:06:01 +08:00
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())
2026-09-18 18:54:54 +08:00
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})
}
}
2026-09-18 18:54:54 +08:00
// zoneStockMaps 实时聚合各级库存总量(需求E):一次扫描在库 inventoriesquantity>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
}
2026-08-28 15:06:01 +08:00
}
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())
2026-09-18 18:54:54 +08:00
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, "请选择所属货架")
2026-09-18 18:54:54 +08:00
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
2026-08-28 15:06:01 +08:00
}
// 建货架(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))
2026-08-28 15:06:01 +08:00
}
}
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
2026-09-18 18:54:54 +08:00
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, "节点不存在")
2026-09-18 18:54:54 +08:00
return
}
status := req.Status
if status == "" {
status = z.Status
2026-09-18 18:54:54 +08:00
}
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})
}
}
2026-09-18 18:54:54 +08:00
// 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))
2026-09-18 18:54:54 +08:00
}
rows, err := q.Order(ent.Asc("code")).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
2026-09-18 18:54:54 +08:00
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,
})
2026-09-18 18:54:54 +08:00
}
ok(w, list)
return
2026-09-18 18:54:54 +08:00
}
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))
2026-09-18 18:54:54 +08:00
}
if zc := strings.TrimSpace(r.URL.Query().Get("zoneCode")); zc != "" {
q = q.Where(zone.ZoneCodeEQ(strings.ToUpper(zc)))
2026-09-18 18:54:54 +08:00
}
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))
2026-09-18 18:54:54 +08:00
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,
2026-09-18 18:54:54 +08:00
})
}
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())
2026-08-28 15:06:01 +08:00
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 个位置,请缩小范围")
2026-08-28 15:06:01 +08:00
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)})
}
}