package handler import ( "net/http" "strconv" "strings" "time" "bj_power_wms/ent" "bj_power_wms/ent/inventory" "bj_power_wms/ent/material" "bj_power_wms/ent/ordermaterialledger" "bj_power_wms/internal/svc" ) // 库存品类(与 schema/inventory.go 保持一致):1=原材料 2=半成品 3=成品 const ( invCatRaw = 1 invCatSemi = 2 invCatFinished = 3 ) // 半成品 / 台账 相关(盘点见 stocktake.go) // // 【统一库存主表收编 2026-09-07】半成品不再写独立 semi_finisheds 表, // 全部落 inventories(manage_mode=2 SN单件, category=2 半成品)。 // 三个 semi 接口签名与响应结构保持不变,MES / 前端零改造平滑切换。 // semiInboundHandler 半成品入库(携带已完成工序) // 与 MES 闭环:MES 工位报工暂存退库时调用本接口,把半成品 SN 入 WMS 库(inventories, category=2)。 // 为保持两系统解耦,materialCode 不强制要求——优先用请求传入,都没有则留空(SN 本身即追溯锚点)。 func semiInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { Sn string `json:"sn"` MaterialCode string `json:"materialCode"` DoneProcessCodes []int `json:"doneProcessCodes"` CompletedProcess string `json:"completedProcess"` ZoneCode string `json:"zoneCode"` OrderNo string `json:"orderNo"` Operator string `json:"operator"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } if req.Sn == "" { fail(w, http.StatusBadRequest, "sn 必填") return } // SN 防重:同一 SN 只允许一条在库/锁定/出库的库存行(含已出库历史,避免重复回流) exists, err := ctx.EntClient.Inventory.Query(). Where(inventory.CategoryEQ(invCatSemi), inventory.SnCodeEQ(req.Sn)). Exist(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } if exists { fail(w, http.StatusConflict, "半成品已存在: "+req.Sn) return } // 已完成工序:MES 传 doneProcessCodes([]int) 优先,否则用 completedProcess 字符串 completed := req.CompletedProcess if len(req.DoneProcessCodes) > 0 { parts := make([]string, 0, len(req.DoneProcessCodes)) for _, c := range req.DoneProcessCodes { parts = append(parts, strconv.Itoa(c)) } completed = strings.Join(parts, ",") } // 物料编码:以请求传入为准;名称从物料档案补齐(档案缺失则用编码兜底) materialCode := req.MaterialCode materialName := materialCode if materialCode != "" { if m, err := ctx.EntClient.Material.Query().Where(material.CodeEQ(materialCode)).First(ctx0()); err == nil && m != nil { materialName = m.Name } } rec, err := ctx.EntClient.Inventory.Create(). SetManageMode(2). SetCategory(invCatSemi). SetSnCode(req.Sn). SetMaterialCode(materialCode). SetMaterialName(materialName). SetQuantity(1). SetQualityStatus("未检"). SetNillableZoneCode(strPtr(req.ZoneCode)). SetStatus("在库"). SetNillableInboundNo(strPtr(req.OrderNo)). SetOrderNo(req.OrderNo). SetCompletedProcess(completed). SetRemark("半成品完工暂存回流"). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ctx.EventLog.Write(ctx0(), "semi.inbound", r.Header.Get("X-Username"), "semi", req.Sn, "半成品入库 "+req.Sn, map[string]any{"materialCode": materialCode}) ok(w, semiView(rec)) } } // semiOutboundHandler 半成品出库(重上线):更新 inventories 状态 func semiOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { Sn string `json:"sn"` ZoneCode string `json:"zoneCode"` Operator string `json:"operator"` } if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误") return } if req.Sn == "" { fail(w, http.StatusBadRequest, "sn 必填") return } rec, err := ctx.EntClient.Inventory.Query(). Where(inventory.CategoryEQ(invCatSemi), inventory.SnCodeEQ(req.Sn)). Only(ctx0()) if err != nil { fail(w, http.StatusNotFound, "半成品不存在: "+req.Sn) return } if rec.Status != "在库" { fail(w, http.StatusConflict, "半成品不在库: "+rec.Status) return } ctx.EntClient.Inventory.UpdateOneID(rec.ID). SetStatus("出库"). SetUpdatedAt(time.Now().Unix()). ExecX(ctx0()) ctx.EventLog.Write(ctx0(), "semi.outbound", r.Header.Get("X-Username"), "semi", req.Sn, "半成品出库(重上线) "+req.Sn, nil) ok(w, map[string]any{"sn": req.Sn, "status": "出库"}) } } // semiView 把统一库存行映射为半成品视图(保持原 /api/semi/query 响应结构,前端零改造) type semiViewRow struct { ID int `json:"id"` Sn string `json:"sn"` MaterialCode string `json:"materialCode"` CompletedProcess string `json:"completedProcess"` Quantity int `json:"quantity"` ZoneCode string `json:"zoneCode"` Status string `json:"status"` OrderNo string `json:"orderNo"` CreatedAt int64 `json:"createdAt"` UpdatedAt int64 `json:"updatedAt"` } func semiView(inv *ent.Inventory) *semiViewRow { return &semiViewRow{ ID: inv.ID, Sn: inv.SnCode, MaterialCode: inv.MaterialCode, CompletedProcess: inv.CompletedProcess, Quantity: inv.Quantity, ZoneCode: inv.ZoneCode, Status: inv.Status, OrderNo: inv.OrderNo, CreatedAt: inv.CreatedAt, UpdatedAt: inv.UpdatedAt, } } // querySemiHandler 半成品查询(读统一库存主表 category=2) func querySemiHandler(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") status := r.URL.Query().Get("status") q := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatSemi)) if materialCode != "" { q = q.Where(inventory.MaterialCodeEqualFold(materialCode)) } if status != "" { q = q.Where(inventory.StatusEQ(status)) } total, err := q.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")). Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } rows := make([]*semiViewRow, 0, len(list)) for _, inv := range list { rows = append(rows, semiView(inv)) } ok(w, map[string]any{ "total": total, "list": rows, "page": page, "pageSize": pageSize, }) } } // ledger 相关:queryLedgerHandler 工单物料台账查询 func queryLedgerHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { orderNo := r.URL.Query().Get("orderNo") materialCode := r.URL.Query().Get("materialCode") materialName := r.URL.Query().Get("materialName") status := r.URL.Query().Get("status") page := atoi(r.URL.Query().Get("page"), 1) pageSize := atoi(r.URL.Query().Get("pageSize"), 20) q := ctx.EntClient.OrderMaterialLedger.Query() if orderNo != "" { q = q.Where(ordermaterialledger.OrderNoEQ(orderNo)) } if materialCode != "" { q = q.Where(ordermaterialledger.MaterialCodeEqualFold(materialCode)) } if materialName != "" { q = q.Where(ordermaterialledger.MaterialNameContains(materialName)) } if status != "" { // 状态枚举:待领料 / 部分领料 由「进行中 + 累计出库数」派生(表内只存 进行中/领料完结) // 待领料 = 进行中 且 已出库=0;部分领料 = 进行中 且 已出库>0 switch status { case "待领料": q = q.Where(ordermaterialledger.And( ordermaterialledger.StatusEQ("进行中"), ordermaterialledger.OutQtyEQ(0), )) case "部分领料": q = q.Where(ordermaterialledger.And( ordermaterialledger.StatusEQ("进行中"), ordermaterialledger.OutQtyGT(0), )) case "领料完结": q = q.Where(ordermaterialledger.StatusEQ("领料完结")) case "进行中": // 兼容旧调用:看全部未完结 q = q.Where(ordermaterialledger.StatusEQ("进行中")) default: q = q.Where(ordermaterialledger.StatusEQ(status)) } } total, err := q.Count(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")). 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, }) } }