feat: 完成附件存储重构与磁盘监控功能,同步物料档案与入库单规则更新
1. 新增跨平台磁盘空间监控能力,每日7点自动检测附件目录剩余空间,触发阈值告警 2. 重构附件存储方案为年/月/日/文件类型分层结构,统一MES与WMS的附件管理逻辑 3. 对齐物料档案与入库单的质量状态校验规则,仅合格品计入库存与出库 4. 实现入库单作废功能与区域库位的多级父子结构管理 5. 删除物料简称字段,补充规格型号与单位必填项,统一系统数据口径 6. 新增附件中心与磁盘状态查询接口,完善权限控制与操作日志
This commit is contained in:
@@ -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)).
|
||||
|
||||
Reference in New Issue
Block a user