Files
bj_power/bj_power_wms/internal/handler/backflush.go
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

127 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
})
}
}