package handler import ( "net/http" "time" "bj_power_wms/ent" "bj_power_wms/ent/inventory" "bj_power_wms/ent/stocktakeitem" "bj_power_wms/ent/stocktakeorder" "bj_power_wms/internal/svc" "github.com/google/uuid" ) // startStocktakeHandler 发起盘点:冻结当前库存快照 // 快照统一来自 inventory 一张表;结构件以批次为单位(bookQty=批次余量),精密件以 SN 为单位(bookQty=1)。 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"` // 可选:限定盘点区域,空=全部 MaterialCodes []string `json:"materialCodes"` // 可选:限定盘点物料,空=全部 ManageMode int `json:"manageMode"` // 可选:1结构件/2精密件,0=全部 QualityStatus string `json:"qualityStatus"` // 可选:限定质量状态,空=全部 } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } no := "ST" + time.Now().Format("20060102150405") + uuid.NewString()[:4] q := ctx.EntClient.Inventory.Query().Where(inventory.StatusIn("在库", "锁定")) if len(req.Zones) > 0 { q = q.Where(inventory.ZoneCodeIn(req.Zones...)) } if len(req.MaterialCodes) > 0 { q = q.Where(inventory.MaterialCodeIn(req.MaterialCodes...)) } if req.ManageMode != 0 { q = q.Where(inventory.ManageModeEQ(req.ManageMode)) } if req.QualityStatus != "" { q = q.Where(inventory.QualityStatusEQ(req.QualityStatus)) } rows, err := q.All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } count := 0 for _, inv := range rows { tt := "BATCH" tid := inv.BatchNo book := inv.Quantity if inv.ManageMode == 2 { tt = "SN" tid = inv.SnCode book = 1 } if tid == "" { continue } ctx.EntClient.StocktakeItem.Create(). SetStocktakeNo(no). SetTargetType(tt). SetTargetID(tid). SetNillableMaterialCode(strPtr(inv.MaterialCode)). SetNillableZoneCode(strPtr(inv.ZoneCode)). SetBookQty(book). 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 完成盘点:统计差异、按需按"增量"修正库存 // 调整原则(问题2):绝不覆盖最终值,只按差异增减——quantity += diffQty。 // - 结构件:批次余量 +diff(盘盈)/-diff(盘亏) // - 精密件:差异为 -1(账面1实盘0)按报废处理;其余差异仅记录 // 整个调整包在事务内,保证盘点修正原子性。 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 // 需要调整时开启事务 var tx *ent.Tx if req.Adjust { tx, err = ctx.EntClient.Tx(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error()) return } } committed := false defer func() { if tx != nil && !committed { _ = tx.Rollback() } }() 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" { // 增量调整:quantity += diffQty(不覆盖终值) if _, e := tx.Inventory.Update(). Where(inventory.ManageModeEQ(1), inventory.BatchNoEQ(it.TargetID)). AddQuantity(it.DiffQty). Save(ctx0()); e != nil { fail(w, http.StatusInternalServerError, "调整批次失败: "+e.Error()) return } } else if it.TargetType == "SN" && it.DiffQty == -1 { // 账面1实盘0 → 报废 if _, e := tx.Inventory.Update(). Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(it.TargetID), inventory.StatusIn("在库", "锁定")). SetStatus("报废").SetLockedQty(0). Save(ctx0()); e != nil { fail(w, http.StatusInternalServerError, "报废 SN 失败: "+e.Error()) return } } } } } if req.Adjust { if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error()) return } committed = true } 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("created_at")).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("created_at")). 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, }) } }