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