118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package handler
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httputil"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
|
||
"sundynix-micro-go/app/gateway/internal/config"
|
||
|
||
"github.com/zeromicro/go-zero/core/logx"
|
||
)
|
||
|
||
// ProxyRouter 反向代理路由器
|
||
type ProxyRouter struct {
|
||
routes []*route
|
||
}
|
||
|
||
type route struct {
|
||
prefix string
|
||
proxy *httputil.ReverseProxy
|
||
target string
|
||
}
|
||
|
||
// NewProxyRouter 根据配置构建路由表
|
||
func NewProxyRouter(upstreams []config.Upstream) *ProxyRouter {
|
||
router := &ProxyRouter{}
|
||
|
||
for _, u := range upstreams {
|
||
targetURL, err := url.Parse(u.Target)
|
||
if err != nil {
|
||
logx.Errorf("解析上游地址失败 [%s]: %v", u.Target, err)
|
||
continue
|
||
}
|
||
|
||
target := targetURL // 显式捕获循环变量
|
||
proxy := &httputil.ReverseProxy{
|
||
Rewrite: func(pr *httputil.ProxyRequest) {
|
||
pr.SetXForwarded()
|
||
pr.Out.URL.Scheme = target.Scheme
|
||
pr.Out.URL.Host = target.Host
|
||
pr.Out.Host = target.Host
|
||
// 路径透传:前端请求 /api/user/info -> 转发到 user-api 的 /api/user/info
|
||
// 不剥前缀,后端API的路由本身就包含 /api/user 前缀
|
||
},
|
||
}
|
||
|
||
// 自定义 Transport:超时控制
|
||
proxy.Transport = &http.Transport{
|
||
MaxIdleConns: 100,
|
||
MaxIdleConnsPerHost: 20,
|
||
IdleConnTimeout: 90 * time.Second,
|
||
ResponseHeaderTimeout: 30 * time.Second, // 上游响应头超时,超出则触发 ErrorHandler
|
||
}
|
||
|
||
prefix := u.Prefix
|
||
targetAddr := u.Target
|
||
|
||
// ErrorHandler:处理网络层错误(连接失败、超时等)
|
||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||
logx.Errorf("[Gateway] ❌ 上游连接失败 | %s %s -> %s | 错误: %v",
|
||
r.Method, r.URL.Path, targetAddr, err)
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
w.WriteHeader(http.StatusBadGateway)
|
||
fmt.Fprintf(w, `{"code":502,"msg":"上游服务不可用,请检查 %s 是否正常运行"}`, prefix)
|
||
}
|
||
|
||
// ModifyResponse:捕获上游返回的 4xx/5xx 并记录日志(网络层正常但业务异常)
|
||
proxy.ModifyResponse = func(resp *http.Response) error {
|
||
if resp.StatusCode >= 500 {
|
||
// 读取响应体用于日志(最多 1KB,读完后要写回)
|
||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
|
||
resp.Body = io.NopCloser(bytes.NewBuffer(body))
|
||
logx.Errorf("[Gateway] ⚠️ 上游服务异常 | %s %s -> %s | 状态码: %d | 响应: %s",
|
||
resp.Request.Method, resp.Request.URL.Path, targetAddr,
|
||
resp.StatusCode, string(body))
|
||
} else if resp.StatusCode >= 400 {
|
||
logx.Infof("[Gateway] ℹ️ 上游返回客户端错误 | %s %s | 状态码: %d",
|
||
resp.Request.Method, resp.Request.URL.Path, resp.StatusCode)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
router.routes = append(router.routes, &route{
|
||
prefix: u.Prefix,
|
||
proxy: proxy,
|
||
target: u.Target,
|
||
})
|
||
|
||
logx.Infof("路由注册: %s -> %s", u.Prefix, u.Target)
|
||
}
|
||
|
||
return router
|
||
}
|
||
|
||
// ServeHTTP 根据 URL 前缀匹配路由并转发
|
||
func (pr *ProxyRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
path := r.URL.Path
|
||
|
||
for _, rt := range pr.routes {
|
||
if strings.HasPrefix(path, rt.prefix) {
|
||
logx.Infof("[Gateway] → %s %s -> %s", r.Method, path, rt.target)
|
||
rt.proxy.ServeHTTP(w, r)
|
||
return
|
||
}
|
||
}
|
||
|
||
// 没有匹配的路由
|
||
logx.Errorf("[Gateway] ❌ 路由未找到: %s %s", r.Method, path)
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
w.WriteHeader(http.StatusNotFound)
|
||
fmt.Fprintf(w, `{"code":404,"msg":"路由未找到: %s"}`, path)
|
||
}
|