package handler import ( "fmt" "net/http" "strconv" "time" "bj_power_wms/ent/ordermaterialledger" "bj_power_wms/internal/svc" "github.com/zeromicro/go-zero/core/logx" ) // backflushRequest 报工反冲核销请求(MES 报工完成时调用,X-API-TOKEN 内部接口) // // 口径(2026-09-19 定稿 · 方案 A「出库扣减,反冲只核销」): // 一次物理位移 = 一次库存账变。备料出库时库存已经扣过,报工消耗**不再动库存、不再生成出库单**, // 只把台账「已消耗(consumed_qty)」累加,供 已发出 / 已接料 / 已消耗 三方对账。 // // 核销上限 = net_out_qty − consumed_qty,其中 net_out_qty = out_qty − returned_qty。 // 上限的意义:没走出库的料(工位自取辅料、现场替换)不会被核销成负数, // 避免出现「库存从未扣过、台账却记了消耗」的漏扣假象。 type backflushRequest struct { OrderNo string `json:"orderNo"` // 关联工单号(空=不做核销) StationNo int `json:"stationNo"` // 当前工位号(记录用) ProcessCode int `json:"processCode"` // 当前工序号(记录用) Operator string `json:"operator"` Items []backflushItem `json:"items"` // 本次消耗的物料明细 } type backflushItem struct { MaterialCode string `json:"materialCode"` Qty int `json:"qty"` // 该工序单件消耗(已含损耗)* 完成件数 } // backflushInternalHandler 报工反冲核销:只累加台账 consumed_qty。 // 不扣库存、不写出库单、不写 out_qty。库存不足/无台账不阻塞产线,只记 shortfall 供对账。 func backflushInternalHandler(ctx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req backflushRequest if err := parseJSON(r, &req); err != nil { fail(w, http.StatusBadRequest, "参数错误: "+err.Error()) return } if len(req.Items) == 0 { ok(w, map[string]any{"consumed": 0, "shortfall": false, "note": "无反冲物料"}) return } if req.OrderNo == "" { // 不挂工单的消耗没有台账可承载,不阻塞产线 logx.Infof("backflush skipped: no orderNo, station=%d", req.StationNo) ok(w, map[string]any{"consumed": 0, "shortfall": false, "note": "未挂工单,不做核销"}) return } tx, err := ctx.EntClient.Tx(ctx0()) if err != nil { fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error()) return } committed := false defer func() { if !committed { _ = tx.Rollback() } }() totalConsumed := 0 shortfall := false notes := make([]string, 0, 4) for _, it := range req.Items { if it.MaterialCode == "" || it.Qty <= 0 { continue } ledger, lerr := tx.OrderMaterialLedger.Query(). Where( ordermaterialledger.OrderNoEQ(req.OrderNo), ordermaterialledger.MaterialCodeEQ(it.MaterialCode), ).Only(ctx0()) if lerr != nil || ledger == nil { shortfall = true notes = append(notes, it.MaterialCode+" 无台账") continue } // 可核销量 = 净领用 − 已消耗(负数归零:退料后可能出现「已消耗 > 净领用」) capa := (ledger.OutQty - ledger.ReturnedQty) - ledger.ConsumedQty if capa < 0 { capa = 0 } take := it.Qty if take > capa { take = capa shortfall = true notes = append(notes, fmt.Sprintf("%s 超出可核销量(需%d/可%d)", it.MaterialCode, it.Qty, capa)) } if take <= 0 { continue } if _, uerr := tx.OrderMaterialLedger.UpdateOneID(ledger.ID). AddConsumedQty(take). SetUpdatedAt(time.Now().Unix()). Save(ctx0()); uerr != nil { fail(w, http.StatusInternalServerError, "更新台账失败: "+uerr.Error()) return } totalConsumed += take } if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error()) return } committed = true ctx.EventLog.Write(ctx0(), "material.backflush", req.Operator, "order_material_ledger", req.OrderNo, "报工消耗核销 工单 "+req.OrderNo+" 工序"+strconv.Itoa(req.ProcessCode)+" 共核销 ×"+strconv.Itoa(totalConsumed), map[string]any{"orderNo": req.OrderNo, "stationNo": req.StationNo, "consumed": totalConsumed, "shortfall": shortfall, "notes": notes}) logx.Infof("backflush(consume-only): order=%s station=%d consumed=%d shortfall=%v", req.OrderNo, req.StationNo, totalConsumed, shortfall) ok(w, map[string]any{ "consumed": totalConsumed, "shortfall": shortfall, "notes": notes, }) } }