feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE - WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置 - WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页 - 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面 - Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理 - 清理各项目球形磨遗留代码,新增部署手册.md
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
"bj_power_wms/ent/inventorybatch"
|
||||
"bj_power_wms/ent/inventorylock"
|
||||
"bj_power_wms/ent/ordermaterialledger"
|
||||
"bj_power_wms/ent/outbounddetail"
|
||||
"bj_power_wms/ent/outboundorder"
|
||||
"bj_power_wms/ent/serialnumber"
|
||||
"bj_power_wms/internal/svc"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
// deductStockRequest 出库扣减请求
|
||||
// createOutboundHandler 创建出库单(等价于扣减库存,走同一套约束)
|
||||
func createOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return deductStockHandler(ctx)
|
||||
}
|
||||
|
||||
type deductStockRequest struct {
|
||||
OrderNo string `json:"orderNo"`
|
||||
MaterialCode string `json:"materialCode"`
|
||||
BatchNo string `json:"batchNo"`
|
||||
SnList []string `json:"snList"`
|
||||
Qty int `json:"qty"`
|
||||
Operator string `json:"operator"`
|
||||
Reviewer string `json:"reviewer"`
|
||||
TargetDock string `json:"targetDock"`
|
||||
ZoneCode string `json:"zoneCode"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// deductStockHandler 出库扣减
|
||||
// body: { orderNo, materialCode, batchNo?, snList?[], qty, operator, targetDock? }
|
||||
// 强约束:累计出库 ≤ 工单 BOM 需求量;等于时领料完结
|
||||
func deductStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req deductStockRequest
|
||||
if err := parseJSON(r, &req); err != nil {
|
||||
fail(w, http.StatusBadRequest, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
if req.OrderNo == "" || req.MaterialCode == "" {
|
||||
fail(w, http.StatusBadRequest, "orderNo 和 materialCode 必填")
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 工单台账强约束:累计出库 ≤ 总需求
|
||||
ledger, err := ctx.EntClient.OrderMaterialLedger.Query().
|
||||
Where(
|
||||
ordermaterialledger.OrderNoEQ(req.OrderNo),
|
||||
ordermaterialledger.MaterialCodeEQ(req.MaterialCode),
|
||||
).
|
||||
Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "工单物料台账不存在,请先同步工单 BOM: "+req.OrderNo+"/"+req.MaterialCode)
|
||||
return
|
||||
}
|
||||
if ledger.Status == "领料完结" {
|
||||
fail(w, http.StatusConflict, "工单领料已完结,禁止再出库")
|
||||
return
|
||||
}
|
||||
|
||||
// 实际扣减数量
|
||||
actualQty := req.Qty
|
||||
if req.BatchNo == "" && len(req.SnList) == 0 {
|
||||
fail(w, http.StatusBadRequest, "batchNo 或 snList 至少提供一个")
|
||||
return
|
||||
}
|
||||
|
||||
outboundNo := "OB" + uuid.NewString()[:12]
|
||||
|
||||
// 2. 创建出库单
|
||||
ob, err := ctx.EntClient.OutboundOrder.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetOutboundType("workorder").
|
||||
SetNillableOrderNo(strPtr(req.OrderNo)).
|
||||
SetMaterialCode(req.MaterialCode).
|
||||
SetManageMode(1).
|
||||
SetNillableBatchNo(strPtr(req.BatchNo)).
|
||||
SetNillableZoneCode(strPtr(req.ZoneCode)).
|
||||
SetQuantity(actualQty).
|
||||
SetNillableOperator(strPtr(req.Operator)).
|
||||
SetNillableReviewer(strPtr(req.Reviewer)).
|
||||
SetNillableTargetDock(strPtr(req.TargetDock)).
|
||||
SetNillableRemark(strPtr(req.Remark)).
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "创建出库单失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 扣减批次
|
||||
if req.BatchNo != "" {
|
||||
batch, err := ctx.EntClient.InventoryBatch.Query().
|
||||
Where(inventorybatch.BatchNoEQ(req.BatchNo)).
|
||||
Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "批次不存在: "+req.BatchNo)
|
||||
return
|
||||
}
|
||||
if batch.Quantity < req.Qty {
|
||||
fail(w, http.StatusConflict, "批次库存不足: "+req.BatchNo+" 当前 "+itoa(batch.Quantity))
|
||||
return
|
||||
}
|
||||
ctx.EntClient.InventoryBatch.UpdateOneID(batch.ID).
|
||||
SetQuantity(batch.Quantity - req.Qty).
|
||||
SetLockedQty(batch.LockedQty - min(req.Qty, batch.LockedQty)).
|
||||
ExecX(ctx0())
|
||||
ctx.EntClient.OutboundDetail.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetBatchNo(req.BatchNo).
|
||||
SetMaterialCode(req.MaterialCode).
|
||||
SetQuantity(req.Qty).
|
||||
SetNillableOrderNo(strPtr(req.OrderNo)).
|
||||
SaveX(ctx0())
|
||||
}
|
||||
|
||||
// 4. 扣减 SN
|
||||
if len(req.SnList) > 0 {
|
||||
for _, sn := range req.SnList {
|
||||
snRec, err := ctx.EntClient.SerialNumber.Query().
|
||||
Where(
|
||||
serialnumber.SnCodeEQ(sn),
|
||||
serialnumber.StatusIn("在库", "锁定"),
|
||||
).
|
||||
Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "SN 不可出库: "+sn)
|
||||
return
|
||||
}
|
||||
ctx.EntClient.SerialNumber.UpdateOneID(snRec.ID).
|
||||
SetStatus("出库").
|
||||
SetCurrentZone(req.ZoneCode).
|
||||
ExecX(ctx0())
|
||||
ctx.EntClient.OutboundDetail.Create().
|
||||
SetOutboundNo(outboundNo).
|
||||
SetSnCode(sn).
|
||||
SetMaterialCode(req.MaterialCode).
|
||||
SetQuantity(1).
|
||||
SetNillableOrderNo(strPtr(req.OrderNo)).
|
||||
SaveX(ctx0())
|
||||
}
|
||||
actualQty = len(req.SnList)
|
||||
}
|
||||
|
||||
// 5. 更新出库单实际数量
|
||||
ctx.EntClient.OutboundOrder.UpdateOneID(ob.ID).
|
||||
SetQuantity(actualQty).ExecX(ctx0())
|
||||
|
||||
// 6. 更新工单台账:累计出库
|
||||
newOutQty := ledger.OutQty + actualQty
|
||||
if newOutQty > ledger.TotalQty {
|
||||
// 超过需求:回滚库存(先释放已扣)
|
||||
rollbackDeduct(ctx, req, actualQty)
|
||||
fail(w, http.StatusConflict, "出库数量超过工单 BOM 需求: 累计 "+itoa(ledger.OutQty)+" + "+itoa(actualQty)+" > 需求 "+itoa(ledger.TotalQty))
|
||||
return
|
||||
}
|
||||
status := ledger.Status
|
||||
if newOutQty >= ledger.TotalQty {
|
||||
status = "领料完结"
|
||||
}
|
||||
ctx.EntClient.OrderMaterialLedger.UpdateOneID(ledger.ID).
|
||||
SetOutQty(newOutQty).
|
||||
SetStatus(status).
|
||||
ExecX(ctx0())
|
||||
|
||||
// 7. 出库记录已生成,返回成功
|
||||
logx.Infof("deduct stock: order=%s material=%s qty=%d", req.OrderNo, req.MaterialCode, actualQty)
|
||||
ok(w, map[string]any{
|
||||
"outboundNo": outboundNo,
|
||||
"qty": actualQty,
|
||||
"orderNo": req.OrderNo,
|
||||
"ledgerOut": newOutQty,
|
||||
"ledgerTotal": ledger.TotalQty,
|
||||
"status": status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// rollbackDeduct 出库扣减回滚(超发保护)
|
||||
func rollbackDeduct(ctx *svc.ServiceContext, req deductStockRequest, qty int) {
|
||||
if req.BatchNo != "" {
|
||||
batch, err := ctx.EntClient.InventoryBatch.Query().
|
||||
Where(inventorybatch.BatchNoEQ(req.BatchNo)).Only(ctx0())
|
||||
if err == nil {
|
||||
ctx.EntClient.InventoryBatch.UpdateOneID(batch.ID).
|
||||
SetQuantity(batch.Quantity + qty).
|
||||
ExecX(ctx0())
|
||||
}
|
||||
}
|
||||
for _, sn := range req.SnList {
|
||||
ctx.EntClient.SerialNumber.Update().
|
||||
Where(serialnumber.SnCodeEQ(sn)).
|
||||
SetStatus("在库").ExecX(ctx0())
|
||||
}
|
||||
// 释放对应锁定
|
||||
ctx.EntClient.InventoryLock.Update().
|
||||
Where(inventorylock.OrderNoEQ(req.OrderNo), inventorylock.StatusEQ("ACTIVE")).
|
||||
SetStatus("RELEASED").
|
||||
ExecX(ctx0())
|
||||
}
|
||||
|
||||
// queryOutboundHandler 出库单查询
|
||||
func queryOutboundHandler(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)
|
||||
orderNo := r.URL.Query().Get("orderNo")
|
||||
materialCode := r.URL.Query().Get("materialCode")
|
||||
outboundNo := r.URL.Query().Get("outboundNo")
|
||||
|
||||
q := ctx.EntClient.OutboundOrder.Query()
|
||||
if orderNo != "" {
|
||||
q = q.Where(outboundorder.OrderNoEQ(orderNo))
|
||||
}
|
||||
if materialCode != "" {
|
||||
q = q.Where(outboundorder.MaterialCodeEQ(materialCode))
|
||||
}
|
||||
if outboundNo != "" {
|
||||
q = q.Where(outboundorder.OutboundNoEQ(outboundNo))
|
||||
}
|
||||
|
||||
total, err := q.Count(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
list, err := q.Order(ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 附带明细
|
||||
type row struct {
|
||||
*ent.OutboundOrder
|
||||
Details []*ent.OutboundDetail `json:"details"`
|
||||
}
|
||||
rows := make([]row, 0, len(list))
|
||||
for _, ob := range list {
|
||||
details, _ := ctx.EntClient.OutboundDetail.Query().
|
||||
Where(outbounddetail.OutboundNoEQ(ob.OutboundNo)).
|
||||
All(ctx0())
|
||||
rows = append(rows, row{ob, details})
|
||||
}
|
||||
|
||||
ok(w, map[string]any{
|
||||
"total": total,
|
||||
"list": rows,
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user