Files

118 lines
3.4 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 (
"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)
}