本提交完成了WMS系统的多维度优化升级: 1. 新增RBAC权限系统,支持角色/菜单/按钮三级权限管控 2. 重构库存盘点、物料管理、区域维护等模块的查询筛选与展示逻辑 3. 优化入库/出库/质检等业务流程,完善数据冗余与业务闭环 4. 移除旧装箱表,将装箱逻辑合并到出库主表 5. 新增前端权限判断工具、导出工具与端到端测试用例 6. 补充完善各类注释与数据库字段说明
624 lines
19 KiB
Go
624 lines
19 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"entgo.io/ent/dialect/sql"
|
|
"bj_power_wms/ent"
|
|
"bj_power_wms/ent/inventory"
|
|
"bj_power_wms/ent/inventorylock"
|
|
"bj_power_wms/ent/material"
|
|
"bj_power_wms/ent/predicate"
|
|
"bj_power_wms/internal/svc"
|
|
)
|
|
|
|
// stockKey 库存聚合行的唯一标识(组合键)+ 明细下钻的过滤条件
|
|
type stockKey struct {
|
|
MaterialCode string
|
|
ZoneCode string
|
|
QualityStatus string
|
|
ManageMode int
|
|
}
|
|
|
|
// stockAggRow 聚合主列表的行结构(按 物料+区域+质量+管理粒度 聚合,只返回数量,不铺开明细)
|
|
type stockAggRow struct {
|
|
ID int64 `json:"id"` // 组合键稳定序号的伪 id,供前端行 key 使用
|
|
MaterialCode string `json:"materialCode"` // 物料编码
|
|
MaterialName string `json:"materialName"` // 物料名称
|
|
Spec string `json:"spec"` // 规格型号
|
|
ManageMode int `json:"manageMode"` // 1结构件/2精密件
|
|
ZoneCode string `json:"zoneCode"` // 区域
|
|
QualityStatus string `json:"qualityStatus"` // 质量状态
|
|
TotalQty int `json:"totalQty"` // 总数量(结构件=数量求和;精密件=SN 行数)
|
|
LockedQty int `json:"lockedQty"` // 锁定量
|
|
AvailQty int `json:"availQty"` // 可用量 = total - locked
|
|
BatchCount int `json:"batchCount"` // 批次数量(结构件维度的行数)
|
|
SnCount int `json:"snCount"` // SN 数量(精密件维度的行数)
|
|
LastInboundNo string `json:"lastInboundNo"` // 最近入库单号
|
|
CreatedAt int64 `json:"createdAt"` // 最近一次入库时间(聚合组内最大 created_at)
|
|
}
|
|
|
|
// queryStockHandler 库存查询(主列表 = 按物料聚合数量)
|
|
// 统一查询 inventory 一张表,按 物料编码+区域+质量状态+管理粒度 分组聚合,
|
|
// 只返回每个物料的数量/锁定/可用与 批次数/SN数,不把每个批次/SN 平铺出来(避免 SN 泄漏与数据量膨胀)。
|
|
// 明细通过 /api/stock/details 点击下钻(分页懒加载)。
|
|
func queryStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
materialCode := r.URL.Query().Get("materialCode")
|
|
materialName := r.URL.Query().Get("materialName") // 物料名称模糊
|
|
zoneCode := r.URL.Query().Get("zoneCode")
|
|
qualityStatus := r.URL.Query().Get("qualityStatus")
|
|
manageMode := r.URL.Query().Get("manageMode") // 1/2/空=全部
|
|
inboundNo := r.URL.Query().Get("inboundNo")
|
|
startDate := r.URL.Query().Get("startDate") // 默认近3个月(由接口保证)
|
|
endDate := r.URL.Query().Get("endDate")
|
|
page := atoi(r.URL.Query().Get("page"), 1)
|
|
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
|
|
|
|
rows, total, err := aggregateStock(ctx, aggregateStockArgs{
|
|
MaterialCode: materialCode,
|
|
MaterialName: materialName,
|
|
ZoneCode: zoneCode,
|
|
QualityStatus: qualityStatus,
|
|
ManageMode: manageMode,
|
|
InboundNo: inboundNo,
|
|
StartDate: startDate,
|
|
EndDate: endDate,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
})
|
|
if err != nil {
|
|
fail(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
ok(w, map[string]any{
|
|
"list": rows,
|
|
"total": total,
|
|
"page": page,
|
|
"pageSize": pageSize,
|
|
})
|
|
}
|
|
}
|
|
|
|
// aggregateStockArgs 聚合主列表的筛选条件
|
|
type aggregateStockArgs struct {
|
|
MaterialCode string
|
|
MaterialName string
|
|
ZoneCode string
|
|
QualityStatus string
|
|
ManageMode string
|
|
InboundNo string
|
|
StartDate string
|
|
EndDate string
|
|
Page int
|
|
PageSize int
|
|
}
|
|
|
|
// aggregateStock 按物料聚合库存主数据。
|
|
// 用相机内存聚合(inventory 表每物料的行数有限:结构件=批次数、精密件=SN 数),
|
|
// 先拉取符合状态条件的行,在 Go 侧分组聚合,再做内存分页。
|
|
// 这样避免手写 GROUP BY 子查询的复杂度,且数据量受"在库/锁定"上限约束,安全可控。
|
|
func aggregateStock(ctx *svc.ServiceContext, a aggregateStockArgs) ([]*stockAggRow, int, error) {
|
|
q := ctx.EntClient.Inventory.Query()
|
|
// 仅展示在库/锁定(出库/使用/报废不计入库存)
|
|
q = q.Where(inventory.StatusIn("在库", "锁定"))
|
|
// 结构件隐藏数量为 0 的行
|
|
q = q.Where(inventory.Or(inventory.ManageModeEQ(2), inventory.QuantityGT(0)))
|
|
|
|
if a.MaterialCode != "" {
|
|
q = q.Where(inventory.MaterialCodeContains(a.MaterialCode))
|
|
}
|
|
if a.MaterialName != "" {
|
|
q = q.Where(inventory.MaterialNameContains(a.MaterialName))
|
|
}
|
|
if a.ZoneCode != "" {
|
|
q = q.Where(inventory.ZoneCodeEQ(a.ZoneCode))
|
|
}
|
|
if a.QualityStatus != "" {
|
|
q = q.Where(inventory.QualityStatusEQ(a.QualityStatus))
|
|
}
|
|
if a.InboundNo != "" {
|
|
q = q.Where(inventory.InboundNoContains(a.InboundNo))
|
|
}
|
|
if a.ManageMode == "1" {
|
|
q = q.Where(inventory.ManageModeEQ(1))
|
|
} else if a.ManageMode == "2" {
|
|
q = q.Where(inventory.ManageModeEQ(2))
|
|
}
|
|
// 时间筛选:默认近3个月
|
|
startUnix := int64(0)
|
|
if a.StartDate != "" {
|
|
if t, err := time.ParseInLocation("2006-01-02", a.StartDate, time.Local); err == nil {
|
|
startUnix = t.Unix()
|
|
}
|
|
}
|
|
endUnix := int64(0)
|
|
if a.EndDate != "" {
|
|
if t, err := time.ParseInLocation("2006-01-02", a.EndDate, time.Local); err == nil {
|
|
endUnix = t.Add(24 * time.Hour).Unix() // 含当天全天
|
|
}
|
|
}
|
|
if a.StartDate == "" && a.EndDate == "" {
|
|
startUnix = time.Now().AddDate(0, -3, 0).Unix() // 默认近3个月
|
|
}
|
|
if startUnix > 0 {
|
|
q = q.Where(inventory.CreatedAtGTE(startUnix))
|
|
}
|
|
if endUnix > 0 {
|
|
q = q.Where(inventory.CreatedAtLTE(endUnix))
|
|
}
|
|
|
|
all, err := q.Order(ent.Desc("created_at")).All(ctx0())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// 收集物料信息(名称/规格从 materials 表补齐,字段更权威)
|
|
matName := map[string]string{}
|
|
matSpec := map[string]string{}
|
|
matCodes := []string{}
|
|
seen := map[string]bool{}
|
|
for _, inv := range all {
|
|
if !seen[inv.MaterialCode] {
|
|
seen[inv.MaterialCode] = true
|
|
matCodes = append(matCodes, inv.MaterialCode)
|
|
}
|
|
}
|
|
if len(matCodes) > 0 {
|
|
mats, _ := ctx.EntClient.Material.Query().
|
|
Where(material.CodeIn(matCodes...)).All(ctx0())
|
|
for _, m := range mats {
|
|
matName[m.Code] = m.Name
|
|
matSpec[m.Code] = m.Spec
|
|
}
|
|
}
|
|
|
|
// 分组聚合
|
|
type agg struct {
|
|
totalQty int
|
|
lockedQty int
|
|
batchCnt int
|
|
snCnt int
|
|
lastInbNo string
|
|
lastTime int64
|
|
}
|
|
group := map[stockKey]*agg{}
|
|
order := []stockKey{}
|
|
for _, inv := range all {
|
|
zc := inv.ZoneCode
|
|
if zc == "" {
|
|
zc = "未分区"
|
|
}
|
|
key := stockKey{inv.MaterialCode, zc, inv.QualityStatus, inv.ManageMode}
|
|
g, ok := group[key]
|
|
if !ok {
|
|
g = &agg{}
|
|
group[key] = g
|
|
order = append(order, key)
|
|
}
|
|
if inv.ManageMode == 1 {
|
|
g.totalQty += inv.Quantity
|
|
g.lockedQty += inv.LockedQty
|
|
g.batchCnt++
|
|
} else {
|
|
g.totalQty++
|
|
if inv.Status == "锁定" {
|
|
g.lockedQty++
|
|
}
|
|
g.snCnt++
|
|
}
|
|
if inv.CreatedAt > g.lastTime {
|
|
g.lastTime = inv.CreatedAt
|
|
g.lastInbNo = inv.InboundNo
|
|
}
|
|
}
|
|
|
|
total := len(order)
|
|
// 分页
|
|
start := (a.Page - 1) * a.PageSize
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := start + a.PageSize
|
|
if end > total {
|
|
end = total
|
|
}
|
|
rows := make([]*stockAggRow, 0, end-start)
|
|
for i := start; i < end; i++ {
|
|
key := order[i]
|
|
g := group[key]
|
|
name := matName[key.MaterialCode]
|
|
if name == "" {
|
|
name = key.MaterialCode
|
|
}
|
|
rows = append(rows, &stockAggRow{
|
|
ID: int64(i + 1),
|
|
MaterialCode: key.MaterialCode,
|
|
MaterialName: name,
|
|
Spec: matSpec[key.MaterialCode],
|
|
ManageMode: key.ManageMode,
|
|
ZoneCode: key.ZoneCode,
|
|
QualityStatus: key.QualityStatus,
|
|
TotalQty: g.totalQty,
|
|
LockedQty: g.lockedQty,
|
|
AvailQty: g.totalQty - g.lockedQty,
|
|
BatchCount: g.batchCnt,
|
|
SnCount: g.snCnt,
|
|
LastInboundNo: g.lastInbNo,
|
|
CreatedAt: g.lastTime,
|
|
})
|
|
}
|
|
return rows, total, nil
|
|
}
|
|
|
|
// stockDetailsHandler 库存明细(点击聚合行下钻,分页懒加载)
|
|
// GET /api/stock/details?materialCode=&zoneCode=&qualityStatus=&manageMode=&page=&pageSize=
|
|
// 返回该物料/区域/质量/类型下的原始批次/SN 明细行(扁平展示,含 SN/批次号)。
|
|
func stockDetailsHandler(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")
|
|
if zoneCode == "未分区" {
|
|
zoneCode = ""
|
|
}
|
|
qualityStatus := r.URL.Query().Get("qualityStatus")
|
|
manageMode := atoi(r.URL.Query().Get("manageMode"), 0)
|
|
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("在库", "锁定"))
|
|
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 qualityStatus != "" {
|
|
q = q.Where(inventory.QualityStatusEQ(qualityStatus))
|
|
}
|
|
if manageMode != 0 {
|
|
q = q.Where(inventory.ManageModeEQ(manageMode))
|
|
}
|
|
|
|
total, err := q.Count(ctx0())
|
|
if err != nil {
|
|
fail(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
list, err := q.Order(ent.Desc("created_at")).
|
|
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,
|
|
})
|
|
}
|
|
}
|
|
|
|
// exportStockHandler 库存汇总全量导出(xlsx)
|
|
// 与 /stock/query 同一套聚合逻辑,但导出当前筛选下的全部行(不分页)。
|
|
// 返回 .xlsx 二进制,文件名 = 库存汇总_时间.xlsx。
|
|
func exportStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
rows, _, err := aggregateStock(ctx, aggregateStockArgs{
|
|
MaterialCode: r.URL.Query().Get("materialCode"),
|
|
MaterialName: r.URL.Query().Get("materialName"),
|
|
ZoneCode: r.URL.Query().Get("zoneCode"),
|
|
QualityStatus: r.URL.Query().Get("qualityStatus"),
|
|
ManageMode: r.URL.Query().Get("manageMode"),
|
|
InboundNo: r.URL.Query().Get("inboundNo"),
|
|
StartDate: r.URL.Query().Get("startDate"),
|
|
EndDate: r.URL.Query().Get("endDate"),
|
|
Page: 1,
|
|
PageSize: 1000000, // 全量
|
|
})
|
|
if err != nil {
|
|
fail(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
headers := []string{"物料编码", "物料名称", "规格", "类型", "区域", "质量状态", "总数量", "锁定量", "可用量", "批次数", "SN数", "最近入库单", "最近入库时间"}
|
|
matrix := make([][]any, 0, len(rows))
|
|
for _, rw := range rows {
|
|
matrix = append(matrix, []any{
|
|
rw.MaterialCode, rw.MaterialName, rw.Spec,
|
|
manageModeLabel(rw.ManageMode), rw.ZoneCode, rw.QualityStatus,
|
|
rw.TotalQty, rw.LockedQty, rw.AvailQty, rw.BatchCount, rw.SnCount,
|
|
rw.LastInboundNo, unixFmt(rw.CreatedAt),
|
|
})
|
|
}
|
|
sendExcel(w, xlsxFilename("库存汇总"), headers, matrix)
|
|
}
|
|
}
|
|
// 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:])
|
|
}
|