package handler import ( "net/http" "bj_power_wms/ent" "bj_power_wms/ent/inbounddetail" "bj_power_wms/ent/inboundorder" "bj_power_wms/ent/inventory" "bj_power_wms/ent/material" "bj_power_wms/internal/svc" "github.com/google/uuid" "github.com/zeromicro/go-zero/core/logx" ) // createInboundHandler 入库 // 结构件: { inboundType, materialCode, batchNo, quantity, zoneCode, operator } // 精密件: { inboundType, materialCode, snList: [sn...], zoneCode, operator } // // 事务保证:入库单 + 库存行(inventory) + 入库明细 三者要么全成功,要么全回滚,杜绝半成品状态。 func createInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { InboundType string `json:"inboundType"` // purchase/semi/finished/return MaterialCode string `json:"materialCode"` BatchNo string `json:"batchNo"` Quantity int `json:"quantity"` SnList []string `json:"snList"` ZoneCode string `json:"zoneCode"` Operator string `json:"operator"` ProductionDate string `json:"productionDate"` Supplier string `json:"supplier"` Remark string `json:"remark"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } if req.MaterialCode == "" { fail(w, http.StatusBadRequest, "物料编码必填") return } if req.ZoneCode == "" { fail(w, http.StatusBadRequest, "区域必填,请先选择入库区域") return } // 物料档案 m, err := ctx.EntClient.Material.Query(). Where(material.CodeEQ(req.MaterialCode)). Only(ctx0()) if err != nil { fail(w, http.StatusBadRequest, "物料不存在: "+req.MaterialCode) return } inboundNo := "IB" + uuid.NewString()[:12] // 开启事务:入库单 + 库存行 + 明细 原子提交 tx, err := ctx.EntClient.Tx(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error()) return } committed := false defer func() { if !committed { _ = tx.Rollback() } }() ib, err := tx.InboundOrder.Create(). SetInboundNo(inboundNo). SetNillableInboundType(strPtr(req.InboundType)). SetMaterialCode(req.MaterialCode). SetNillableMaterialName(strPtr(m.Name)). SetManageMode(m.ManageMode). SetNillableZoneCode(strPtr(req.ZoneCode)). SetQuantity(0). SetNillableOperator(strPtr(req.Operator)). SetNillableRemark(strPtr(req.Remark)). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "创建入库单失败: "+err.Error()) return } totalQty := 0 // 结构件(批次管理) if m.ManageMode == 1 { // 结构件走批次入库,不允许携带 SN 清单 if len(req.SnList) > 0 { fail(w, http.StatusBadRequest, "该物料为结构件(按批次管理),不应提交 SN 清单,请使用『结构件入库』") return } batchNo := req.BatchNo if batchNo == "" { batchNo = genBatchNo(req.MaterialCode) } qty := req.Quantity if qty <= 0 { fail(w, http.StatusBadRequest, "数量必填且大于 0,请输入本次入库数量") return } // 同批次追加(多批到货追加) existing, e := tx.Inventory.Query(). Where(inventory.ManageModeEQ(1), inventory.BatchNoEQ(batchNo)).Only(ctx0()) if e == nil { if _, err = tx.Inventory.UpdateOneID(existing.ID). AddQuantity(qty). SetNillableSupplier(strPtr(req.Supplier)). Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "追加批次失败: "+err.Error()) return } } else { if _, err = tx.Inventory.Create(). SetManageMode(1). SetMaterialCode(req.MaterialCode). SetNillableMaterialName(strPtr(m.Name)). SetBatchNo(batchNo). SetQuantity(qty).SetLockedQty(0). SetNillableProductionDate(strPtr(req.ProductionDate)). SetNillableSupplier(strPtr(req.Supplier)). SetQualityStatus("未检"). SetNillableZoneCode(strPtr(req.ZoneCode)). SetStatus("在库"). SetInboundNo(inboundNo). Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "创建库存行失败: "+err.Error()) return } } // 明细 if _, err = tx.InboundDetail.Create(). SetInboundNo(inboundNo). SetBatchNo(batchNo). SetMaterialCode(req.MaterialCode). SetQuantity(qty). Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "创建入库明细失败: "+err.Error()) return } totalQty = qty } // 精密件(SN 管理) if m.ManageMode == 2 { if len(req.SnList) == 0 { fail(w, http.StatusBadRequest, "该物料为精密件(按 SN 管理),请至少录入 1 条序列号(SN)") return } for _, sn := range req.SnList { // 查重 exists, _ := tx.Inventory.Query(). Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(sn)). Exist(ctx0()) if exists { fail(w, http.StatusConflict, "SN 已存在: "+sn) return } if _, err = tx.Inventory.Create(). SetManageMode(2). SetMaterialCode(req.MaterialCode). SetNillableMaterialName(strPtr(m.Name)). SetSnCode(sn). SetQuantity(1).SetLockedQty(0). SetQualityStatus("未检"). SetNillableZoneCode(strPtr(req.ZoneCode)). SetStatus("在库"). SetInboundNo(inboundNo). Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "创建 SN 库存行失败: "+err.Error()) return } if _, err = tx.InboundDetail.Create(). SetInboundNo(inboundNo). SetSnCode(sn). SetMaterialCode(req.MaterialCode). SetQuantity(1). Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "创建入库明细失败: "+err.Error()) return } } totalQty = len(req.SnList) } // 更新入库单数量 if _, err = tx.InboundOrder.UpdateOneID(ib.ID). SetQuantity(totalQty).Save(ctx0()); err != nil { fail(w, http.StatusInternalServerError, "更新入库单数量失败: "+err.Error()) return } if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error()) return } committed = true logx.Infof("inbound: no=%s material=%s qty=%d by=%s", inboundNo, req.MaterialCode, totalQty, req.Operator) ok(w, map[string]any{ "inboundNo": inboundNo, "quantity": totalQty, "batchNo": req.BatchNo, "snCount": len(req.SnList), }) } } // excelInboundHandler 实现见 excel.go // queryInboundHandler 入库单查询 func queryInboundHandler(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) materialCode := r.URL.Query().Get("materialCode") inboundType := r.URL.Query().Get("inboundType") inboundNo := r.URL.Query().Get("inboundNo") q := ctx.EntClient.InboundOrder.Query() if materialCode != "" { q = q.Where(inboundorder.MaterialCodeEQ(materialCode)) } if inboundType != "" { q = q.Where(inboundorder.InboundTypeEQ(inboundType)) } if inboundNo != "" { q = q.Where(inboundorder.InboundNoEQ(inboundNo)) } 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.InboundOrder Details []*ent.InboundDetail `json:"details"` } rows := make([]row, 0, len(list)) for _, ib := range list { details, _ := ctx.EntClient.InboundDetail.Query(). Where(inbounddetail.InboundNoEQ(ib.InboundNo)). All(ctx0()) rows = append(rows, row{ib, details}) } ok(w, map[string]any{ "total": total, "list": rows, "page": page, "pageSize": pageSize, }) } } // genBatchNo 生成批次号:日期 + 自增 func genBatchNo(materialCode string) string { // TODO: 使用数据库序列或 Redis INCR 保证自增唯一 // 暂用时间戳 + 随机后缀 now := nowStr() return "B" + now + materialCodeSuffix(materialCode) } func nowStr() string { // YYYYMMDDHHMMSS const layout = "20060102150405" return timeNow().Format(layout) } func materialCodeSuffix(code string) string { if len(code) > 4 { code = code[len(code)-4:] } return code }