refactor(wms): 合并库存表为统一inventory,重构业务逻辑

1. 合并inventory_batch与serial_number为统一inventory表,用manage_mode区分结构件批次/精密件SN
2. 移除冗余的material_type字段与相关逻辑
3. 重构库存查询、检验、入库出库等业务逻辑适配新表结构
4. 调整前端路由与菜单,拆分基础数据为区域维护和物料档案
5. 优化入库校验逻辑,避免空单问题
6. 调整Vite构建配置,关闭自动清空输出目录
7. 为各模块添加详细中文注释,统一业务术语
8. 生成并更新Ent自动代码文件
This commit is contained in:
SunYF
2026-09-02 08:21:11 +08:00
parent 47573e44b0
commit 68f50ee46e
70 changed files with 5895 additions and 9818 deletions
+162 -145
View File
@@ -1,110 +1,144 @@
package handler
import (
"fmt"
"net/http"
"strings"
"bj_power_wms/ent/inventorybatch"
"bj_power_wms/ent/inventorylock"
"bj_power_wms/ent/serialnumber"
"bj_power_wms/internal/svc"
"entgo.io/ent/dialect/sql"
"bj_power_wms/ent"
"bj_power_wms/ent/inventory"
"bj_power_wms/ent/inventorylock"
"bj_power_wms/ent/predicate"
"bj_power_wms/internal/svc"
)
// queryStockHandler 库存查询(MES 查料)
// 支持按物料编码区域关键字、类型(batch/serial)过滤
// queryStockHandler 库存查询(MES 查料 / 客户端查料
// 统一查询 inventory 一张表:按 物料编码/区域/关键字/管理粒度(manageMode) 过滤
// 返回单一 list(结构件与精密件同表),前端统一展示、统一导出。
func queryStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
materialCode := r.URL.Query().Get("materialCode")
zoneCode := r.URL.Query().Get("zoneCode")
stockType := r.URL.Query().Get("type") // batch / serial
manageMode := r.URL.Query().Get("manageMode") // 1/2/空=全部
keyword := r.URL.Query().Get("keyword")
page := atoi(r.URL.Query().Get("page"), 1)
pageSize := atoi(r.URL.Query().Get("pageSize"), 50)
result := map[string]any{
"batch": []any{},
"serial": []any{},
"total": 0,
}
// 批次库存
batchQ := ctx.EntClient.InventoryBatch.Query()
q := ctx.EntClient.Inventory.Query()
// 仅展示在库/锁定(出库/使用/报废不计入库存)
q = q.Where(inventory.StatusIn("在库", "锁定"))
// 结构件隐藏数量为 0 的行
q = q.Where(inventory.Or(inventory.ManageModeEQ(2), inventory.QuantityGT(0)))
if materialCode != "" {
batchQ = batchQ.Where(inventorybatch.MaterialCodeEQ(materialCode))
q = q.Where(inventory.MaterialCodeEQ(materialCode))
}
if zoneCode != "" {
batchQ = batchQ.Where(inventorybatch.ZoneCodeEQ(zoneCode))
q = q.Where(inventory.ZoneCodeEQ(zoneCode))
}
if manageMode == "1" {
q = q.Where(inventory.ManageModeEQ(1))
} else if manageMode == "2" {
q = q.Where(inventory.ManageModeEQ(2))
}
if keyword != "" {
batchQ = batchQ.Where(
inventorybatch.Or(
inventorybatch.BatchNoContains(keyword),
inventorybatch.MaterialNameContains(keyword),
),
)
q = q.Where(inventory.Or(
inventory.BatchNoContains(keyword),
inventory.MaterialCodeContains(keyword),
inventory.SnCodeContains(keyword),
inventory.MaterialNameContains(keyword),
))
}
batchTotal, err := batchQ.Count(ctx0())
total, err := q.Count(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
batches, err := batchQ.Order(ent.Desc("id")).
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
}
// SN 库存
serialQ := ctx.EntClient.SerialNumber.Query()
if materialCode != "" {
serialQ = serialQ.Where(serialnumber.MaterialCodeEQ(materialCode))
}
if zoneCode != "" {
serialQ = serialQ.Where(serialnumber.CurrentZoneEQ(zoneCode))
}
if keyword != "" {
serialQ = serialQ.Where(
serialnumber.Or(
serialnumber.SnCodeContains(keyword),
serialnumber.MaterialCodeContains(keyword),
),
)
}
serialTotal, err := serialQ.Count(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
serials, err := serialQ.Order(ent.Desc("id")).
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
if stockType == "batch" {
result["batch"] = batches
result["total"] = batchTotal
} else if stockType == "serial" {
result["serial"] = serials
result["total"] = serialTotal
} else {
result["batch"] = batches
result["serial"] = serials
result["total"] = batchTotal + serialTotal
}
result["page"] = page
result["pageSize"] = pageSize
ok(w, result)
ok(w, map[string]any{
"list": list,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
}
// lockStockHandler 工单锁定库存
// zoneSummaryHandler 区域库存汇总(按 区域 + 质量状态 + 类型 聚合)
// GET /api/stock/zone-summary
// 返回 rows: [{zoneCode, zoneName, quality, type(批次/精密件), qty}]
// 仅按"区域(Z01~Z04)"维度聚合,不引入货架/层/格(业务决策:小货架高频取货无需精确货位索引)。
func zoneSummaryHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
zones, _ := ctx.EntClient.Zone.Query().All(ctx0())
zoneNameMap := map[string]string{}
for _, z := range zones {
zoneNameMap[z.ZoneCode] = z.ZoneName
}
rows, _ := ctx.EntClient.Inventory.Query().
Where(inventory.StatusIn("在库", "锁定")).All(ctx0())
type summRow struct {
ZoneCode string `json:"zoneCode"`
ZoneName string `json:"zoneName"`
Quality string `json:"quality"`
Type string `json:"type"` // 批次/精密件
Qty int `json:"qty"`
}
agg := map[string]int{} // key: zc|quality|type
for _, inv := range rows {
zc := inv.ZoneCode
if zc == "" {
zc = "未分区"
}
t := "批次"
if inv.ManageMode == 2 {
t = "精密件"
}
key := zc + "|" + inv.QualityStatus + "|" + t
if inv.ManageMode == 1 {
agg[key] += inv.Quantity
} else {
agg[key] += 1
}
}
out := make([]summRow, 0, len(agg))
for k, v := range agg {
parts := strings.Split(k, "|")
name := zoneNameMap[parts[0]]
if name == "" {
name = parts[0]
}
out = append(out, summRow{parts[0], name, parts[1], parts[2], v})
}
ok(w, map[string]any{"rows": out})
}
}
// invAvailGTE 自定义谓词:可用量(quantity - locked_qty) >= need
// 用于锁库存时的原子条件校验,避免并发超锁。
func invAvailGTE(need int) predicate.Inventory {
return predicate.Inventory(func(s *sql.Selector) {
s.Where(sql.ExprP(
fmt.Sprintf("%s.%s - %s.%s >= ?",
inventory.Table, inventory.FieldQuantity,
inventory.Table, inventory.FieldLockedQty),
need,
))
})
}
// lockStockHandler 工单锁定库存(库存占用)
// body: { orderNo, items: [{ materialCode, batchNo?, sn?, qty }] }
// 结构件锁批次数量;精密件锁 SN
// 锁定为"增量"操作:结构件 locked_qty += qty;精密件状态置 锁定。绝不覆盖终值。
// 锁批次用原子增量 + 可用量条件校验;锁 SN 用状态条件原子更新,杜绝超锁与并发冲突。
func lockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
@@ -130,70 +164,64 @@ func lockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
if item.MaterialCode == "" {
continue
}
// 精密件:锁 SN
// 精密件:锁 SN(原子:仅 在库 才能锁)
if item.Sn != "" {
sn, err := ctx.EntClient.SerialNumber.Query().
Where(
serialnumber.SnCodeEQ(item.Sn),
serialnumber.MaterialCodeEQ(item.MaterialCode),
serialnumber.StatusEQ("在库"),
).
Only(ctx0())
if err != nil {
fail(w, http.StatusBadRequest, "SN 不在库或不存在: "+item.Sn)
return
}
_, err = ctx.EntClient.InventoryLock.Create().
SetTargetType("SN").
SetTargetID(sn.SnCode).
SetMaterialCode(item.MaterialCode).
SetLockedQty(1).
SetOrderNo(req.OrderNo).
SetStatus("ACTIVE").
aff, err := ctx.EntClient.Inventory.Update().
Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(item.Sn), inventory.StatusEQ("在库")).
SetStatus("锁定").SetLockedQty(1).
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "锁定 SN 失败: "+err.Error())
return
}
// SN 状态 → 锁定
ctx.EntClient.SerialNumber.UpdateOneID(sn.ID).
SetStatus("锁定").ExecX(ctx0())
if aff == 0 {
fail(w, http.StatusBadRequest, "SN 不在库或不存在: "+item.Sn)
return
}
ctx.EntClient.InventoryLock.Create().
SetTargetType("SN").
SetTargetID(item.Sn).
SetMaterialCode(item.MaterialCode).
SetLockedQty(1).
SetOrderNo(req.OrderNo).
SetStatus("ACTIVE").
SaveX(ctx0())
locked++
continue
}
// 结构件:锁批次数量
// 结构件:锁批次数量(原子增量 + 可用量校验)
qty := item.Qty
if qty <= 0 {
qty = 1
}
batch, err := ctx.EntClient.InventoryBatch.Query().
Where(inventorybatch.BatchNoEQ(item.BatchNo)).
Only(ctx0())
b, err := ctx.EntClient.Inventory.Query().
Where(inventory.ManageModeEQ(1), inventory.BatchNoEQ(item.BatchNo)).Only(ctx0())
if err != nil {
fail(w, http.StatusBadRequest, "批次不存在: "+item.BatchNo)
return
}
avail := batch.Quantity - batch.LockedQty
avail := b.Quantity - b.LockedQty
if avail < qty {
fail(w, http.StatusConflict, "批次库存不足: "+item.BatchNo+" 可用 "+itoa(avail))
return
}
_, err = ctx.EntClient.InventoryLock.Create().
aff, err := ctx.EntClient.Inventory.Update().
Where(inventory.ID(b.ID), invAvailGTE(qty)).
AddLockedQty(qty).
Save(ctx0())
if err != nil || aff == 0 {
fail(w, http.StatusConflict, "锁定失败(库存不足或并发冲突): "+item.BatchNo)
return
}
ctx.EntClient.InventoryLock.Create().
SetTargetType("BATCH").
SetTargetID(batch.BatchNo).
SetTargetID(item.BatchNo).
SetMaterialCode(item.MaterialCode).
SetLockedQty(qty).
SetOrderNo(req.OrderNo).
SetStatus("ACTIVE").
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "锁定批次失败: "+err.Error())
return
}
// 累加批次锁定数
ctx.EntClient.InventoryBatch.UpdateOneID(batch.ID).
SetLockedQty(batch.LockedQty + qty).ExecX(ctx0())
SaveX(ctx0())
locked += qty
}
@@ -205,8 +233,8 @@ func lockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
func unlockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
OrderNo string `json:"orderNo"`
LockIDs []int `json:"lockIds"`
OrderNo string `json:"orderNo"`
LockIDs []int `json:"lockIds"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
@@ -231,20 +259,20 @@ func unlockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
for _, l := range locks {
// 还原批次锁定数 / SN 状态
if l.TargetType == "BATCH" {
b, err := ctx.EntClient.InventoryBatch.Query().
Where(inventorybatch.BatchNoEQ(l.TargetID)).Only(ctx0())
if err == nil {
newLocked := b.LockedQty - l.LockedQty
if newLocked < 0 {
newLocked = 0
}
ctx.EntClient.InventoryBatch.UpdateOneID(b.ID).
SetLockedQty(newLocked).ExecX(ctx0())
}
ctx.EntClient.Inventory.Update().
Where(inventory.BatchNoEQ(l.TargetID)).
AddLockedQty(-l.LockedQty).
SaveX(ctx0())
// 防止并发下锁定数为负
ctx.EntClient.Inventory.Update().
Where(inventory.BatchNoEQ(l.TargetID), inventory.LockedQtyLT(0)).
SetLockedQty(0).
SaveX(ctx0())
} else if l.TargetType == "SN" {
ctx.EntClient.SerialNumber.Update().
Where(serialnumber.SnCodeEQ(l.TargetID)).
SetStatus("在库").ExecX(ctx0())
ctx.EntClient.Inventory.Update().
Where(inventory.SnCodeEQ(l.TargetID), inventory.StatusEQ("锁定")).
SetStatus("在库").SetLockedQty(0).
SaveX(ctx0())
}
ctx.EntClient.InventoryLock.UpdateOneID(l.ID).
SetStatus("RELEASED").ExecX(ctx0())
@@ -272,29 +300,18 @@ func checkStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
results := make([]map[string]any, 0, len(req.Items))
allOk := true
for _, item := range req.Items {
// 汇总批次可用量
batches, err := ctx.EntClient.InventoryBatch.Query().
Where(
inventorybatch.MaterialCodeEQ(item.MaterialCode),
inventorybatch.QualityStatusEQ("合格"),
).
All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
// 汇总可用量:结构件 quantity-locked_qty;精密件 在库数
rows, _ := ctx.EntClient.Inventory.Query().
Where(inventory.MaterialCodeEQ(item.MaterialCode)).All(ctx0())
availBatch := 0
for _, b := range batches {
availBatch += b.Quantity - b.LockedQty
availSerial := 0
for _, row := range rows {
if row.ManageMode == 1 {
availBatch += row.Quantity - row.LockedQty
} else if row.Status == "在库" {
availSerial++
}
}
// 汇总 SN 在库数
availSerial, _ := ctx.EntClient.SerialNumber.Query().
Where(
serialnumber.MaterialCodeEQ(item.MaterialCode),
serialnumber.StatusEQ("在库"),
).
Count(ctx0())
enough := availBatch+availSerial >= item.Qty
if !enough {
allOk = false