package handler import ( "net/http" "time" "bj_power_wms/ent" "bj_power_wms/ent/inventorybatch" "bj_power_wms/ent/serialnumber" "bj_power_wms/ent/stocktakeitem" "bj_power_wms/ent/stocktakeorder" "bj_power_wms/internal/svc" "github.com/google/uuid" ) // startStocktakeHandler 发起盘点:冻结当前库存快照 func startStocktakeHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { Operator string `json:"operator"` Zones []string `json:"zones"` // 可选:限定盘点区域,空=全部 } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } no := "ST" + time.Now().Format("20060102150405") + uuid.NewString()[:4] count := 0 // 快照批次库存 bq := ctx.EntClient.InventoryBatch.Query() batches, err := bq.All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } for _, b := range batches { ctx.EntClient.StocktakeItem.Create(). SetStocktakeNo(no). SetTargetType("BATCH"). SetTargetID(b.BatchNo). SetNillableMaterialCode(strPtr(b.MaterialCode)). SetNillableZoneCode(strPtr(b.ZoneCode)). SetBookQty(b.Quantity). SaveX(ctx0()) count++ } // 快照 SN 库存 sns, err := ctx.EntClient.SerialNumber.Query().All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } for _, s := range sns { // 仅在库/锁定的才算库存;出库的不进快照 if s.Status == "出库" || s.Status == "报废" { continue } ctx.EntClient.StocktakeItem.Create(). SetStocktakeNo(no). SetTargetType("SN"). SetTargetID(s.SnCode). SetNillableMaterialCode(strPtr(s.MaterialCode)). SetNillableZoneCode(strPtr(s.CurrentZone)). SetBookQty(1). SaveX(ctx0()) count++ } rec, err := ctx.EntClient.StocktakeOrder.Create(). SetStocktakeNo(no). SetNillableOperator(strPtr(req.Operator)). SetTotalTargets(count). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "创建盘点单失败: "+err.Error()) return } ok(w, map[string]any{"stocktakeNo": rec.StocktakeNo, "totalTargets": count}) } } // recordStocktakeHandler 扫码盘点记录:查快照并记录实盘数与差异 func recordStocktakeHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { StocktakeNo string `json:"stocktakeNo"` TargetType string `json:"targetType"` // BATCH/SN,可空则自动判断 TargetId string `json:"targetId"` ScanQty int `json:"scanQty"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } if req.StocktakeNo == "" || req.TargetId == "" { fail(w, http.StatusBadRequest, "stocktakeNo 和 targetId 必填") return } order, err := ctx.EntClient.StocktakeOrder.Query(). Where(stocktakeorder.StocktakeNoEQ(req.StocktakeNo)).Only(ctx0()) if err != nil { fail(w, http.StatusNotFound, "盘点单不存在: "+req.StocktakeNo) return } if order.Status != "进行中" { fail(w, http.StatusConflict, "盘点单已完成,禁止录入") return } item, err := ctx.EntClient.StocktakeItem.Query(). Where( stocktakeitem.StocktakeNoEQ(req.StocktakeNo), stocktakeitem.TargetIDEQ(req.TargetId), ).First(ctx0()) if err != nil { fail(w, http.StatusNotFound, "该目标不在本次盘点快照中: "+req.TargetId) return } qty := req.ScanQty if qty < 0 { fail(w, http.StatusBadRequest, "scanQty 不能为负数") return } diff := qty - item.BookQty ctx.EntClient.StocktakeItem.UpdateOneID(item.ID). SetScannedQty(qty). SetDiffQty(diff). SetCounted(true). SetUpdatedAt(time.Now().Unix()). ExecX(ctx0()) ok(w, map[string]any{ "targetId": req.TargetId, "bookQty": item.BookQty, "scannedQty": qty, "diffQty": diff, }) } } // finishStocktakeHandler 完成盘点:统计差异、按需修正库存 func finishStocktakeHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { StocktakeNo string `json:"stocktakeNo"` Adjust bool `json:"adjust"` // true=把差异写回库存 } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } if req.StocktakeNo == "" { fail(w, http.StatusBadRequest, "stocktakeNo 必填") return } order, err := ctx.EntClient.StocktakeOrder.Query(). Where(stocktakeorder.StocktakeNoEQ(req.StocktakeNo)).Only(ctx0()) if err != nil { fail(w, http.StatusNotFound, "盘点单不存在") return } items, err := ctx.EntClient.StocktakeItem.Query(). Where(stocktakeitem.StocktakeNoEQ(req.StocktakeNo)).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } type diffRow struct { TargetId string `json:"targetId"` Type string `json:"type"` Book int `json:"bookQty"` Scanned int `json:"scannedQty"` Diff int `json:"diffQty"` } diffs := []diffRow{} counted := 0 uncounted := 0 for _, it := range items { if !it.Counted { uncounted++ continue } counted++ if it.DiffQty != 0 { diffs = append(diffs, diffRow{it.TargetID, it.TargetType, it.BookQty, it.ScannedQty, it.DiffQty}) if req.Adjust { if it.TargetType == "BATCH" { b, err := ctx.EntClient.InventoryBatch.Query(). Where(inventorybatch.BatchNoEQ(it.TargetID)).Only(ctx0()) if err == nil && b.Quantity+it.DiffQty >= b.LockedQty { ctx.EntClient.InventoryBatch.UpdateOneID(b.ID). SetQuantity(b.Quantity + it.DiffQty).ExecX(ctx0()) } } else if it.DiffQty == -1 { // 账面1实盘0 → 报废标记 ctx.EntClient.SerialNumber.Update(). Where(serialnumber.SnCodeEQ(it.TargetID), serialnumber.StatusIn("在库", "锁定")). SetStatus("报废").ExecX(ctx0()) } } } } ctx.EntClient.StocktakeOrder.UpdateOneID(order.ID). SetStatus("已完成"). SetFinishedAt(time.Now().Unix()). ExecX(ctx0()) ok(w, map[string]any{ "stocktakeNo": req.StocktakeNo, "adjusted": req.Adjust, "counted": counted, "uncounted": uncounted, "diffs": diffs, }) } } // queryStocktakeHandler 盘点结果查询 func queryStocktakeHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { no := r.URL.Query().Get("stocktakeNo") page := atoi(r.URL.Query().Get("page"), 1) pageSize := atoi(r.URL.Query().Get("pageSize"), 50) if no == "" { orders, _ := ctx.EntClient.StocktakeOrder.Query(). Order(ent.Desc("id")).Limit(100).All(ctx0()) ok(w, map[string]any{"list": orders}) return } total, err := ctx.EntClient.StocktakeItem.Query(). Where(stocktakeitem.StocktakeNoEQ(no)).Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := ctx.EntClient.StocktakeItem.Query(). Where(stocktakeitem.StocktakeNoEQ(no)). Order(ent.Desc("id")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } order, _ := ctx.EntClient.StocktakeOrder.Query(). Where(stocktakeorder.StocktakeNoEQ(no)).First(ctx0()) ok(w, map[string]any{ "order": order, "total": total, "list": list, "page": page, "pageSize": pageSize, }) } }