Files
bj_power/bj_power_workstation/internal/handler/proxy.go
T

179 lines
6.2 KiB
Go
Raw Normal View History

2026-08-28 15:06:01 +08:00
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=。
2026-08-28 15:06:01 +08:00
// 响应体原样透传(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 参数")
2026-08-28 15:06:01 +08:00
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)
2026-08-28 15:06:01 +08:00
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))
2026-08-28 15:06:01 +08:00
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 直链;仅单层文件名)
2026-08-28 15:06:01 +08:00
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 上传落盘格式),拒绝路径穿越与多级/绝对路径。
2026-08-28 15:06:01 +08:00
func forwardFile(w http.ResponseWriter, mesc *mes.Client, cache *pdfcache.Cache, name string) {
name = strings.TrimSpace(name)
if name == "" {
fail(w, http.StatusBadRequest, "缺少文件名")
return
}
if strings.Contains(name, "..") || strings.ContainsAny(name, `\`) ||
strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") ||
strings.Count(name, "/") > 1 || name == "." {
2026-08-28 15:06:01 +08:00
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}})
2026-08-28 15:06:01 +08:00
if err != nil {
fail(w, http.StatusBadGateway, "工艺文件获取失败:"+errString(err))
return
}
// 3) 写入本地预缓存(失败不影响本次响应)
_, _ = cache.Put(name, body)
streamBytes(body, w, name, header.Get("Content-Type"))
}
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)
}