feat: 完成附件存储重构与磁盘监控功能,同步物料档案与入库单规则更新

1.  新增跨平台磁盘空间监控能力,每日7点自动检测附件目录剩余空间,触发阈值告警
2.  重构附件存储方案为年/月/日/文件类型分层结构,统一MES与WMS的附件管理逻辑
3.  对齐物料档案与入库单的质量状态校验规则,仅合格品计入库存与出库
4.  实现入库单作废功能与区域库位的多级父子结构管理
5.  删除物料简称字段,补充规格型号与单位必填项,统一系统数据口径
6.  新增附件中心与磁盘状态查询接口,完善权限控制与操作日志
This commit is contained in:
SunYF
2026-09-19 10:54:15 +08:00
parent 9002d9a642
commit 62e45b7379
95 changed files with 10467 additions and 2677 deletions
+20 -96
View File
@@ -2,115 +2,39 @@ package handler
import (
"net/http"
"strings"
"bj_power_wms/ent"
"bj_power_wms/ent/inventory"
"bj_power_wms/ent/zone"
"bj_power_wms/ent/inboundorder"
"bj_power_wms/internal/svc"
)
// zoneLoc 库位推荐候选(历史常用位 / 空闲位共用):四级货位
type zoneLoc struct {
ZoneCode string `json:"zoneCode"`
ZoneName string `json:"zoneName"`
ShelfNo string `json:"shelfNo"`
LayerNo string `json:"layerNo"`
PositionNo string `json:"positionNo"`
}
// locKey 四级货位归一键(NULL/空串归一为""),与 DB 组合唯一索引 ux_zones_location 语义一致。
func locKey(zoneCode, shelfNo, layerNo, positionNo string) string {
return zoneCode + "\x00" + shelfNo + "\x00" + layerNo + "\x00" + positionNo
}
// zoneRecommendHandler 库位推荐(客户诉求 问题记录 附二#4:拒绝全自动识别,
// 采用「历史常用位 + 同区空位优先」推荐,由库房一键确认或手改)。
//
// 入参:materialCode(必填,图号)。
// 返回(history/empty 均为四级货位对象,前端点击可一键回填区域+货架/层/位):
// - history: 该物料历史入库使用过的货位(按最近使用排序,最多 5 个)
// - empty: 空闲货位(zones 中启用且当前无在库/锁定库存引用,最多 10 个;同区域优先)
func zoneRecommendHandler(ctx *svc.ServiceContext) http.HandlerFunc {
// lastInboundLocationHandler 入库库位联动:返回该物料最近一次入库的库位(区域/货架/层/位置号)
// 前端在「选物料」后自动回填到入库表单(可手动修改),替代原「推荐库位」按钮式交互。
func lastInboundLocationHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
materialCode := r.URL.Query().Get("materialCode")
materialCode := strings.TrimSpace(r.URL.Query().Get("materialCode"))
if materialCode == "" {
fail(w, http.StatusBadRequest, "materialCode 必填")
return
}
// 1. 历史常用货位:该物料库存行按最近更新倒序,去重取前 5 个四级货位。
history := []zoneLoc{}
histSet := map[string]bool{}
preferredZone := ""
if invs, err := ctx.EntClient.Inventory.Query().
Where(inventory.MaterialCodeEQ(materialCode)).
Order(ent.Desc("updated_at"), ent.Desc("id")).
Limit(200).All(ctx0()); err == nil {
for _, inv := range invs {
if inv.ZoneCode == "" {
continue
}
key := locKey(inv.ZoneCode, inv.ShelfNo, inv.LayerNo, inv.PositionNo)
if histSet[key] {
continue
}
histSet[key] = true
if preferredZone == "" {
preferredZone = inv.ZoneCode
}
history = append(history, zoneLoc{
ZoneCode: inv.ZoneCode, ShelfNo: inv.ShelfNo, LayerNo: inv.LayerNo, PositionNo: inv.PositionNo,
})
if len(history) >= 5 {
break
}
ib, err := ctx.EntClient.InboundOrder.Query().
Where(inboundorder.MaterialCodeEQ(materialCode)).
Order(ent.Desc("created_at"), ent.Desc("id")).
First(ctx0())
if err != nil {
if ent.IsNotFound(err) {
ok(w, map[string]any{"zoneCode": "", "shelfNo": "", "layerNo": "", "positionNo": ""})
return
}
fail(w, http.StatusInternalServerError, err.Error())
return
}
// 2. 被占用货位集合:在库/锁定库存行引用的四级货位(U18:按货位而非区域判占用)。
occupied := map[string]bool{}
if rows, err := ctx.EntClient.Inventory.Query().
Where(inventory.StatusIn("在库", "锁定")).
Select(inventory.FieldZoneCode, inventory.FieldShelfNo, inventory.FieldLayerNo, inventory.FieldPositionNo).
All(ctx0()); err == nil {
for _, row := range rows {
if row.ZoneCode != "" {
occupied[locKey(row.ZoneCode, row.ShelfNo, row.LayerNo, row.PositionNo)] = true
}
}
}
// 3. 空闲货位:启用且四级货位未被占用的 zones 行;同区域(preferredZone)的排前面。
sameArea := []zoneLoc{}
otherArea := []zoneLoc{}
if zones, err := ctx.EntClient.Zone.Query().
Where(zone.StatusEQ("启用")).
Order(ent.Asc("id")).
All(ctx0()); err == nil {
for _, z := range zones {
if occupied[locKey(z.ZoneCode, z.ShelfNo, z.LayerNo, z.PositionNo)] {
continue
}
zl := zoneLoc{
ZoneCode: z.ZoneCode, ZoneName: z.ZoneName,
ShelfNo: z.ShelfNo, LayerNo: z.LayerNo, PositionNo: z.PositionNo,
}
// 同区判定:区域(zone_code)相同(U18zone_code 即区域级,不再靠前缀猜测)。
if preferredZone != "" && z.ZoneCode == preferredZone {
sameArea = append(sameArea, zl)
} else {
otherArea = append(otherArea, zl)
}
}
}
empty := append(sameArea, otherArea...)
if len(empty) > 10 {
empty = empty[:10]
}
ok(w, map[string]any{
"history": history,
"empty": empty,
"zoneCode": ib.ZoneCode,
"shelfNo": ib.ShelfNo,
"layerNo": ib.LayerNo,
"positionNo": ib.PositionNo,
})
}
}