Files
bj_power/bj_power_wms/internal/handler/zone.go
T
SunYF 132c3ed6bd docs(test): 更新电表箱装配业务回归测试文档
- 将文档从代码审计任务转换为端到端业务回归测试链路
- 重新组织文档结构为链路总览、示例数据基线和详细步骤
- 添加完整的业务场景清单涵盖WMS/MES/工位终端三大系统
- 定义统一的测试数据包括产品型号、物料清单、工位派工等
- 提供详细的步骤断言和数量等式验证方法
- 建立贯穿全链路的数据流向和状态变更追踪体系
2026-09-21 10:48:53 +08:00

617 lines
23 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 新建层级节点请求。
// 父级以完整路径给出(区域→货架→层),后端据此精确定位父级,避免仅凭 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)})
}
}