- 新增过程巡检按步骤上报、数量不符上报、工位退料功能 - 增加工单工程编号三级归属字段 - 新增物料安全库存与缺货预警 - 新增附件管理、退库单管理模块 - 优化生产看板展示逻辑与前端页面文案 - 清理冗余的工艺路线相关代码与备份文件
201 lines
6.5 KiB
Go
201 lines
6.5 KiB
Go
package handler
|
||
|
||
import (
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"bj_power_wms/ent"
|
||
"bj_power_wms/ent/attachment"
|
||
"bj_power_wms/internal/svc"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
const (
|
||
// maxAttachmentSize 单文件上限 20MB(图纸 PDF/检验报告足够;超限直接拒绝,避免拖垮服务)
|
||
maxAttachmentSize = 20 << 20
|
||
// uploadRoot 附件磁盘根目录(相对服务运行目录)
|
||
uploadRoot = "uploads"
|
||
)
|
||
|
||
// allowedAttachmentExt 允许的附件扩展名白名单(图纸/报告/图片/表格)。
|
||
// 白名单而非黑名单:避免上传可执行文件。
|
||
var allowedAttachmentExt = map[string]bool{
|
||
".pdf": true, ".png": true, ".jpg": true, ".jpeg": true, ".gif": true,
|
||
".xlsx": true, ".xls": true, ".doc": true, ".docx": true,
|
||
".dwg": true, ".dxf": true, ".zip": true, ".txt": true, ".csv": true,
|
||
}
|
||
|
||
// uploadAttachmentHandler 上传附件(multipart/form-data)
|
||
// 表单字段:bizType、bizId、file
|
||
// 落盘:uploads/<uuid><ext>(纯 ASCII 名,规避中文 URL 编码问题),原始文件名存库。
|
||
func uploadAttachmentHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
if err := r.ParseMultipartForm(maxAttachmentSize); err != nil {
|
||
fail(w, http.StatusBadRequest, "解析上传表单失败(单文件不超过20MB): "+err.Error())
|
||
return
|
||
}
|
||
bizType := strings.TrimSpace(r.FormValue("bizType"))
|
||
bizId := strings.TrimSpace(r.FormValue("bizId"))
|
||
if bizType == "" || bizId == "" {
|
||
fail(w, http.StatusBadRequest, "bizType 与 bizId 必填")
|
||
return
|
||
}
|
||
file, header, err := r.FormFile("file")
|
||
if err != nil {
|
||
fail(w, http.StatusBadRequest, "未取到上传文件: "+err.Error())
|
||
return
|
||
}
|
||
defer file.Close()
|
||
|
||
if header.Size > maxAttachmentSize {
|
||
fail(w, http.StatusBadRequest, "文件超过 20MB 上限,请压缩后重传")
|
||
return
|
||
}
|
||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||
if ext == "" || !allowedAttachmentExt[ext] {
|
||
fail(w, http.StatusBadRequest, "不支持的文件类型(仅支持 PDF/图片/Office/图纸/压缩包)")
|
||
return
|
||
}
|
||
|
||
if err := os.MkdirAll(uploadRoot, 0o755); err != nil {
|
||
fail(w, http.StatusInternalServerError, "创建附件目录失败: "+err.Error())
|
||
return
|
||
}
|
||
storedName := uuid.NewString() + ext
|
||
dst := filepath.Join(uploadRoot, storedName)
|
||
out, err := os.Create(dst)
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, "写入附件失败: "+err.Error())
|
||
return
|
||
}
|
||
defer out.Close()
|
||
size, err := io.Copy(out, file)
|
||
if err != nil {
|
||
_ = os.Remove(dst)
|
||
fail(w, http.StatusInternalServerError, "保存附件失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
rec, err := ctx.EntClient.Attachment.Create().
|
||
SetBizType(bizType).
|
||
SetBizID(bizId).
|
||
SetFileName(header.Filename).
|
||
SetFilePath(storedName).
|
||
SetFileSize(size).
|
||
SetNillableMimeType(strPtr(header.Header.Get("Content-Type"))).
|
||
SetNillableUploadedBy(strPtr(r.Header.Get("X-Username"))).
|
||
Save(ctx0())
|
||
if err != nil {
|
||
_ = os.Remove(dst)
|
||
fail(w, http.StatusInternalServerError, "登记附件失败: "+err.Error())
|
||
return
|
||
}
|
||
|
||
ctx.EventLog.Write(ctx0(), "attachment.upload", r.Header.Get("X-Username"), bizType, bizId,
|
||
"上传附件 "+header.Filename, map[string]any{"fileSize": size})
|
||
ok(w, attachmentView(rec))
|
||
}
|
||
}
|
||
|
||
// attachmentView 附件返回视图:附带可直接预览的 URL
|
||
type attachmentViewRow struct {
|
||
ID int64 `json:"id"`
|
||
BizType string `json:"bizType"`
|
||
BizID string `json:"bizId"`
|
||
FileName string `json:"fileName"`
|
||
FileSize int64 `json:"fileSize"`
|
||
MimeType string `json:"mimeType"`
|
||
UploadedBy string `json:"uploadedBy"`
|
||
CreatedAt int64 `json:"createdAt"`
|
||
URL string `json:"url"`
|
||
}
|
||
|
||
func attachmentView(rec *ent.Attachment) *attachmentViewRow {
|
||
return &attachmentViewRow{
|
||
ID: int64(rec.ID),
|
||
BizType: rec.BizType,
|
||
BizID: rec.BizID,
|
||
FileName: rec.FileName,
|
||
FileSize: rec.FileSize,
|
||
MimeType: rec.MimeType,
|
||
UploadedBy: rec.UploadedBy,
|
||
CreatedAt: rec.CreatedAt,
|
||
URL: "/uploads/" + rec.FilePath,
|
||
}
|
||
}
|
||
|
||
// listAttachmentsHandler 查询某业务对象的附件列表
|
||
// GET /api/attachments?bizType=&bizId=
|
||
func listAttachmentsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
bizType := r.URL.Query().Get("bizType")
|
||
bizId := r.URL.Query().Get("bizId")
|
||
if bizType == "" || bizId == "" {
|
||
fail(w, http.StatusBadRequest, "bizType 与 bizId 必填")
|
||
return
|
||
}
|
||
list, err := ctx.EntClient.Attachment.Query().
|
||
Where(attachment.BizTypeEQ(bizType), attachment.BizIDEQ(bizId)).
|
||
Order(ent.Desc("created_at"), ent.Desc("id")).
|
||
All(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
rows := make([]*attachmentViewRow, 0, len(list))
|
||
for _, rec := range list {
|
||
rows = append(rows, attachmentView(rec))
|
||
}
|
||
ok(w, map[string]any{"list": rows, "total": len(rows)})
|
||
}
|
||
}
|
||
|
||
// deleteAttachmentHandler 删除附件(同时删除磁盘文件)
|
||
// POST /api/attachments/delete body: { id }
|
||
func deleteAttachmentHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req struct {
|
||
ID int64 `json:"id"`
|
||
}
|
||
if err := parseJSON(r, &req); err != nil || req.ID <= 0 {
|
||
fail(w, http.StatusBadRequest, "参数错误:id 必填")
|
||
return
|
||
}
|
||
rec, err := ctx.EntClient.Attachment.Get(ctx0(), int(req.ID))
|
||
if err != nil {
|
||
fail(w, http.StatusNotFound, "附件不存在")
|
||
return
|
||
}
|
||
if err := ctx.EntClient.Attachment.DeleteOneID(int(req.ID)).Exec(ctx0()); err != nil {
|
||
fail(w, http.StatusInternalServerError, "删除附件失败: "+err.Error())
|
||
return
|
||
}
|
||
_ = os.Remove(filepath.Join(uploadRoot, filepath.Base(rec.FilePath)))
|
||
ctx.EventLog.Write(ctx0(), "attachment.delete", r.Header.Get("X-Username"), rec.BizType, rec.BizID,
|
||
"删除附件 "+rec.FileName, nil)
|
||
ok(w, map[string]any{"id": req.ID})
|
||
}
|
||
}
|
||
|
||
// serveUploadFileHandler 附件静态文件服务(PDF 预览/图片查看),免鉴权。
|
||
// GET /uploads/:name —— 仅允许扁平文件名(uuid+ext),显式阻断目录穿越。
|
||
func serveUploadFileHandler() http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
name := filepath.Base(r.URL.Path) // 只取最后一段,消灭 ../ 穿越
|
||
if name == "" || name == "." || name == "/" {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
p := filepath.Join(uploadRoot, name)
|
||
if _, err := os.Stat(p); err != nil {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
http.ServeFile(w, r, p)
|
||
}
|
||
}
|