feat: 完成产线与仓储系统多模块迭代升级
本次迭代覆盖MES与WMS核心业务: 1. 新增接驳台托盘传感器读取与AGV对接能力 2. 完善工单排产、备料流程与权限体系拆分 3. 优化看板接口与前端路由、样式 4. 新增操作日志、库存盘点与角色保护逻辑 5. 修复代理地址、BOM保存等已知问题
This commit is contained in:
@@ -254,6 +254,207 @@ func aggregateStock(ctx *svc.ServiceContext, a aggregateStockArgs) ([]*stockAggR
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
// materialAggRow 物料汇总行(按 物料编码 聚合,忽略区域与质量状态)
|
||||
// 回答"这个物料总共有多少":总数/锁定/可用 + 质量分布(合格/未检/不合格) + 分布区域列表。
|
||||
type materialAggRow struct {
|
||||
MaterialCode string `json:"materialCode"`
|
||||
MaterialName string `json:"materialName"`
|
||||
Spec string `json:"spec"`
|
||||
ManageMode int `json:"manageMode"` // 1结构件/2精密件
|
||||
TotalQty int `json:"totalQty"` // 总数量(结构件=数量求和;精密件=SN 行数)
|
||||
LockedQty int `json:"lockedQty"` // 锁定量
|
||||
AvailQty int `json:"availQty"` // 可用量 = total - locked
|
||||
QtyQualified int `json:"qtyQualified"` // 合格数量
|
||||
QtyPending int `json:"qtyPending"` // 未检数量
|
||||
QtyRejected int `json:"qtyRejected"` // 不合格数量
|
||||
Zones []string `json:"zones"` // 分布区域列表
|
||||
ZoneCount int `json:"zoneCount"` // 分布区域数
|
||||
BatchCount int `json:"batchCount"` // 批次数(结构件维度的行数)
|
||||
SnCount int `json:"snCount"` // SN 数(精密件维度的行数)
|
||||
LastInboundNo string `json:"lastInboundNo"` // 最近入库单号
|
||||
CreatedAt int64 `json:"createdAt"` // 最近一次入库时间
|
||||
}
|
||||
|
||||
// materialSummaryArgs 物料汇总的筛选条件
|
||||
type materialSummaryArgs struct {
|
||||
MaterialCode string
|
||||
MaterialName string
|
||||
ManageMode string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// materialSummaryHandler 物料汇总(按物料聚合所有区域/质量状态)
|
||||
// GET /api/stock/material-summary?materialCode=&materialName=&manageMode=&page=&pageSize=
|
||||
// 返回 rows: 一物料一行,含 总数/可用/锁定/合格/未检/不合格/分布区域。明细经 /stock/details 点击下钻。
|
||||
// 与主列表不同:汇总忽略区域/质量/入库时间筛选(回答总量问题,只认物料与类型)。
|
||||
func materialSummaryHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
rows, total, err := aggregateMaterialSummary(ctx, materialSummaryArgs{
|
||||
MaterialCode: r.URL.Query().Get("materialCode"),
|
||||
MaterialName: r.URL.Query().Get("materialName"),
|
||||
ManageMode: r.URL.Query().Get("manageMode"),
|
||||
Page: atoi(r.URL.Query().Get("page"), 1),
|
||||
PageSize: atoi(r.URL.Query().Get("pageSize"), 20),
|
||||
})
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
ok(w, map[string]any{"rows": rows, "total": total})
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateMaterialSummary 按物料编码聚合全部在库库存(忽略区域/质量/时间维度),Go 侧内存聚合 + 分页。
|
||||
func aggregateMaterialSummary(ctx *svc.ServiceContext, a materialSummaryArgs) ([]*materialAggRow, int, error) {
|
||||
q := ctx.EntClient.Inventory.Query()
|
||||
q = q.Where(inventory.StatusIn("在库", "锁定"))
|
||||
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.ManageMode == "1" {
|
||||
q = q.Where(inventory.ManageModeEQ(1))
|
||||
} else if a.ManageMode == "2" {
|
||||
q = q.Where(inventory.ManageModeEQ(2))
|
||||
}
|
||||
|
||||
all, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 物料名称/规格补全
|
||||
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
|
||||
qQualified int
|
||||
qPending int
|
||||
qRejected int
|
||||
mMode int // 管理粒度(同物料唯一,取首见行的值)
|
||||
batchCnt int
|
||||
snCnt int
|
||||
zones []string
|
||||
zoneSeen map[string]bool
|
||||
lastInbNo string
|
||||
lastTime int64
|
||||
}
|
||||
group := map[string]*agg{} // key: materialCode
|
||||
order := []string{}
|
||||
for _, inv := range all {
|
||||
key := inv.MaterialCode
|
||||
g, ok := group[key]
|
||||
if !ok {
|
||||
g = &agg{mMode: inv.ManageMode, zoneSeen: map[string]bool{}}
|
||||
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++
|
||||
}
|
||||
// 质量分布(未检=空或非合格/不合格)
|
||||
switch inv.QualityStatus {
|
||||
case "合格":
|
||||
g.qQualified += qtyOf(inv)
|
||||
case "不合格":
|
||||
g.qRejected += qtyOf(inv)
|
||||
default:
|
||||
g.qPending += qtyOf(inv)
|
||||
}
|
||||
// 分布区域
|
||||
zc := inv.ZoneCode
|
||||
if zc == "" {
|
||||
zc = "未分区"
|
||||
}
|
||||
if !g.zoneSeen[zc] {
|
||||
g.zoneSeen[zc] = true
|
||||
g.zones = append(g.zones, zc)
|
||||
}
|
||||
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([]*materialAggRow, 0, end-start)
|
||||
for i := start; i < end; i++ {
|
||||
code := order[i]
|
||||
g := group[code]
|
||||
name := matName[code]
|
||||
if name == "" {
|
||||
name = code
|
||||
}
|
||||
rows = append(rows, &materialAggRow{
|
||||
MaterialCode: code,
|
||||
MaterialName: name,
|
||||
Spec: matSpec[code],
|
||||
ManageMode: g.mMode,
|
||||
TotalQty: g.totalQty,
|
||||
LockedQty: g.lockedQty,
|
||||
AvailQty: g.totalQty - g.lockedQty,
|
||||
QtyQualified: g.qQualified,
|
||||
QtyPending: g.qPending,
|
||||
QtyRejected: g.qRejected,
|
||||
Zones: g.zones,
|
||||
ZoneCount: len(g.zones),
|
||||
BatchCount: g.batchCnt,
|
||||
SnCount: g.snCnt,
|
||||
LastInboundNo: g.lastInbNo,
|
||||
CreatedAt: g.lastTime,
|
||||
})
|
||||
}
|
||||
return rows, total, nil
|
||||
}
|
||||
|
||||
// qtyOf 库存行计入数量的口径:结构件按行数量、精密件按 1
|
||||
func qtyOf(inv *ent.Inventory) int {
|
||||
if inv.ManageMode == 1 {
|
||||
return inv.Quantity
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// stockDetailsHandler 库存明细(点击聚合行下钻,分页懒加载)
|
||||
// GET /api/stock/details?materialCode=&zoneCode=&qualityStatus=&manageMode=&page=&pageSize=
|
||||
// 返回该物料/区域/质量/类型下的原始批次/SN 明细行(扁平展示,含 SN/批次号)。
|
||||
@@ -307,9 +508,36 @@ func stockDetailsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
// exportStockHandler 库存汇总全量导出(xlsx)
|
||||
// 与 /stock/query 同一套聚合逻辑,但导出当前筛选下的全部行(不分页)。
|
||||
// 支持 ?view=material 导出「物料汇总」(/stock/material-summary 的全量版本)。
|
||||
// 返回 .xlsx 二进制,文件名 = 库存汇总_时间.xlsx。
|
||||
func exportStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("view") == "material" {
|
||||
rows, _, err := aggregateMaterialSummary(ctx, materialSummaryArgs{
|
||||
MaterialCode: r.URL.Query().Get("materialCode"),
|
||||
MaterialName: r.URL.Query().Get("materialName"),
|
||||
ManageMode: r.URL.Query().Get("manageMode"),
|
||||
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.TotalQty, rw.LockedQty, rw.AvailQty,
|
||||
rw.QtyQualified, rw.QtyPending, rw.QtyRejected,
|
||||
strings.Join(rw.Zones, "、"), rw.BatchCount, rw.SnCount,
|
||||
rw.LastInboundNo, unixFmt(rw.CreatedAt),
|
||||
})
|
||||
}
|
||||
sendExcel(w, xlsxFilename("物料汇总"), headers, matrix)
|
||||
return
|
||||
}
|
||||
rows, _, err := aggregateStock(ctx, aggregateStockArgs{
|
||||
MaterialCode: r.URL.Query().Get("materialCode"),
|
||||
MaterialName: r.URL.Query().Get("materialName"),
|
||||
|
||||
Reference in New Issue
Block a user