package handler import ( "net/http" "bj_power_wms/ent" "bj_power_wms/ent/inventory" "bj_power_wms/ent/zone" "bj_power_wms/internal/svc" ) // zoneLoc 库位推荐候选(历史常用位 / 空闲位共用):四级货位。 type zoneLoc struct { ZoneCode string `json:"zoneCode"` ZoneName string `json:"zoneName"` ShelfNo string `json:"shelfNo"` LayerNo string `json:"layerNo"` PositionNo string `json:"positionNo"` } // locKey 四级货位归一键(NULL/空串归一为""),与 DB 组合唯一索引 ux_zones_location 语义一致。 func locKey(zoneCode, shelfNo, layerNo, positionNo string) string { return zoneCode + "\x00" + shelfNo + "\x00" + layerNo + "\x00" + positionNo } // zoneRecommendHandler 库位推荐(客户诉求 问题记录 附二#4:拒绝全自动识别, // 采用「历史常用位 + 同区空位优先」推荐,由库房一键确认或手改)。 // // 入参:materialCode(必填,图号)。 // 返回(history/empty 均为四级货位对象,前端点击可一键回填区域+货架/层/位): // - history: 该物料历史入库使用过的货位(按最近使用排序,最多 5 个) // - empty: 空闲货位(zones 中启用且当前无在库/锁定库存引用,最多 10 个;同区域优先) func zoneRecommendHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { materialCode := r.URL.Query().Get("materialCode") if materialCode == "" { fail(w, http.StatusBadRequest, "materialCode 必填") return } // 1. 历史常用货位:该物料库存行按最近更新倒序,去重取前 5 个四级货位。 history := []zoneLoc{} histSet := map[string]bool{} preferredZone := "" if invs, err := ctx.EntClient.Inventory.Query(). Where(inventory.MaterialCodeEQ(materialCode)). Order(ent.Desc("updated_at"), ent.Desc("id")). Limit(200).All(ctx0()); err == nil { for _, inv := range invs { if inv.ZoneCode == "" { continue } key := locKey(inv.ZoneCode, inv.ShelfNo, inv.LayerNo, inv.PositionNo) if histSet[key] { continue } histSet[key] = true if preferredZone == "" { preferredZone = inv.ZoneCode } history = append(history, zoneLoc{ ZoneCode: inv.ZoneCode, ShelfNo: inv.ShelfNo, LayerNo: inv.LayerNo, PositionNo: inv.PositionNo, }) if len(history) >= 5 { break } } } // 2. 被占用货位集合:在库/锁定库存行引用的四级货位(U18:按货位而非区域判占用)。 occupied := map[string]bool{} if rows, err := ctx.EntClient.Inventory.Query(). Where(inventory.StatusIn("在库", "锁定")). Select(inventory.FieldZoneCode, inventory.FieldShelfNo, inventory.FieldLayerNo, inventory.FieldPositionNo). All(ctx0()); err == nil { for _, row := range rows { if row.ZoneCode != "" { occupied[locKey(row.ZoneCode, row.ShelfNo, row.LayerNo, row.PositionNo)] = true } } } // 3. 空闲货位:启用且四级货位未被占用的 zones 行;同区域(preferredZone)的排前面。 sameArea := []zoneLoc{} otherArea := []zoneLoc{} if zones, err := ctx.EntClient.Zone.Query(). Where(zone.StatusEQ("启用")). Order(ent.Asc("id")). All(ctx0()); err == nil { for _, z := range zones { if occupied[locKey(z.ZoneCode, z.ShelfNo, z.LayerNo, z.PositionNo)] { continue } zl := zoneLoc{ ZoneCode: z.ZoneCode, ZoneName: z.ZoneName, ShelfNo: z.ShelfNo, LayerNo: z.LayerNo, PositionNo: z.PositionNo, } // 同区判定:区域(zone_code)相同(U18:zone_code 即区域级,不再靠前缀猜测)。 if preferredZone != "" && z.ZoneCode == preferredZone { sameArea = append(sameArea, zl) } else { otherArea = append(otherArea, zl) } } } empty := append(sameArea, otherArea...) if len(empty) > 10 { empty = empty[:10] } ok(w, map[string]any{ "history": history, "empty": empty, }) } }