package handler import ( "fmt" "net/http" "strings" "entgo.io/ent/dialect/sql" "bj_power_wms/ent" "bj_power_wms/ent/inventory" "bj_power_wms/ent/inventorylock" "bj_power_wms/ent/predicate" "bj_power_wms/internal/svc" ) // queryStockHandler 库存查询(MES 查料 / 客户端查料) // 统一查询 inventory 一张表:按 物料编码/区域/关键字/管理粒度(manageMode) 过滤。 // 返回单一 list(结构件与精密件同表),前端统一展示、统一导出。 func queryStockHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { materialCode := r.URL.Query().Get("materialCode") zoneCode := r.URL.Query().Get("zoneCode") manageMode := r.URL.Query().Get("manageMode") // 1/2/空=全部 keyword := r.URL.Query().Get("keyword") page := atoi(r.URL.Query().Get("page"), 1) pageSize := atoi(r.URL.Query().Get("pageSize"), 50) q := ctx.EntClient.Inventory.Query() // 仅展示在库/锁定(出库/使用/报废不计入库存) q = q.Where(inventory.StatusIn("在库", "锁定")) // 结构件隐藏数量为 0 的行 q = q.Where(inventory.Or(inventory.ManageModeEQ(2), inventory.QuantityGT(0))) if materialCode != "" { q = q.Where(inventory.MaterialCodeEQ(materialCode)) } if zoneCode != "" { q = q.Where(inventory.ZoneCodeEQ(zoneCode)) } if manageMode == "1" { q = q.Where(inventory.ManageModeEQ(1)) } else if manageMode == "2" { q = q.Where(inventory.ManageModeEQ(2)) } if keyword != "" { q = q.Where(inventory.Or( inventory.BatchNoContains(keyword), inventory.MaterialCodeContains(keyword), inventory.SnCodeContains(keyword), inventory.MaterialNameContains(keyword), )) } total, err := q.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := q.Order(ent.Desc("id")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]any{ "list": list, "total": total, "page": page, "pageSize": pageSize, }) } } // zoneSummaryHandler 区域库存汇总(按 区域 + 质量状态 + 类型 聚合) // GET /api/stock/zone-summary // 返回 rows: [{zoneCode, zoneName, quality, type(批次/精密件), qty}] // 仅按"区域(Z01~Z04)"维度聚合,不引入货架/层/格(业务决策:小货架高频取货无需精确货位索引)。 func zoneSummaryHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zones, _ := ctx.EntClient.Zone.Query().All(ctx0()) zoneNameMap := map[string]string{} for _, z := range zones { zoneNameMap[z.ZoneCode] = z.ZoneName } rows, _ := ctx.EntClient.Inventory.Query(). Where(inventory.StatusIn("在库", "锁定")).All(ctx0()) type summRow struct { ZoneCode string `json:"zoneCode"` ZoneName string `json:"zoneName"` Quality string `json:"quality"` Type string `json:"type"` // 批次/精密件 Qty int `json:"qty"` } agg := map[string]int{} // key: zc|quality|type for _, inv := range rows { zc := inv.ZoneCode if zc == "" { zc = "未分区" } t := "批次" if inv.ManageMode == 2 { t = "精密件" } key := zc + "|" + inv.QualityStatus + "|" + t if inv.ManageMode == 1 { agg[key] += inv.Quantity } else { agg[key] += 1 } } out := make([]summRow, 0, len(agg)) for k, v := range agg { parts := strings.Split(k, "|") name := zoneNameMap[parts[0]] if name == "" { name = parts[0] } out = append(out, summRow{parts[0], name, parts[1], parts[2], v}) } ok(w, map[string]any{"rows": out}) } } // invAvailGTE 自定义谓词:可用量(quantity - locked_qty) >= need // 用于锁库存时的原子条件校验,避免并发超锁。 func invAvailGTE(need int) predicate.Inventory { return predicate.Inventory(func(s *sql.Selector) { s.Where(sql.ExprP( fmt.Sprintf("%s.%s - %s.%s >= ?", inventory.Table, inventory.FieldQuantity, inventory.Table, inventory.FieldLockedQty), need, )) }) } // lockStockHandler 工单锁定库存(库存占用) // body: { orderNo, items: [{ materialCode, batchNo?, sn?, qty }] } // 锁定为"增量"操作:结构件 locked_qty += qty;精密件状态置 锁定。绝不覆盖终值。 // 锁批次用原子增量 + 可用量条件校验;锁 SN 用状态条件原子更新,杜绝超锁与并发冲突。 func lockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { OrderNo string `json:"orderNo"` Items []struct { MaterialCode string `json:"materialCode"` BatchNo string `json:"batchNo"` Sn string `json:"sn"` Qty int `json:"qty"` } `json:"items"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } if req.OrderNo == "" || len(req.Items) == 0 { fail(w, http.StatusBadRequest, "orderNo 和 items 必填") return } locked := 0 for _, item := range req.Items { if item.MaterialCode == "" { continue } // 精密件:锁 SN(原子:仅 在库 才能锁) if item.Sn != "" { aff, err := ctx.EntClient.Inventory.Update(). Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(item.Sn), inventory.StatusEQ("在库")). SetStatus("锁定").SetLockedQty(1). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "锁定 SN 失败: "+err.Error()) return } if aff == 0 { fail(w, http.StatusBadRequest, "SN 不在库或不存在: "+item.Sn) return } ctx.EntClient.InventoryLock.Create(). SetTargetType("SN"). SetTargetID(item.Sn). SetMaterialCode(item.MaterialCode). SetLockedQty(1). SetOrderNo(req.OrderNo). SetStatus("ACTIVE"). SaveX(ctx0()) locked++ continue } // 结构件:锁批次数量(原子增量 + 可用量校验) qty := item.Qty if qty <= 0 { qty = 1 } b, err := ctx.EntClient.Inventory.Query(). Where(inventory.ManageModeEQ(1), inventory.BatchNoEQ(item.BatchNo)).Only(ctx0()) if err != nil { fail(w, http.StatusBadRequest, "批次不存在: "+item.BatchNo) return } avail := b.Quantity - b.LockedQty if avail < qty { fail(w, http.StatusConflict, "批次库存不足: "+item.BatchNo+" 可用 "+itoa(avail)) return } aff, err := ctx.EntClient.Inventory.Update(). Where(inventory.ID(b.ID), invAvailGTE(qty)). AddLockedQty(qty). Save(ctx0()) if err != nil || aff == 0 { fail(w, http.StatusConflict, "锁定失败(库存不足或并发冲突): "+item.BatchNo) return } ctx.EntClient.InventoryLock.Create(). SetTargetType("BATCH"). SetTargetID(item.BatchNo). SetMaterialCode(item.MaterialCode). SetLockedQty(qty). SetOrderNo(req.OrderNo). SetStatus("ACTIVE"). SaveX(ctx0()) locked += qty } ok(w, map[string]any{"locked": locked}) } } // unlockStockHandler 释放库存锁定(取消锁定/返工) func unlockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { OrderNo string `json:"orderNo"` LockIDs []int `json:"lockIds"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } q := ctx.EntClient.InventoryLock.Query(). Where(inventorylock.StatusEQ("ACTIVE")) if req.OrderNo != "" { q = q.Where(inventorylock.OrderNoEQ(req.OrderNo)) } if len(req.LockIDs) > 0 { q = q.Where(inventorylock.IDIn(req.LockIDs...)) } locks, err := q.All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } released := 0 for _, l := range locks { // 还原批次锁定数 / SN 状态 if l.TargetType == "BATCH" { ctx.EntClient.Inventory.Update(). Where(inventory.BatchNoEQ(l.TargetID)). AddLockedQty(-l.LockedQty). SaveX(ctx0()) // 防止并发下锁定数为负 ctx.EntClient.Inventory.Update(). Where(inventory.BatchNoEQ(l.TargetID), inventory.LockedQtyLT(0)). SetLockedQty(0). SaveX(ctx0()) } else if l.TargetType == "SN" { ctx.EntClient.Inventory.Update(). Where(inventory.SnCodeEQ(l.TargetID), inventory.StatusEQ("锁定")). SetStatus("在库").SetLockedQty(0). SaveX(ctx0()) } ctx.EntClient.InventoryLock.UpdateOneID(l.ID). SetStatus("RELEASED").ExecX(ctx0()) released++ } ok(w, map[string]any{"released": released}) } } // checkStockHandler 校验库存是否满足需求(锁库前检查) func checkStockHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { Items []struct { MaterialCode string `json:"materialCode"` Qty int `json:"qty"` } `json:"items"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } results := make([]map[string]any, 0, len(req.Items)) allOk := true for _, item := range req.Items { // 汇总可用量:结构件 quantity-locked_qty;精密件 在库数量 rows, _ := ctx.EntClient.Inventory.Query(). Where(inventory.MaterialCodeEQ(item.MaterialCode)).All(ctx0()) availBatch := 0 availSerial := 0 for _, row := range rows { if row.ManageMode == 1 { availBatch += row.Quantity - row.LockedQty } else if row.Status == "在库" { availSerial++ } } enough := availBatch+availSerial >= item.Qty if !enough { allOk = false } results = append(results, map[string]any{ "materialCode": item.MaterialCode, "requireQty": item.Qty, "availQty": availBatch + availSerial, "batchAvail": availBatch, "serialAvail": availSerial, "enough": enough, }) } ok(w, map[string]any{"allOk": allOk, "items": results}) } } func itoa(n int) string { if n == 0 { return "0" } neg := n < 0 if neg { n = -n } var b [20]byte i := len(b) for n > 0 { i-- b[i] = byte('0' + n%10) n /= 10 } if neg { i-- b[i] = '-' } return string(b[i:]) }