package handler import ( "net/http" "bj_power_wms/ent/inventorybatch" "bj_power_wms/ent/inventorylock" "bj_power_wms/ent/serialnumber" "bj_power_wms/internal/svc" "bj_power_wms/ent" ) // queryStockHandler 库存查询(MES 查料) // 支持按物料编码、区域、关键字、类型(batch/serial)过滤 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") stockType := r.URL.Query().Get("type") // batch / serial keyword := r.URL.Query().Get("keyword") page := atoi(r.URL.Query().Get("page"), 1) pageSize := atoi(r.URL.Query().Get("pageSize"), 50) result := map[string]any{ "batch": []any{}, "serial": []any{}, "total": 0, } // 批次库存 batchQ := ctx.EntClient.InventoryBatch.Query() if materialCode != "" { batchQ = batchQ.Where(inventorybatch.MaterialCodeEQ(materialCode)) } if zoneCode != "" { batchQ = batchQ.Where(inventorybatch.ZoneCodeEQ(zoneCode)) } if keyword != "" { batchQ = batchQ.Where( inventorybatch.Or( inventorybatch.BatchNoContains(keyword), inventorybatch.MaterialNameContains(keyword), ), ) } batchTotal, err := batchQ.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } batches, err := batchQ.Order(ent.Desc("id")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } // SN 库存 serialQ := ctx.EntClient.SerialNumber.Query() if materialCode != "" { serialQ = serialQ.Where(serialnumber.MaterialCodeEQ(materialCode)) } if zoneCode != "" { serialQ = serialQ.Where(serialnumber.CurrentZoneEQ(zoneCode)) } if keyword != "" { serialQ = serialQ.Where( serialnumber.Or( serialnumber.SnCodeContains(keyword), serialnumber.MaterialCodeContains(keyword), ), ) } serialTotal, err := serialQ.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } serials, err := serialQ.Order(ent.Desc("id")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } if stockType == "batch" { result["batch"] = batches result["total"] = batchTotal } else if stockType == "serial" { result["serial"] = serials result["total"] = serialTotal } else { result["batch"] = batches result["serial"] = serials result["total"] = batchTotal + serialTotal } result["page"] = page result["pageSize"] = pageSize ok(w, result) } } // lockStockHandler 工单锁定库存 // body: { orderNo, items: [{ materialCode, batchNo?, sn?, 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 != "" { sn, err := ctx.EntClient.SerialNumber.Query(). Where( serialnumber.SnCodeEQ(item.Sn), serialnumber.MaterialCodeEQ(item.MaterialCode), serialnumber.StatusEQ("在库"), ). Only(ctx0()) if err != nil { fail(w, http.StatusBadRequest, "SN 不在库或不存在: "+item.Sn) return } _, err = ctx.EntClient.InventoryLock.Create(). SetTargetType("SN"). SetTargetID(sn.SnCode). SetMaterialCode(item.MaterialCode). SetLockedQty(1). SetOrderNo(req.OrderNo). SetStatus("ACTIVE"). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "锁定 SN 失败: "+err.Error()) return } // SN 状态 → 锁定 ctx.EntClient.SerialNumber.UpdateOneID(sn.ID). SetStatus("锁定").ExecX(ctx0()) locked++ continue } // 结构件:锁批次数量 qty := item.Qty if qty <= 0 { qty = 1 } batch, err := ctx.EntClient.InventoryBatch.Query(). Where(inventorybatch.BatchNoEQ(item.BatchNo)). Only(ctx0()) if err != nil { fail(w, http.StatusBadRequest, "批次不存在: "+item.BatchNo) return } avail := batch.Quantity - batch.LockedQty if avail < qty { fail(w, http.StatusConflict, "批次库存不足: "+item.BatchNo+" 可用 "+itoa(avail)) return } _, err = ctx.EntClient.InventoryLock.Create(). SetTargetType("BATCH"). SetTargetID(batch.BatchNo). SetMaterialCode(item.MaterialCode). SetLockedQty(qty). SetOrderNo(req.OrderNo). SetStatus("ACTIVE"). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "锁定批次失败: "+err.Error()) return } // 累加批次锁定数 ctx.EntClient.InventoryBatch.UpdateOneID(batch.ID). SetLockedQty(batch.LockedQty + qty).ExecX(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" { b, err := ctx.EntClient.InventoryBatch.Query(). Where(inventorybatch.BatchNoEQ(l.TargetID)).Only(ctx0()) if err == nil { newLocked := b.LockedQty - l.LockedQty if newLocked < 0 { newLocked = 0 } ctx.EntClient.InventoryBatch.UpdateOneID(b.ID). SetLockedQty(newLocked).ExecX(ctx0()) } } else if l.TargetType == "SN" { ctx.EntClient.SerialNumber.Update(). Where(serialnumber.SnCodeEQ(l.TargetID)). SetStatus("在库").ExecX(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 { // 汇总批次可用量 batches, err := ctx.EntClient.InventoryBatch.Query(). Where( inventorybatch.MaterialCodeEQ(item.MaterialCode), inventorybatch.QualityStatusEQ("合格"), ). All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } availBatch := 0 for _, b := range batches { availBatch += b.Quantity - b.LockedQty } // 汇总 SN 在库数 availSerial, _ := ctx.EntClient.SerialNumber.Query(). Where( serialnumber.MaterialCodeEQ(item.MaterialCode), serialnumber.StatusEQ("在库"), ). Count(ctx0()) 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:]) }