Files
bj_power/bj_power_wms/internal/handler/backflush.go
T
SunYF 62e45b7379 feat: 完成附件存储重构与磁盘监控功能,同步物料档案与入库单规则更新
1.  新增跨平台磁盘空间监控能力,每日7点自动检测附件目录剩余空间,触发阈值告警
2.  重构附件存储方案为年/月/日/文件类型分层结构,统一MES与WMS的附件管理逻辑
3.  对齐物料档案与入库单的质量状态校验规则,仅合格品计入库存与出库
4.  实现入库单作废功能与区域库位的多级父子结构管理
5.  删除物料简称字段,补充规格型号与单位必填项,统一系统数据口径
6.  新增附件中心与磁盘状态查询接口,完善权限控制与操作日志
2026-09-19 10:54:15 +08:00

214 lines
7.1 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 (
"net/http"
"strconv"
"time"
"bj_power_wms/ent"
"bj_power_wms/ent/inventory"
"bj_power_wms/ent/ordermaterialledger"
"bj_power_wms/internal/svc"
"github.com/google/uuid"
"github.com/zeromicro/go-zero/core/logx"
)
// backflushRequest 反冲扣账请求(MES 报工完成时调用,X-API-TOKEN 内部接口)
// 物料在该道工序被消耗,不依赖事先发料:报工完成即反冲扣库存。
// items 由 MES 按「工单产品 BOM + 当前工序 processCode」算好(含损耗),WMS 只负责 FIFO 扣减与记账。
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 反冲扣账(库存 FIFO 扣减 + 生成 backflush 类型出库单 + 台账累计)
// 事务保证:出库单 + 库存扣减 + 出库明细 + 工单台账 原子提交。
// 任意物料库存不足(FIFO 无法足额)时,扣足可用部分、记短欠日志、不阻塞产线(反冲降级同成品回流)。
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{"outboundNo": "", "consumed": 0, "shortfall": false, "note": "无反冲物料"})
return
}
outboundNo := "BF" + 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()
}
}()
// 1. 创建反冲出库单(type=backflush,数量后置更新)
ob, err := tx.OutboundOrder.Create().
SetOutboundNo(outboundNo).
SetOutboundType("backflush").
SetNillableOrderNo(strPtr(req.OrderNo)).
SetMaterialCode(""). // 反冲单为整份 BOM,主表物料留空,明细承载
SetManageMode(1).
SetQuantity(0).
SetNillableOperator(strPtr(req.Operator)).
SetNillableTargetStation(strPtr(strconv.Itoa(req.StationNo))).
SetRemark("反冲扣账(工序" + strconv.Itoa(req.ProcessCode) + ")").
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "创建反冲单失败: "+err.Error())
return
}
// 2. FIFO 逐物料扣减 + 台账累计
totalConsumed := 0
shortfall := false
for _, it := range req.Items {
if it.MaterialCode == "" || it.Qty <= 0 {
continue
}
consumed, sf := fifoConsume(tx, it.MaterialCode, it.Qty, outboundNo, req.OrderNo)
totalConsumed += consumed
if sf {
shortfall = true
logx.Errorf("backflush shortfall: order=%s material=%s need=%d got=%d", req.OrderNo, it.MaterialCode, it.Qty, consumed)
}
// 台账累计(仅挂工单时)
if req.OrderNo != "" {
ledger, lerr := tx.OrderMaterialLedger.Query().
Where(
ordermaterialledger.OrderNoEQ(req.OrderNo),
ordermaterialledger.MaterialCodeEQ(it.MaterialCode),
).Only(ctx0())
if lerr == nil && ledger != nil {
newOut := ledger.OutQty + consumed
status := ledger.Status
if newOut >= ledger.TotalQty {
status = "领料完结"
}
upd := tx.OrderMaterialLedger.UpdateOneID(ledger.ID).
SetOutQty(newOut).
SetStatus(status)
if status == "领料完结" && ledger.Status != "领料完结" {
upd.SetCompletedAt(time.Now().Unix())
}
if _, lerr = upd.Save(ctx0()); lerr != nil {
fail(w, http.StatusInternalServerError, "更新台账失败: "+lerr.Error())
return
}
}
}
}
// 3. 回写出库单实际数量
if _, err = tx.OutboundOrder.UpdateOneID(ob.ID).SetQuantity(totalConsumed).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
ctx.EventLog.Write(ctx0(), "material.backflush", req.Operator, "backflush", outboundNo,
"反冲扣账 工单 "+req.OrderNo+" 工序"+strconv.Itoa(req.ProcessCode)+" 共消耗 ×"+strconv.Itoa(totalConsumed),
map[string]any{"orderNo": req.OrderNo, "stationNo": req.StationNo, "consumed": totalConsumed, "shortfall": shortfall})
logx.Infof("backflush: order=%s station=%d consumed=%d shortfall=%v", req.OrderNo, req.StationNo, totalConsumed, shortfall)
ok(w, map[string]any{
"outboundNo": outboundNo,
"consumed": totalConsumed,
"shortfall": shortfall,
})
}
}
// fifoConsume 按 created_at 升序(先进先出)扣减某物料库存,跨批次/单件 SN 统一处理。
// 返回实际扣减数;库存不足足额时 shortfall=true(已扣可用部分),不报错。
func fifoConsume(tx *ent.Tx, materialCode string, qty int, outboundNo, orderNo string) (int, bool) {
rows, err := tx.Inventory.Query().
Where(
inventory.MaterialCodeEQ(materialCode),
inventory.StatusEQ("在库"),
// 可用口径(客户口径):仅「合格」可被消耗(未检/不合格/待退 不计可用)
inventory.QualityStatusEQ(QStatusPass),
).
Order(ent.Asc("created_at")).
All(ctx0())
if err != nil {
return 0, true
}
remaining := qty
consumed := 0
for _, row := range rows {
if remaining <= 0 {
break
}
avail := row.Quantity - row.LockedQty
if avail <= 0 {
continue
}
take := avail
if take > remaining {
take = remaining
}
if row.ManageMode == 2 {
// SN 单件:整件消耗并置出库
aff, e := tx.Inventory.Update().
Where(inventory.ID(row.ID), inventory.StatusEQ("在库")).
SetStatus("出库").Save(ctx0())
if e != nil || aff == 0 {
continue
}
if _, e = tx.OutboundDetail.Create().
SetOutboundNo(outboundNo).
SetSnCode(row.SnCode).
SetMaterialCode(materialCode).
SetQuantity(1).
SetNillableOrderNo(strPtr(orderNo)).Save(ctx0()); e != nil {
return consumed, true
}
consumed += 1
remaining -= 1
} else {
// 批次:条件原子扣减(quantity>=take 才减,避免超扣/并发负库存)
aff, e := tx.Inventory.Update().
Where(inventory.ID(row.ID), inventory.QuantityGTE(take)).
AddQuantity(-take).
AddLockedQty(-min(take, row.LockedQty)).
Save(ctx0())
if e != nil || aff == 0 {
continue
}
if _, e = tx.OutboundDetail.Create().
SetOutboundNo(outboundNo).
SetBatchNo(row.BatchNo).
SetMaterialCode(materialCode).
SetQuantity(take).
SetNillableOrderNo(strPtr(orderNo)).Save(ctx0()); e != nil {
return consumed, true
}
consumed += take
remaining -= take
}
}
return consumed, remaining > 0
}