1. 合并inventory_batch与serial_number为统一inventory表,用manage_mode区分结构件批次/精密件SN 2. 移除冗余的material_type字段与相关逻辑 3. 重构库存查询、检验、入库出库等业务逻辑适配新表结构 4. 调整前端路由与菜单,拆分基础数据为区域维护和物料档案 5. 优化入库校验逻辑,避免空单问题 6. 调整Vite构建配置,关闭自动清空输出目录 7. 为各模块添加详细中文注释,统一业务术语 8. 生成并更新Ent自动代码文件
354 lines
10 KiB
Go
354 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"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 查料 / 客户端查料)
|
|
// 统一查询 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")
|
|
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)
|
|
|
|
q := ctx.EntClient.Inventory.Query()
|
|
// 仅展示在库/锁定(出库/使用/报废不计入库存)
|
|
q = q.Where(inventory.StatusIn("在库", "锁定"))
|
|
// 结构件隐藏数量为 0 的行
|
|
q = q.Where(inventory.Or(inventory.ManageModeEQ(2), inventory.QuantityGT(0)))
|
|
if materialCode != "" {
|
|
q = q.Where(inventory.MaterialCodeEQ(materialCode))
|
|
}
|
|
if 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 != "" {
|
|
q = q.Where(inventory.Or(
|
|
inventory.BatchNoContains(keyword),
|
|
inventory.MaterialCodeContains(keyword),
|
|
inventory.SnCodeContains(keyword),
|
|
inventory.MaterialNameContains(keyword),
|
|
))
|
|
}
|
|
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
|
|
}
|
|
ok(w, map[string]any{
|
|
"list": list,
|
|
"total": total,
|
|
"page": page,
|
|
"pageSize": pageSize,
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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 }] }
|
|
// 锁定为"增量"操作:结构件 locked_qty += qty;精密件状态置 锁定。绝不覆盖终值。
|
|
// 锁批次用原子增量 + 可用量条件校验;锁 SN 用状态条件原子更新,杜绝超锁与并发冲突。
|
|
func lockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
OrderNo string `json:"orderNo"`
|
|
Items []struct {
|
|
MaterialCode string `json:"materialCode"`
|
|
BatchNo string `json:"batchNo"`
|
|
Sn string `json:"sn"`
|
|
Qty int `json:"qty"`
|
|
} `json:"items"`
|
|
}
|
|
if err := parseJSON(r, &req); err != nil {
|
|
fail(w, http.StatusBadRequest, "参数错误: "+err.Error())
|
|
return
|
|
}
|
|
if req.OrderNo == "" || len(req.Items) == 0 {
|
|
fail(w, http.StatusBadRequest, "orderNo 和 items 必填")
|
|
return
|
|
}
|
|
|
|
locked := 0
|
|
for _, item := range req.Items {
|
|
if item.MaterialCode == "" {
|
|
continue
|
|
}
|
|
// 精密件:锁 SN(原子:仅 在库 才能锁)
|
|
if item.Sn != "" {
|
|
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
|
|
}
|
|
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
|
|
}
|
|
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 := b.Quantity - b.LockedQty
|
|
if avail < qty {
|
|
fail(w, http.StatusConflict, "批次库存不足: "+item.BatchNo+" 可用 "+itoa(avail))
|
|
return
|
|
}
|
|
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(item.BatchNo).
|
|
SetMaterialCode(item.MaterialCode).
|
|
SetLockedQty(qty).
|
|
SetOrderNo(req.OrderNo).
|
|
SetStatus("ACTIVE").
|
|
SaveX(ctx0())
|
|
locked += qty
|
|
}
|
|
|
|
ok(w, map[string]any{"locked": locked})
|
|
}
|
|
}
|
|
|
|
// unlockStockHandler 释放库存锁定(取消锁定/返工)
|
|
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"`
|
|
}
|
|
if err := parseJSON(r, &req); err != nil {
|
|
fail(w, http.StatusBadRequest, "参数错误")
|
|
return
|
|
}
|
|
|
|
q := ctx.EntClient.InventoryLock.Query().
|
|
Where(inventorylock.StatusEQ("ACTIVE"))
|
|
if req.OrderNo != "" {
|
|
q = q.Where(inventorylock.OrderNoEQ(req.OrderNo))
|
|
}
|
|
if len(req.LockIDs) > 0 {
|
|
q = q.Where(inventorylock.IDIn(req.LockIDs...))
|
|
}
|
|
locks, err := q.All(ctx0())
|
|
if err != nil {
|
|
fail(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
released := 0
|
|
for _, l := range locks {
|
|
// 还原批次锁定数 / SN 状态
|
|
if l.TargetType == "BATCH" {
|
|
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.Inventory.Update().
|
|
Where(inventory.SnCodeEQ(l.TargetID), inventory.StatusEQ("锁定")).
|
|
SetStatus("在库").SetLockedQty(0).
|
|
SaveX(ctx0())
|
|
}
|
|
ctx.EntClient.InventoryLock.UpdateOneID(l.ID).
|
|
SetStatus("RELEASED").ExecX(ctx0())
|
|
released++
|
|
}
|
|
|
|
ok(w, map[string]any{"released": released})
|
|
}
|
|
}
|
|
|
|
// checkStockHandler 校验库存是否满足需求(锁库前检查)
|
|
func checkStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Items []struct {
|
|
MaterialCode string `json:"materialCode"`
|
|
Qty int `json:"qty"`
|
|
} `json:"items"`
|
|
}
|
|
if err := parseJSON(r, &req); err != nil {
|
|
fail(w, http.StatusBadRequest, "参数错误")
|
|
return
|
|
}
|
|
|
|
results := make([]map[string]any, 0, len(req.Items))
|
|
allOk := true
|
|
for _, item := range req.Items {
|
|
// 汇总可用量:结构件 quantity-locked_qty;精密件 在库数量
|
|
rows, _ := ctx.EntClient.Inventory.Query().
|
|
Where(inventory.MaterialCodeEQ(item.MaterialCode)).All(ctx0())
|
|
availBatch := 0
|
|
availSerial := 0
|
|
for _, row := range rows {
|
|
if row.ManageMode == 1 {
|
|
availBatch += row.Quantity - row.LockedQty
|
|
} else if row.Status == "在库" {
|
|
availSerial++
|
|
}
|
|
}
|
|
enough := availBatch+availSerial >= item.Qty
|
|
if !enough {
|
|
allOk = false
|
|
}
|
|
results = append(results, map[string]any{
|
|
"materialCode": item.MaterialCode,
|
|
"requireQty": item.Qty,
|
|
"availQty": availBatch + availSerial,
|
|
"batchAvail": availBatch,
|
|
"serialAvail": availSerial,
|
|
"enough": enough,
|
|
})
|
|
}
|
|
|
|
ok(w, map[string]any{"allOk": allOk, "items": results})
|
|
}
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := n < 0
|
|
if neg {
|
|
n = -n
|
|
}
|
|
var b [20]byte
|
|
i := len(b)
|
|
for n > 0 {
|
|
i--
|
|
b[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
b[i] = '-'
|
|
}
|
|
return string(b[i:])
|
|
}
|