Files
bj_power/bj_power_wms/internal/handler/zone.go
T
SunYF 8e6e8e1d20 docs(business): 生成业务契约文档并重构MES客户端代码
- 创建案例.md文档,包含业务场景清单、业务流程、案例和回归测试三份完整文档
- WMS库房管理系统涵盖入库、出库、库存、检验、盘点等24个业务场景
- MES产线控制系统包含工单、排产、BOM、备料、质检、追溯等23个业务场景
- 工位终端系统支持上线、报工、下线、领料、巡检等15个现场作业场景
- 主链路从合同到成品入库,异常链路处理不合格、数量不符、退料等情况
- 删除MES客户端中的ScanRecord相关代码,精简客户端结构
- 更新客户端初始化逻辑,移除扫描记录相关的依赖注入配置
2026-09-20 18:25:44 +08:00

623 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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):每次查询实时聚合 inventoriesquantity>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):一次扫描在库 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
}
}
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)})
}
}