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
+65 -46
View File
@@ -27,13 +27,33 @@ func itemTypeLabel(t int) string {
}
}
// validateMaterialRequired 物料档案必填校验(2026-09-19 用户裁决:
// 新增/编辑表单除「说明」外全部必填)。返回空串表示通过,否则返回中文错误文案。
//
// 说明:图号 code 只在新增时必填(编辑不可改),由调用方单独校验;
// 「其他」类物料不做 SN,管理粒度由后端强制收敛为 1(结构件),故不参与必填。
func validateMaterialRequired(name, spec, unit string, itemType, manageMode int) string {
switch {
case strings.TrimSpace(name) == "":
return "名称为必填项"
case strings.TrimSpace(spec) == "":
return "规格型号为必填项"
case strings.TrimSpace(unit) == "":
return "单位为必填项"
case itemType < 1 || itemType > invItemOther:
return "品类为必填项(原材料 / 半成品 / 成品 / 其他)"
case itemType != invItemOther && manageMode != 1 && manageMode != 2:
return "类型为必填项(结构件 / 电气件)"
}
return ""
}
// createMaterialHandler 创建物料档案
func createMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Code string `json:"code"`
Name string `json:"name"`
ShortName string `json:"shortName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
Description string `json:"description"`
@@ -46,13 +66,13 @@ func createMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.Code == "" || req.Name == "" {
fail(w, http.StatusBadRequest, "图号和名称必填")
// 必填口径(2026-09-19 用户裁决):除「说明」外全部必填。
if strings.TrimSpace(req.Code) == "" {
fail(w, http.StatusBadRequest, "图号为必填项")
return
}
// 其他类(辅料/工装/试验设备):规格强制必填(客户诉求 问题记录 L50)
if req.ItemType == invItemOther && strings.TrimSpace(req.Spec) == "" {
fail(w, http.StatusBadRequest, "其他类物料必须填写规格型号")
if msg := validateMaterialRequired(req.Name, req.Spec, req.Unit, req.ItemType, req.ManageMode); msg != "" {
fail(w, http.StatusBadRequest, msg)
return
}
@@ -69,14 +89,9 @@ func createMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return
}
// 品类/管理粒度已在上方校验,这里只做「其他类不做 SN」的业务收敛
manageMode := req.ManageMode
if manageMode == 0 {
manageMode = 1
}
itemType := req.ItemType
if itemType < 1 || itemType > invItemOther {
itemType = 1
}
// 其他类(辅料/工装/试验设备)只按数量管理,不做 SN
if itemType == invItemOther {
manageMode = 1
@@ -85,7 +100,6 @@ func createMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
m, err := ctx.EntClient.Material.Create().
SetCode(req.Code).
SetName(req.Name).
SetNillableShortName(strPtr(req.ShortName)).
SetNillableSpec(strPtr(req.Spec)).
SetNillableUnit(strPtr(req.Unit)).
SetNillableDescription(strPtr(req.Description)).
@@ -114,7 +128,6 @@ func updateMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
var req struct {
ID int `json:"id"`
Name string `json:"name"`
ShortName string `json:"shortName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
Description string `json:"description"`
@@ -126,31 +139,32 @@ func updateMaterialHandler(ctx *svc.ServiceContext) http.HandlerFunc {
fail(w, http.StatusBadRequest, "参数错误")
return
}
// 必填口径(2026-09-19 用户裁决):除「说明」外全部必填
if msg := validateMaterialRequired(req.Name, req.Spec, req.Unit, req.ItemType, req.ManageMode); msg != "" {
fail(w, http.StatusBadRequest, msg)
return
}
cur, gerr := ctx.EntClient.Material.Get(ctx0(), req.ID)
if gerr != nil {
fail(w, http.StatusNotFound, "物料不存在")
return
}
upd := ctx.EntClient.Material.UpdateOneID(req.ID)
if req.Name != "" {
upd.SetName(req.Name)
manageMode := req.ManageMode
if req.ItemType == invItemOther {
manageMode = 1
}
upd.SetShortName(req.ShortName)
upd.SetSpec(req.Spec)
upd.SetUnit(req.Unit)
upd.SetDescription(req.Description)
if req.ItemType > 0 {
upd.SetItemType(req.ItemType)
}
upd.SetSafetyStock(req.SafetyStock)
if req.ManageMode > 0 {
upd.SetManageMode(req.ManageMode).
SetIsBatchManaged(req.ManageMode == 1).
SetIsSerialManaged(req.ManageMode == 2)
}
m, err := upd.Save(ctx0())
m, err := ctx.EntClient.Material.UpdateOneID(req.ID).
SetName(req.Name).
SetSpec(req.Spec).
SetUnit(req.Unit).
SetDescription(req.Description).
SetItemType(req.ItemType).
SetSafetyStock(req.SafetyStock).
SetManageMode(manageMode).
SetIsBatchManaged(manageMode == 1).
SetIsSerialManaged(manageMode == 2).
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, "更新物料失败: "+err.Error())
return
@@ -397,7 +411,6 @@ func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
ID int
Code string
Name string
ShortName string
Spec string
Unit string
ItemType int
@@ -408,7 +421,7 @@ func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
rows := make([]exportRow, 0, len(list))
for _, m := range list {
rows = append(rows, exportRow{
ID: m.ID, Code: m.Code, Name: m.Name, ShortName: m.ShortName, Spec: m.Spec,
ID: m.ID, Code: m.Code, Name: m.Name, Spec: m.Spec,
Unit: m.Unit, ItemType: m.ItemType, ManageMode: m.ManageMode,
Description: m.Description, CreatedAt: m.CreatedAt,
})
@@ -416,11 +429,12 @@ func exportMaterialsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
if r.URL.Query().Get("format") == "xlsx" {
// 图号=物料编码(问题记录 L18 合并裁决):导出表头统一叫「图号」
headers := []string{"ID", "图号", "名称", "简称", "规格", "单位", "品类", "类型", "说明", "创建时间"}
// 「简称」已按 2026-09-19 用户裁决整体删除,导出列同步移除
headers := []string{"ID", "图号", "名称", "规格", "单位", "品类", "类型", "说明", "创建时间"}
matrix := make([][]any, 0, len(rows))
for _, rr := range rows {
matrix = append(matrix, []any{
rr.ID, rr.Code, rr.Name, rr.ShortName, rr.Spec, rr.Unit,
rr.ID, rr.Code, rr.Name, rr.Spec, rr.Unit,
itemTypeLabel(rr.ItemType), manageModeLabel(rr.ManageMode),
rr.Description, unixFmt(rr.CreatedAt),
})
@@ -451,10 +465,10 @@ func parseManageMode(s string) int {
}
// excelMaterialImportHandler 物料档案 Excel 批量导入
// 模板(首行忽略表头):图号 | 名称 | 简称 | 规格 | 单位 | 类型(结构件/电气件) | 说明
// (图号=物料编码,问题记录 L18 合并裁决,不再单设图号列)
// 模板(首行忽略表头):图号 | 名称 | 规格 | 单位 | 类型(结构件/电气件) | 说明
// (图号=物料编码,问题记录 L18 合并裁决,不再单设图号列;「简称」列已按 2026-09-19 裁决移除
// 强约束:全成功或全失败(一次性全导入 or 全失败,禁止部分成功)。
// 阶段1 逐行解析+校验(编码必填且唯一 / 名称必填 / 类型合法) → 任一错则整体拒绝并列出错误行;
// 阶段1 逐行解析+校验(图号必填且唯一 / 名称、规格、单位必填 / 类型合法) → 任一错则整体拒绝并列出错误行;
// 阶段2 全部通过 → 开启单事务整体提交,任一步失败整体回滚。
func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -490,7 +504,6 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
row int
code string
name string
shortName string
spec string
unit string
manageMode int
@@ -513,11 +526,10 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
pr := planRow{row: i + 1}
pr.code = cell(0)
pr.name = cell(1)
pr.shortName = cell(2)
pr.spec = cell(3)
pr.unit = cell(4)
pr.manageMode = parseManageMode(cell(5))
pr.description = cell(6)
pr.spec = cell(2)
pr.unit = cell(3)
pr.manageMode = parseManageMode(cell(4))
pr.description = cell(5)
if pr.code == "" {
errs = append(errs, importErr{pr.row, pr.code, "图号为空"})
@@ -527,6 +539,14 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
errs = append(errs, importErr{pr.row, pr.code, "名称为空"})
continue
}
if pr.spec == "" {
errs = append(errs, importErr{pr.row, pr.code, "规格型号为空"})
continue
}
if pr.unit == "" {
errs = append(errs, importErr{pr.row, pr.code, "单位为空"})
continue
}
if pr.manageMode == 0 {
errs = append(errs, importErr{pr.row, pr.code, "类型无法识别,请填写「结构件」或「电气件」(兼容 1/2)"})
continue
@@ -563,7 +583,6 @@ func excelMaterialImportHandler(ctx *svc.ServiceContext) http.HandlerFunc {
if _, e = tx.Material.Create().
SetCode(pr.code).
SetName(pr.name).
SetNillableShortName(strPtr(pr.shortName)).
SetNillableSpec(strPtr(pr.spec)).
SetNillableUnit(strPtr(pr.unit)).
SetNillableDescription(strPtr(pr.description)).