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

205 lines
7.1 KiB
Go
Raw 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 (
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"bj_power_workstation/internal/mes"
"bj_power_workstation/internal/pdfcache"
"bj_power_workstation/internal/svc"
)
// taskCurrentHandler 代理 MES GET /api/internal/station/task?stationNo=。
// 响应体原样透传(MES 已按 {code,message,data} 封装,不再二次包装)。
func taskCurrentHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
stationNo := atoi(strings.TrimSpace(r.URL.Query().Get("stationNo")), 0)
if stationNo <= 0 {
fail(w, http.StatusBadRequest, "缺少 stationNo 参数")
return
}
body, _, err := ctx.Mes.Get("/api/internal/station/task", url.Values{"stationNo": {strconv.Itoa(stationNo)}})
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
}
// workloadHandler 代理 MES GET /api/internal/station/workload?operator=&stationNo=&from=&to=(块6)。
// 工位终端「我的工作量」查询当前登录人全部工位的工作,stationNo 为空=全部工位;MES 响应体原样透传。
func workloadHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
operator := strings.TrimSpace(r.URL.Query().Get("operator"))
stationNo := strings.TrimSpace(r.URL.Query().Get("stationNo"))
from := strings.TrimSpace(r.URL.Query().Get("from"))
to := strings.TrimSpace(r.URL.Query().Get("to"))
params := url.Values{}
if operator != "" {
params.Set("operator", operator)
}
if stationNo != "" {
params.Set("stationNo", stationNo)
}
if from != "" {
params.Set("from", from)
}
if to != "" {
params.Set("to", to)
}
body, _, err := ctx.Mes.Get("/api/internal/station/workload", params)
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
}
// stationsHandler 代理 MES GET /stations(工位数以数据库为准,不写死 12),原样透传响应体。
func stationsHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, _, err := ctx.Mes.Get("/stations", nil)
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
}
// ---------- 工艺 PDF 直通代理(含本地预缓存) ----------
//
// 前端 iframe/window.open 无法携带 Authorization,故提供免 JWT 前缀 /pdfopen/<name>
// 内部仍走 MES /api/internal/files/<name>(由 X-API-TOKEN 保护)。
// 启用本地预缓存后,首次访问回源下载并落盘,后续直接读本地缓存(断网也能查看)。
// 两端点均把文件流原样转发并透传 Content-Type,不套 {code,message,data}。
// pdfQueryHandler GET /api/pdf?name=<file>(需登录)
func pdfQueryHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
forwardFile(w, ctx.Mes, ctx.PdfCache, strings.TrimSpace(r.URL.Query().Get("name")))
}
}
// pdfOpenHandler GET /pdfopen/<name>(免鉴权,供 iframe 直链;仅单层文件名)
func pdfOpenHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
raw := strings.TrimPrefix(r.URL.EscapedPath(), "/pdfopen/")
name, err := url.PathUnescape(raw)
if err != nil {
fail(w, http.StatusBadRequest, "非法文件名")
return
}
forwardFile(w, ctx.Mes, ctx.PdfCache, name)
}
}
// pdfViewHandler GET /pdfview?name=<encoded>(免鉴权,注册在鉴权中间件之前的路由组)。
// 以 query 传文件名,天然支持 MES 落盘的「日期子目录/文件名」(如 2026-09-16/xxx.pdf),
// 供工艺流程 PDF 与工序步骤图纸附件(Item J)统一使用。
func pdfViewHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
forwardFile(w, ctx.Mes, ctx.PdfCache, strings.TrimSpace(r.URL.Query().Get("name")))
}
}
// forwardFile 校验文件名后拉取 MES 文件流并原样转发;优先命中本地预缓存。
// 允许「单层日期子目录/文件名」(MES 上传落盘格式),拒绝路径穿越与多级/绝对路径。
func forwardFile(w http.ResponseWriter, mesc *mes.Client, cache *pdfcache.Cache, name string) {
name = strings.TrimSpace(name)
if name == "" {
fail(w, http.StatusBadRequest, "缺少文件名")
return
}
// 统一存储后 name 为多级相对路径(年/月/日/文件类型/uuid.ext),逐段校验:
// 禁止 ../ 穿越、反斜杠、绝对路径、空段、以及非法字符;段数上限防超长构造。
if !validRelPath(name) {
fail(w, http.StatusBadRequest, "非法文件名")
return
}
// 1) 命中本地预缓存 → 直接回源本地文件
if p, ok := cache.Get(name); ok {
f, err := os.Open(p)
if err == nil {
stream(f, w, name, "")
return
}
f.Close()
}
// 2) 回源 MES 下载(内部文件接口按 query 参数 name 读取,支持日期子目录)
body, header, err := mesc.Get("/api/internal/files", url.Values{"name": {name}})
if err != nil {
fail(w, http.StatusBadGateway, "工艺文件获取失败:"+errString(err))
return
}
// 3) 写入本地预缓存(失败不影响本次响应)
_, _ = cache.Put(name, body)
streamBytes(body, w, name, header.Get("Content-Type"))
}
// validRelPath 校验「年/月/日/文件类型/uuid.ext」这类多级相对路径是否安全。
// 规则:非空、最多 8 段、无绝对路径/反斜杠/穿越段、每段仅允许字母数字与 . _ -。
func validRelPath(name string) bool {
if name == "" || strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") ||
strings.Contains(name, "\\") || strings.Contains(name, "..") {
return false
}
parts := strings.Split(name, "/")
if len(parts) > 8 {
return false
}
for _, p := range parts {
if p == "" || p == "." || p == ".." {
return false
}
for _, r := range p {
ok := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-'
if !ok {
return false
}
}
}
return true
}
func stream(f io.ReadCloser, w http.ResponseWriter, name, contentType string) {
defer f.Close()
ct := contentType
if ct == "" {
ct = "application/pdf"
}
w.Header().Set("Content-Type", ct)
w.Header().Set("X-Original-Filename", strconv.Quote(name))
w.Header().Set("X-PDF-Cache", "hit")
w.WriteHeader(http.StatusOK)
_, _ = io.Copy(w, f)
}
func streamBytes(body []byte, w http.ResponseWriter, name, contentType string) {
ct := contentType
if ct == "" {
ct = "application/pdf"
}
w.Header().Set("Content-Type", ct)
w.Header().Set("X-Original-Filename", strconv.Quote(name))
w.Header().Set("X-PDF-Cache", "miss")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}