package handler import ( "net/http" "time" "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 // inboundRow 列表/导出统一结构:入库单 + 明细总数。 // 注意:明细(批次/SN)不在此内嵌——精密件单可能上万条 SN, // 列表查询若一次性加载全部明细会撑爆内存,故只带总数,明细按需分页加载。 type inboundRow struct { *ent.InboundOrder DetailCount int `json:"detailCount"` } // applyInboundFilters 统一列表与导出的筛选条件 // 结构件(manageMode=1)与精密件(manageMode=2)同主表(inbound_order),按 manageMode 区分类型 func applyInboundFilters(q *ent.InboundOrderQuery, r *http.Request) *ent.InboundOrderQuery { if v := r.URL.Query().Get("inboundNo"); v != "" { q = q.Where(inboundorder.InboundNoContains(v)) } if v := r.URL.Query().Get("materialCode"); v != "" { q = q.Where(inboundorder.MaterialCodeContains(v)) } if v := r.URL.Query().Get("manageMode"); v != "" { if m := atoi(v, 0); m != 0 { q = q.Where(inboundorder.ManageModeEQ(m)) } } if v := r.URL.Query().Get("inboundType"); v != "" { q = q.Where(inboundorder.InboundTypeEQ(v)) } if v := r.URL.Query().Get("keyword"); v != "" { kw := v q = q.Where(inboundorder.Or( inboundorder.MaterialCodeContains(kw), inboundorder.InboundNoContains(kw), inboundorder.MaterialNameContains(kw), )) } if v := r.URL.Query().Get("startDate"); v != "" { if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil { q = q.Where(inboundorder.CreatedAtGTE(t.Unix())) } } if v := r.URL.Query().Get("endDate"); v != "" { if t, err := time.ParseInLocation("2006-01-02", v, time.Local); err == nil { // 含当天全天 q = q.Where(inboundorder.CreatedAtLTE(t.Add(24 * time.Hour).Unix())) } } return q } // buildInboundRows 仅组装入库单 + 各单明细总数(count,代价极低,不加载明细内容) func buildInboundRows(ctx *svc.ServiceContext, list []*ent.InboundOrder) []inboundRow { rows := make([]inboundRow, 0, len(list)) for _, ib := range list { cnt, _ := ctx.EntClient.InboundDetail.Query(). Where(inbounddetail.InboundNoEQ(ib.InboundNo)).Count(ctx0()) rows = append(rows, inboundRow{ib, cnt}) } return rows } // inboundDetailsHandler 入库单明细分页查询(批次/SN)。 // 单独接口、按需加载、分页返回——避免一次拉取上万条 SN 撑爆内存/前端。 // 参数:inboundNo(必填) page pageSize(默认 50) func inboundDetailsHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { inboundNo := r.URL.Query().Get("inboundNo") if inboundNo == "" { fail(w, http.StatusBadRequest, "inboundNo 必填") return } page := atoi(r.URL.Query().Get("page"), 1) pageSize := atoi(r.URL.Query().Get("pageSize"), 50) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 500 { pageSize = 50 } q := ctx.EntClient.InboundDetail.Query(). Where(inbounddetail.InboundNoEQ(inboundNo)) total, err := q.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := q.Order(ent.Desc("created_at")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]any{ "total": total, "list": list, "page": page, "pageSize": pageSize, }) } } // 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) if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } q := ctx.EntClient.InboundOrder.Query() q = applyInboundFilters(q, r) total, err := q.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := q.Order(ent.Desc("created_at")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ok(w, map[string]any{ "total": total, "list": buildInboundRows(ctx, list), "page": page, "pageSize": pageSize, }) } } // exportInboundHandler 入库单导出:按筛选条件返回全部(不分页),前端转 CSV, // 与 /api/outbound/export 保持一致。 func exportInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := ctx.EntClient.InboundOrder.Query() q = applyInboundFilters(q, r) list, err := q.Order(ent.Desc("created_at")).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } rows := buildInboundRows(ctx, list) // xlsx 导出:格式化 = xlsx → 直接生成二进制文件;否则保持 JSON 兼容 if r.URL.Query().Get("format") == "xlsx" { headers := []string{"入库单号", "物料编码", "物料名称", "类型", "数量", "区域", "操作人", "备注", "明细条数", "入库时间"} matrix := make([][]any, 0, len(rows)) for _, row := range rows { ib := row.InboundOrder matrix = append(matrix, []any{ ib.InboundNo, ib.MaterialCode, ib.MaterialName, manageModeLabel(ib.ManageMode), ib.Quantity, ib.ZoneCode, ib.Operator, ib.Remark, row.DetailCount, unixFmt(ib.CreatedAt), }) } sendExcel(w, xlsxFilename("入库管理"), headers, matrix) return } ok(w, map[string]any{"total": len(rows), "list": rows}) } } // 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 }