Files
bj_power/bj_power_wms/internal/handler/zone.go
T
2026-09-18 18:54:54 +08:00

372 lines
12 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"
"time"
"bj_power_wms/ent"
"bj_power_wms/ent/zone"
"bj_power_wms/internal/svc"
)
// zoneLocationReq 区域下的一个货位(货架号 / 第几层 / 位置号,均可手填)。
// 区域维护按「区域编码」分组展示:一个区域编码 + 名称只登记一次,货位可以有多条。
type zoneLocationReq struct {
ID int `json:"id"`
ShelfNo string `json:"shelfNo"`
LayerNo string `json:"layerNo"`
PositionNo string `json:"positionNo"`
}
// normLoc 归一货位三级输入(去首尾空格),与 DB 组合唯一索引 COALESCE 归一空值的口径保持一致
func normLoc(loc *zoneLocationReq) {
loc.ShelfNo = strings.TrimSpace(loc.ShelfNo)
loc.LayerNo = strings.TrimSpace(loc.LayerNo)
loc.PositionNo = strings.TrimSpace(loc.PositionNo)
}
// locComboKey 货位三级组合的比较键(同区域内判重用)
func locComboKey(shelfNo, layerNo, positionNo string) string {
return shelfNo + "\x00" + layerNo + "\x00" + positionNo
}
// createZoneHandler 新建区域(一次可登记多个货位)
func createZoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
ZoneCode string `json:"zoneCode"`
ZoneName string `json:"zoneName"`
Description string `json:"description"`
Locations []zoneLocationReq `json:"locations"`
// 兼容旧版单货位入参
ShelfNo string `json:"shelfNo"`
LayerNo string `json:"layerNo"`
PositionNo string `json:"positionNo"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
req.ZoneCode = strings.ToUpper(strings.TrimSpace(req.ZoneCode))
req.ZoneName = strings.TrimSpace(req.ZoneName)
if req.ZoneCode == "" || req.ZoneName == "" {
fail(w, http.StatusBadRequest, "区域编码和区域名称必填")
return
}
if req.Locations == nil {
req.Locations = []zoneLocationReq{{ShelfNo: req.ShelfNo, LayerNo: req.LayerNo, PositionNo: req.PositionNo}}
}
if len(req.Locations) == 0 {
fail(w, http.StatusBadRequest, "至少填写一个货位")
return
}
// 区域编码已存在:区域维护按区域编码分组,同一区域应通过「编辑」继续添加货位
sameZone, err := ctx.EntClient.Zone.Query().
Where(zone.ZoneCodeEQ(req.ZoneCode)).
All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
if len(sameZone) > 0 {
fail(w, http.StatusConflict, "区域编码已存在:"+req.ZoneCode+",请在列表中编辑该区域添加货位")
return
}
// 货位唯一性(U18):请求内按四级组合判重,与 DB 组合唯一索引 ux_zones_location 一致
seen := map[string]bool{}
for i := range req.Locations {
normLoc(&req.Locations[i])
key := locComboKey(req.Locations[i].ShelfNo, req.Locations[i].LayerNo, req.Locations[i].PositionNo)
if seen[key] {
fail(w, http.StatusConflict, locDupMsg(req.ZoneCode, req.Locations[i].ShelfNo, req.Locations[i].LayerNo, req.Locations[i].PositionNo))
return
}
seen[key] = true
}
created := make([]*ent.Zone, 0, len(req.Locations))
for _, loc := range req.Locations {
z, err := ctx.EntClient.Zone.Create().
SetZoneCode(req.ZoneCode).
SetZoneName(req.ZoneName).
SetNillableDescription(strPtr(strings.TrimSpace(req.Description))).
SetNillableShelfNo(strPtr(loc.ShelfNo)).
SetNillableLayerNo(strPtr(loc.LayerNo)).
SetNillablePositionNo(strPtr(loc.PositionNo)).
SetStatus("启用").
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
created = append(created, z)
}
ctx.EventLog.Write(ctx0(), "zone.create", r.Header.Get("X-Username"), "zone", req.ZoneCode,
"新建区域 "+req.ZoneName+""+strconv.Itoa(len(created))+" 个货位)", nil)
ok(w, created)
}
}
// updateZoneHandler 编辑区域(区域信息 + 该区域下的货位一次性同步)
func updateZoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID int `json:"id"`
ZoneCode string `json:"zoneCode"`
ZoneName string `json:"zoneName"`
Description string `json:"description"`
Status string `json:"status"`
Locations []zoneLocationReq `json:"locations"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
req.ZoneName = strings.TrimSpace(req.ZoneName)
if req.ZoneName == "" {
fail(w, http.StatusBadRequest, "区域名称必填")
return
}
// 兼容旧版:仅传 id 时先反查区域编码
if req.ZoneCode == "" && req.ID > 0 {
cur, err := ctx.EntClient.Zone.Get(ctx0(), req.ID)
if err != nil {
fail(w, http.StatusNotFound, "区域不存在")
return
}
req.ZoneCode = cur.ZoneCode
}
req.ZoneCode = strings.ToUpper(strings.TrimSpace(req.ZoneCode))
if req.ZoneCode == "" {
fail(w, http.StatusBadRequest, "区域编码必填")
return
}
status := req.Status
if status == "" {
status = "启用"
}
if status != "启用" && status != "停用" {
fail(w, http.StatusBadRequest, "状态取值非法")
return
}
rows, err := ctx.EntClient.Zone.Query().Where(zone.ZoneCodeEQ(req.ZoneCode)).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
if len(rows) == 0 {
fail(w, http.StatusNotFound, "区域不存在:"+req.ZoneCode)
return
}
// locations 为 nil(未传)时只改区域信息,不动货位;传了就按提交结果整体同步
var toCreate []zoneLocationReq
keepIDs := make([]int, 0, len(req.Locations))
if req.Locations != nil {
if len(req.Locations) == 0 {
fail(w, http.StatusBadRequest, "至少保留一个货位")
return
}
exist := make(map[int]bool, len(rows))
for _, z := range rows {
exist[z.ID] = true
}
seen := map[string]bool{}
for i := range req.Locations {
normLoc(&req.Locations[i])
loc := req.Locations[i]
key := locComboKey(loc.ShelfNo, loc.LayerNo, loc.PositionNo)
if seen[key] {
fail(w, http.StatusConflict, locDupMsg(req.ZoneCode, loc.ShelfNo, loc.LayerNo, loc.PositionNo))
return
}
seen[key] = true
if loc.ID > 0 {
if !exist[loc.ID] {
fail(w, http.StatusBadRequest, "货位不存在或不属于该区域")
return
}
keepIDs = append(keepIDs, loc.ID)
} else {
toCreate = append(toCreate, loc)
}
}
}
// 区域信息(名称/说明/状态)在该区域所有货位行上统一更新
if _, err := ctx.EntClient.Zone.Update().Where(zone.ZoneCodeEQ(req.ZoneCode)).
SetZoneName(req.ZoneName).
SetNillableDescription(strPtr(strings.TrimSpace(req.Description))).
SetStatus(status).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新区域失败: "+err.Error())
return
}
if req.Locations != nil {
// 先删后增:避免与 DB 组合唯一索引 ux_zones_location 冲突
del := ctx.EntClient.Zone.Delete().Where(zone.ZoneCodeEQ(req.ZoneCode))
if len(keepIDs) > 0 {
del = del.Where(zone.IDNotIn(keepIDs...))
}
if _, err := del.Exec(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "删除货位失败: "+err.Error())
return
}
for _, loc := range req.Locations {
if loc.ID <= 0 {
continue
}
if _, err := ctx.EntClient.Zone.UpdateOneID(loc.ID).
SetZoneName(req.ZoneName).
SetNillableShelfNo(strPtr(loc.ShelfNo)).
SetNillableLayerNo(strPtr(loc.LayerNo)).
SetNillablePositionNo(strPtr(loc.PositionNo)).
SetStatus(status).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "更新货位失败: "+err.Error())
return
}
}
for _, loc := range toCreate {
if _, err := ctx.EntClient.Zone.Create().
SetZoneCode(req.ZoneCode).
SetZoneName(req.ZoneName).
SetNillableDescription(strPtr(strings.TrimSpace(req.Description))).
SetNillableShelfNo(strPtr(loc.ShelfNo)).
SetNillableLayerNo(strPtr(loc.LayerNo)).
SetNillablePositionNo(strPtr(loc.PositionNo)).
SetStatus(status).
Save(ctx0()); err != nil {
fail(w, http.StatusInternalServerError, "新增货位失败: "+err.Error())
return
}
}
}
ctx.EventLog.Write(ctx0(), "zone.update", r.Header.Get("X-Username"), "zone", req.ZoneCode,
"编辑区域 "+req.ZoneName, nil)
ok(w, map[string]any{"zoneCode": req.ZoneCode, "zoneName": req.ZoneName})
}
}
// listZonesGroupedHandler 区域列表(按区域编码分组:一个区域一行,货位作为子项返回)
// 优化点(小周问题记录 L6):原先每行 = 一个货位,同一区域编码/名称在列表里大量重复;
// 现按区域编码聚合,货位放在 locations 子数组里,分页按「区域」计数。
func listZonesGroupedHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
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
}
zoneCode := r.URL.Query().Get("zoneCode")
zoneName := r.URL.Query().Get("zoneName")
status := r.URL.Query().Get("status")
startDate := r.URL.Query().Get("startDate")
endDate := r.URL.Query().Get("endDate")
q := ctx.EntClient.Zone.Query()
if zoneCode != "" {
// 区域编码搜索大小写不敏感
q = q.Where(zone.ZoneCodeContainsFold(zoneCode))
}
if zoneName != "" {
q = q.Where(zone.ZoneNameContainsFold(zoneName))
}
if status != "" {
q = q.Where(zone.StatusEQ(status))
}
if startDate != "" {
if t, err := time.ParseInLocation("2006-01-02", startDate, time.Local); err == nil {
q = q.Where(zone.CreatedAtGTE(t.Unix()))
}
}
if endDate != "" {
if t, err := time.ParseInLocation("2006-01-02", endDate, time.Local); err == nil {
q = q.Where(zone.CreatedAtLTE(t.Add(24 * time.Hour).Unix()))
}
}
rows, err := q.Order(ent.Asc("zone_code"), ent.Asc("id")).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
type zoneGroup struct {
ZoneCode string `json:"zoneCode"`
ZoneName string `json:"zoneName"`
Description string `json:"description"`
Status string `json:"status"`
CreatedAt int64 `json:"createdAt"`
LocationCount int `json:"locationCount"`
Locations []map[string]any `json:"locations"`
}
order := make([]string, 0)
groups := map[string]*zoneGroup{}
for _, z := range rows {
g, found := groups[z.ZoneCode]
if !found {
g = &zoneGroup{
ZoneCode: z.ZoneCode, ZoneName: z.ZoneName, Description: z.Description,
Status: z.Status, CreatedAt: z.CreatedAt,
Locations: make([]map[string]any, 0, 4),
}
groups[z.ZoneCode] = g
order = append(order, z.ZoneCode)
}
g.Locations = append(g.Locations, map[string]any{
"id": z.ID, "shelfNo": z.ShelfNo, "layerNo": z.LayerNo, "positionNo": z.PositionNo,
})
g.LocationCount++
}
total := len(order)
start := (page - 1) * pageSize
if start > total {
start = total
}
end := start + pageSize
if end > total {
end = total
}
list := make([]*zoneGroup, 0, end-start)
for _, code := range order[start:end] {
list = append(list, groups[code])
}
ok(w, map[string]any{
"total": total,
"list": list,
"page": page,
"pageSize": pageSize,
})
}
}
// listZonesForPicker 区域下拉列表(不分页,仅启用)
func listZonesForPicker(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
list, err := ctx.EntClient.Zone.Query().
Where(zone.StatusEQ("启用")).
Order(ent.Asc("id")).
All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ok(w, list)
}
}
// locDupMsg 货位重复登记的统一提示(复用 locLabel 拼四级货位串,U18)。
func locDupMsg(zoneCode, shelfNo, layerNo, positionNo string) string {
return "该货位已存在:" + locLabel(zoneCode, shelfNo, layerNo, positionNo)
}