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

158 lines
6.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"net/http"
"net/url"
"bj_power_mes/common/httpx"
"bj_power_mes/internal/logic"
"bj_power_mes/internal/svc"
"github.com/xuri/excelize/v2"
)
// CreateInspectionHandler POST /inspectionsPAD 巡检终端提交记录)
func CreateInspectionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.InspectionReq
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
// 按钮级权限:按记录类型校验(块2/8)
categoryPerm := map[string]string{
"CHECKIN": "sys.inspect:checkin",
"POINT": "sys.inspect:point",
"PROCESS": "sys.inspect:process",
"DONE": "sys.inspect:done",
"ALARM": "sys.inspect:alarm",
}
// 质量检验三类(过程检 QC_PROCESS / 完工检 FINAL / 来料检 INCOMING)与 PAD 巡检共用本接口,
// 持有质量检验录入权限 produce.quality:edit 即放行,避免被巡检按钮权限误拦截。
// 注:PAD 巡检「过程巡检」仍用 PROCESS(走下方 categoryPerm 的 sys.inspect:process),与质检隔离(P1-8)。
qualityCats := map[string]bool{"QC_PROCESS": true, "FINAL": true, "INCOMING": true}
if qualityCats[req.Category] && userHasPerm(r, svcCtx, "produce.quality:edit") {
// 质检录入已校验,跳过巡检按钮权限
} else if code, ok := categoryPerm[req.Category]; ok && !userHasPerm(r, svcCtx, code) {
httpx.FailHTTP(w, http.StatusForbidden, "无操作权限:"+code)
return
}
if err := logic.New(svcCtx).CreateInspection(r.Context(), req, operator(r, "")); err != nil {
httpx.Fail(w, 2301, err.Error())
return
}
httpx.OkMessage(w, "巡检记录已提交", nil)
}
}
// ListInspectionsHandler GET /inspections?category=&operator=&from=&to=&materialCode=&materialName=&reportNo=&page=&pageSize=
// 返回 {list, total},后端真分页(created_at desc / id desc,最新置顶)
// materialCode/materialName/reportNo:质量检验模糊查询(每框内模糊、多框取交集)
func ListInspectionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
page := atoiDefault(q.Get("page"), 1)
pageSize := atoiDefault(q.Get("pageSize"), 50)
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 1000 {
pageSize = 50
}
data, total, err := logic.New(svcCtx).ListInspectionsPaged(r.Context(),
q.Get("category"), q.Get("operator"), q.Get("from"), q.Get("to"),
q.Get("orderNo"), q.Get("stationNo"),
q.Get("materialCode"), q.Get("materialName"), q.Get("reportNo"), page, pageSize)
if err != nil {
httpx.Fail(w, 2302, err.Error())
return
}
httpx.Ok(w, map[string]any{"list": data, "total": total})
}
}
// UploadPhotoHandler POST /inspections/upload(巡检/异常拍照上传)
// 统一存储:<Upload.Dir>/年/月/日/INSPECTION_PHOTO/<uuid>.<ext>;返回相对路径 + 免鉴权直链。
func UploadPhotoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(svcCtx.Config.Upload.MaxMB << 20); err != nil {
httpx.BadRequest(w, "上传文件过大或格式错误")
return
}
res, err := saveTypedUpload(r, "file", svcCtx.Config.Upload.Dir, "inspection", FileTypeInspectionPhoto,
uploadMaxBytes(svcCtx.Config.Upload.MaxSize, svcCtx.Config.Upload.MaxMB, FileTypeInspectionPhoto))
if err != nil {
httpx.Fail(w, 2303, "照片保存失败:"+err.Error())
return
}
httpx.Ok(w, map[string]any{"filename": res.RelPath, "url": "/api/v1/files/" + res.RelPath})
}
}
// QueryBomMaterialHandler GET /quality/bom-material?keyword= 按物料编码或名称/图号检索 BOM 料
func QueryBomMaterialHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, err := logic.New(svcCtx).QueryBomMaterial(r.Context(), r.URL.Query().Get("keyword"))
if err != nil {
httpx.Fail(w, 2304, err.Error())
return
}
httpx.Ok(w, data)
}
}
// InspectionDisposalHandler POST /quality/disposal 质量检验处置(退货/返修/退换)
func InspectionDisposalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Id int `json:"id"`
DisposalType string `json:"disposalType"`
DisposalRemark string `json:"disposalRemark"`
}
if err := httpx.ParseJSON(r, &req); err != nil || req.Id <= 0 {
httpx.BadRequest(w, "缺少 id")
return
}
if err := logic.New(svcCtx).SetInspectionDisposal(r.Context(), req.Id, req.DisposalType, req.DisposalRemark, operator(r, "")); err != nil {
httpx.Fail(w, 2305, err.Error())
return
}
httpx.OkMessage(w, "处置已记录", nil)
}
}
// UnqualifiedReportHandler GET /quality/unqualified-report?id= 一键生成《不合格单》xlsx 模板(预填检验数据)
func UnqualifiedReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := atoiDefault(r.URL.Query().Get("id"), 0)
if id <= 0 {
httpx.Fail(w, 2306, "缺少 id")
return
}
rec, err := logic.New(svcCtx).GetInspectionRecord(r.Context(), id)
if err != nil {
httpx.Fail(w, 2307, err.Error())
return
}
f := excelize.NewFile()
sheet := "不合格单"
f.SetSheetName("Sheet1", sheet)
headers := []string{"报检单号", "图号", "物料名称", "生产厂家", "检验单号", "结论", "处置方式", "处置说明", "记录时间"}
for c, h := range headers {
cell, _ := excelize.CoordinatesToCellName(c+1, 1)
_ = f.SetCellValue(sheet, cell, h)
}
row := []any{
rec.ReportNo, rec.MaterialCode, rec.MaterialName, rec.Manufacturer, rec.InspectionNo,
rec.Result, rec.DisposalType, rec.DisposalRemark, rec.CreatedAt.Format("2006-01-02 15:04:05"),
}
for c, v := range row {
cell, _ := excelize.CoordinatesToCellName(c+1, 2)
_ = f.SetCellValue(sheet, cell, v)
}
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("不合格单.xlsx"))
_, _ = f.WriteTo(w)
}
}