package handler import ( "net/http" "time" "bj_power_wms/ent/inboundorder" "bj_power_wms/ent/inventory" "bj_power_wms/ent/material" "bj_power_wms/ent/ordermaterialledger" "bj_power_wms/ent/outboundorder" "bj_power_wms/internal/svc" ) // ledgerSyncRequest MES 下发工单 BOM 后同步台账 type ledgerSyncRequest struct { OrderNo string `json:"orderNo"` ProductCode string `json:"productCode"` Operator string `json:"operator"` Items []struct { MaterialCode string `json:"materialCode"` MaterialName string `json:"materialName"` TotalQty int `json:"totalQty"` } `json:"items"` } // ledgerSyncHandler 同步/更新工单物料台账(幂等:按 orderNo+materialCode upsert) func ledgerSyncHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req ledgerSyncRequest if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } if req.OrderNo == "" || len(req.Items) == 0 { fail(w, http.StatusBadRequest, "orderNo 和 items 必填") return } saved := 0 for _, it := range req.Items { existing, err := ctx.EntClient.OrderMaterialLedger.Query(). Where( ordermaterialledger.OrderNoEQ(req.OrderNo), ordermaterialledger.MaterialCodeEQ(it.MaterialCode), ).Only(ctx0()) if err == nil { ctx.EntClient.OrderMaterialLedger.UpdateOneID(existing.ID). SetTotalQty(it.TotalQty). SetStatus("进行中"). SetUpdatedAt(time.Now().Unix()). ExecX(ctx0()) } else { ctx.EntClient.OrderMaterialLedger.Create(). SetOrderNo(req.OrderNo). SetMaterialCode(it.MaterialCode). SetNillableMaterialName(strPtr(it.MaterialName)). SetTotalQty(it.TotalQty). SetOutQty(0). SetStatus("进行中"). SaveX(ctx0()) } saved++ } ok(w, map[string]any{"orderNo": req.OrderNo, "synced": saved}) } } // internalDisplayHandler 库存总览(55寸大屏免登录用) func internalDisplayHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // 批次库存(结构件)总量(按质量状态) batches, _ := ctx.EntClient.Inventory.Query().Where(inventory.ManageModeEQ(1)).All(ctx0()) totalQty := 0 lockedQty := 0 type zoneRow struct { Zone, Quality string Qty int } zones := map[string]int{} batchCount := len(batches) for _, b := range batches { totalQty += b.Quantity lockedQty += b.LockedQty zc := b.ZoneCode if zc == "" { zc = "未分区" } zones[zc+"/"+b.QualityStatus] += b.Quantity } snInStock, _ := ctx.EntClient.Inventory.Query().Where(inventory.ManageModeEQ(2), inventory.StatusIn("在库", "锁定")).Count(ctx0()) snOut, _ := ctx.EntClient.Inventory.Query().Where(inventory.ManageModeEQ(2), inventory.StatusEQ("出库")).Count(ctx0()) // 半成品统计:统一库存主表 category=2(semi_finisheds 已收编废弃) semiInStock, _ := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatSemi), inventory.StatusEQ("在库")).Count(ctx0()) // 成品在库统计:category=3 finishedInStock, _ := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatFinished), inventory.StatusIn("在库", "锁定")).Count(ctx0()) pkgCount, _ := ctx.EntClient.OutboundOrder.Query().Where(outboundorder.BoxNoNEQ("")).Count(ctx0()) dayStart := time.Now().Truncate(24 * time.Hour).Unix() inToday, _ := ctx.EntClient.InboundOrder.Query(). Where(inboundorder.CreatedAtGTE(dayStart)).Count(ctx0()) outToday, _ := ctx.EntClient.OutboundOrder.Query(). Where(outboundorder.CreatedAtGTE(dayStart)).Count(ctx0()) ok(w, map[string]any{ "batchCount": batchCount, "totalQty": totalQty, "lockedQty": lockedQty, "snInStock": snInStock, "snOut": snOut, "semiInStock": semiInStock, "finishedInStock": finishedInStock, "packageCount": pkgCount, "inToday": inToday, "outToday": outToday, "zoneDetail": zones, "time": time.Now().Format("2006-01-02 15:04:05"), }) } } // finishedInboundRequest 成品完工回流入库请求(MES 完工后调用,X-API-TOKEN 内部接口) // 定稿(2026-09-07):成品 SN 由 MES 系统生成(系统必发号,实物可不贴码,允许手填覆盖), // SN 与实物码解耦;完工品经本接口自动回流 WMS 统一库存主表(manage_mode=2 SN单件, category=3 成品)。 type finishedInboundRequest struct { ProductCode string `json:"productCode"` // 产品编码(成品物料编码) ProductName string `json:"productName"` // 产品名称(档案缺失时兜底展示) Sn string `json:"sn"` // 成品 SN(系统生成或手填,全局唯一) OrderNo string `json:"orderNo"` // 关联工单号 ZoneCode string `json:"zoneCode"` // 入库区域(可空=未分区) QualityStatus string `json:"qualityStatus"` // 默认 合格(完工已过全工序校验) SnSource string `json:"snSource"` // AUTO=系统生成 / MANUAL=手填,仅记录 Operator string `json:"operator"` } // finishedInboundHandler 成品完工回流入库 // 物料档案缺失时不阻塞回流(库存行 material_code 仍落产品编码,名称用请求传入兜底), // 记 eventlog 提示补建档——编码主数据仍以 WMS 为单一来源,产线不被档案缺失卡死。 func finishedInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req finishedInboundRequest if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } if req.Sn == "" || req.ProductCode == "" { fail(w, http.StatusBadRequest, "sn 与 productCode 必填") return } quality := req.QualityStatus if quality == "" { quality = "合格" } // SN 全局防重(同品类成品 + 半成品 + 精密件 SN 都不允许撞号) dup, err := ctx.EntClient.Inventory.Query(). Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(req.Sn)).Exist(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } if dup { fail(w, http.StatusConflict, "成品 SN 已存在库存: "+req.Sn) return } // 物料档案补名称;缺失时记日志(不阻塞回流) materialName := req.ProductName archiveMiss := false if m, err := ctx.EntClient.Material.Query().Where(material.CodeEQ(req.ProductCode)).First(ctx0()); err == nil && m != nil { materialName = m.Name } else { archiveMiss = true } rec, err := ctx.EntClient.Inventory.Create(). SetManageMode(2). SetCategory(invCatFinished). SetMaterialCode(req.ProductCode). SetMaterialName(materialName). SetSnCode(req.Sn). SetQuantity(1). SetQualityStatus(quality). SetNillableZoneCode(strPtr(req.ZoneCode)). SetStatus("在库"). SetInboundNo("MES:" + req.OrderNo). SetOrderNo(req.OrderNo). SetRemark("成品完工回流(SN来源:" + snSourceLabel(req.SnSource) + ")"). Save(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, err.Error()) return } ctx.EventLog.Write(ctx0(), "finished.inbound", req.Operator, "finished", req.Sn, "成品完工回流入库 "+req.Sn, map[string]any{ "productCode": req.ProductCode, "orderNo": req.OrderNo, "snSource": req.SnSource, "archiveMiss": archiveMiss, }) ok(w, rec) } } func snSourceLabel(s string) string { if s == "MANUAL" { return "手填" } return "系统生成" }