package handler import ( "net/http" "bj_power_wms/ent" "bj_power_wms/ent/inventorybatch" "bj_power_wms/ent/inventorylock" "bj_power_wms/ent/ordermaterialledger" "bj_power_wms/ent/outbounddetail" "bj_power_wms/ent/outboundorder" "bj_power_wms/ent/serialnumber" "bj_power_wms/internal/svc" "github.com/google/uuid" "github.com/zeromicro/go-zero/core/logx" ) // deductStockRequest 出库扣减请求 // createOutboundHandler 创建出库单(等价于扣减库存,走同一套约束) func createOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc { return deductStockHandler(ctx) } type deductStockRequest struct { OrderNo string `json:"orderNo"` MaterialCode string `json:"materialCode"` BatchNo string `json:"batchNo"` SnList []string `json:"snList"` Qty int `json:"qty"` Operator string `json:"operator"` Reviewer string `json:"reviewer"` TargetDock string `json:"targetDock"` ZoneCode string `json:"zoneCode"` Remark string `json:"remark"` } // deductStockHandler 出库扣减 // body: { orderNo, materialCode, batchNo?, snList?[], qty, operator, targetDock? } // 强约束:累计出库 ≤ 工单 BOM 需求量;等于时领料完结 func deductStockHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req deductStockRequest if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } if req.OrderNo == "" || req.MaterialCode == "" { fail(w, http.StatusBadRequest, "orderNo 和 materialCode 必填") return } // 1. 工单台账强约束:累计出库 ≤ 总需求 ledger, err := ctx.EntClient.OrderMaterialLedger.Query(). Where( ordermaterialledger.OrderNoEQ(req.OrderNo), ordermaterialledger.MaterialCodeEQ(req.MaterialCode), ). Only(ctx0()) if err != nil { fail(w, http.StatusBadRequest, "工单物料台账不存在,请先同步工单 BOM: "+req.OrderNo+"/"+req.MaterialCode) return } if ledger.Status == "领料完结" { fail(w, http.StatusConflict, "工单领料已完结,禁止再出库") return } // 实际扣减数量 actualQty := req.Qty if req.BatchNo == "" && len(req.SnList) == 0 { fail(w, http.StatusBadRequest, "batchNo 或 snList 至少提供一个") return } outboundNo := "OB" + uuid.NewString()[:12] // 2. 创建出库单 ob, err := ctx.EntClient.OutboundOrder.Create(). SetOutboundNo(outboundNo). SetOutboundType("workorder"). SetNillableOrderNo(strPtr(req.OrderNo)). SetMaterialCode(req.MaterialCode). SetManageMode(1). SetNillableBatchNo(strPtr(req.BatchNo)). SetNillableZoneCode(strPtr(req.ZoneCode)). SetQuantity(actualQty). SetNillableOperator(strPtr(req.Operator)). SetNillableReviewer(strPtr(req.Reviewer)). SetNillableTargetDock(strPtr(req.TargetDock)). SetNillableRemark(strPtr(req.Remark)). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "创建出库单失败: "+err.Error()) return } // 3. 扣减批次 if req.BatchNo != "" { batch, err := ctx.EntClient.InventoryBatch.Query(). Where(inventorybatch.BatchNoEQ(req.BatchNo)). Only(ctx0()) if err != nil { fail(w, http.StatusBadRequest, "批次不存在: "+req.BatchNo) return } if batch.Quantity < req.Qty { fail(w, http.StatusConflict, "批次库存不足: "+req.BatchNo+" 当前 "+itoa(batch.Quantity)) return } ctx.EntClient.InventoryBatch.UpdateOneID(batch.ID). SetQuantity(batch.Quantity - req.Qty). SetLockedQty(batch.LockedQty - min(req.Qty, batch.LockedQty)). ExecX(ctx0()) ctx.EntClient.OutboundDetail.Create(). SetOutboundNo(outboundNo). SetBatchNo(req.BatchNo). SetMaterialCode(req.MaterialCode). SetQuantity(req.Qty). SetNillableOrderNo(strPtr(req.OrderNo)). SaveX(ctx0()) } // 4. 扣减 SN if len(req.SnList) > 0 { for _, sn := range req.SnList { snRec, err := ctx.EntClient.SerialNumber.Query(). Where( serialnumber.SnCodeEQ(sn), serialnumber.StatusIn("在库", "锁定"), ). Only(ctx0()) if err != nil { fail(w, http.StatusBadRequest, "SN 不可出库: "+sn) return } ctx.EntClient.SerialNumber.UpdateOneID(snRec.ID). SetStatus("出库"). SetCurrentZone(req.ZoneCode). ExecX(ctx0()) ctx.EntClient.OutboundDetail.Create(). SetOutboundNo(outboundNo). SetSnCode(sn). SetMaterialCode(req.MaterialCode). SetQuantity(1). SetNillableOrderNo(strPtr(req.OrderNo)). SaveX(ctx0()) } actualQty = len(req.SnList) } // 5. 更新出库单实际数量 ctx.EntClient.OutboundOrder.UpdateOneID(ob.ID). SetQuantity(actualQty).ExecX(ctx0()) // 6. 更新工单台账:累计出库 newOutQty := ledger.OutQty + actualQty if newOutQty > ledger.TotalQty { // 超过需求:回滚库存(先释放已扣) rollbackDeduct(ctx, req, actualQty) fail(w, http.StatusConflict, "出库数量超过工单 BOM 需求: 累计 "+itoa(ledger.OutQty)+" + "+itoa(actualQty)+" > 需求 "+itoa(ledger.TotalQty)) return } status := ledger.Status if newOutQty >= ledger.TotalQty { status = "领料完结" } ctx.EntClient.OrderMaterialLedger.UpdateOneID(ledger.ID). SetOutQty(newOutQty). SetStatus(status). ExecX(ctx0()) // 7. 出库记录已生成,返回成功 logx.Infof("deduct stock: order=%s material=%s qty=%d", req.OrderNo, req.MaterialCode, actualQty) ok(w, map[string]any{ "outboundNo": outboundNo, "qty": actualQty, "orderNo": req.OrderNo, "ledgerOut": newOutQty, "ledgerTotal": ledger.TotalQty, "status": status, }) } } // rollbackDeduct 出库扣减回滚(超发保护) func rollbackDeduct(ctx *svc.ServiceContext, req deductStockRequest, qty int) { if req.BatchNo != "" { batch, err := ctx.EntClient.InventoryBatch.Query(). Where(inventorybatch.BatchNoEQ(req.BatchNo)).Only(ctx0()) if err == nil { ctx.EntClient.InventoryBatch.UpdateOneID(batch.ID). SetQuantity(batch.Quantity + qty). ExecX(ctx0()) } } for _, sn := range req.SnList { ctx.EntClient.SerialNumber.Update(). Where(serialnumber.SnCodeEQ(sn)). SetStatus("在库").ExecX(ctx0()) } // 释放对应锁定 ctx.EntClient.InventoryLock.Update(). Where(inventorylock.OrderNoEQ(req.OrderNo), inventorylock.StatusEQ("ACTIVE")). SetStatus("RELEASED"). ExecX(ctx0()) } // queryOutboundHandler 出库单查询 func queryOutboundHandler(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) orderNo := r.URL.Query().Get("orderNo") materialCode := r.URL.Query().Get("materialCode") outboundNo := r.URL.Query().Get("outboundNo") q := ctx.EntClient.OutboundOrder.Query() if orderNo != "" { q = q.Where(outboundorder.OrderNoEQ(orderNo)) } if materialCode != "" { q = q.Where(outboundorder.MaterialCodeEQ(materialCode)) } if outboundNo != "" { q = q.Where(outboundorder.OutboundNoEQ(outboundNo)) } 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 } // 附带明细 type row struct { *ent.OutboundOrder Details []*ent.OutboundDetail `json:"details"` } rows := make([]row, 0, len(list)) for _, ob := range list { details, _ := ctx.EntClient.OutboundDetail.Query(). Where(outbounddetail.OutboundNoEQ(ob.OutboundNo)). All(ctx0()) rows = append(rows, row{ob, details}) } ok(w, map[string]any{ "total": total, "list": rows, "page": page, "pageSize": pageSize, }) } } func min(a, b int) int { if a < b { return a } return b }