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)}) } }