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) }