From 77cb594ee47c55c6ee0547b45e5df267308eb277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Fri, 9 Jan 2026 20:59:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/http/request/website.go | 4 +- pkg/types/website.go | 4 +- pkg/webserver/apache/proxy.go | 31 ++-- pkg/webserver/apache/vhost.go | 4 +- pkg/webserver/nginx/upstream.go | 66 +++++-- pkg/webserver/nginx/vhost.go | 4 +- pkg/webserver/types/proxy.go | 1 + pkg/webserver/types/vhost.go | 4 +- web/src/views/website/EditView.vue | 269 ++++++++++++++++------------- 9 files changed, 227 insertions(+), 160 deletions(-) diff --git a/internal/http/request/website.go b/internal/http/request/website.go index c8a212ff..75767655 100644 --- a/internal/http/request/website.go +++ b/internal/http/request/website.go @@ -64,8 +64,8 @@ type WebsiteUpdate struct { OpenBasedir bool `form:"open_basedir" json:"open_basedir"` // 反向代理 - Upstreams map[string]types.Upstream `json:"upstreams"` - Proxies []types.Proxy `json:"proxies"` + Upstreams []types.Upstream `json:"upstreams"` + Proxies []types.Proxy `json:"proxies"` } type WebsiteUpdateRemark struct { diff --git a/pkg/types/website.go b/pkg/types/website.go index cf8b4ea1..9a877891 100644 --- a/pkg/types/website.go +++ b/pkg/types/website.go @@ -44,6 +44,6 @@ type WebsiteSetting struct { OpenBasedir bool `json:"open_basedir"` // 反向代理 - Upstreams map[string]types.Upstream `json:"upstreams"` - Proxies []types.Proxy `json:"proxies"` + Upstreams []types.Upstream `json:"upstreams"` + Proxies []types.Proxy `json:"proxies"` } diff --git a/pkg/webserver/apache/proxy.go b/pkg/webserver/apache/proxy.go index 75c998f8..21565b97 100644 --- a/pkg/webserver/apache/proxy.go +++ b/pkg/webserver/apache/proxy.go @@ -252,7 +252,7 @@ func generateProxyConfig(proxy types.Proxy) string { } // parseBalancerFiles 从 shared 目录解析所有负载均衡配置(Apache 的 upstream 等价物) -func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) { +func parseBalancerFiles(sharedDir string) ([]types.Upstream, error) { entries, err := os.ReadDir(sharedDir) if err != nil { if os.IsNotExist(err) { @@ -261,7 +261,7 @@ func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) { return nil, err } - upstreams := make(map[string]types.Upstream) + var upstreams []types.Upstream for _, entry := range entries { if entry.IsDir() { continue @@ -272,14 +272,13 @@ func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) { continue } - name := matches[2] filePath := filepath.Join(sharedDir, entry.Name()) - upstream, err := parseBalancerFile(filePath) + upstream, err := parseBalancerFile(filePath, matches[2]) if err != nil { continue // 跳过解析失败的文件 } if upstream != nil { - upstreams[name] = *upstream + upstreams = append(upstreams, *upstream) } } @@ -287,7 +286,7 @@ func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) { } // parseBalancerFile 解析单个负载均衡配置文件 -func parseBalancerFile(filePath string) (*types.Upstream, error) { +func parseBalancerFile(filePath string, name string) (*types.Upstream, error) { content, err := os.ReadFile(filePath) if err != nil { return nil, err @@ -295,6 +294,7 @@ func parseBalancerFile(filePath string) (*types.Upstream, error) { contentStr := string(content) upstream := &types.Upstream{ + Name: name, Servers: make(map[string]string), } @@ -342,23 +342,22 @@ func parseBalancerFile(filePath string) (*types.Upstream, error) { } // writeBalancerFiles 将负载均衡配置写入文件 -func writeBalancerFiles(sharedDir string, upstreams map[string]types.Upstream) error { +func writeBalancerFiles(sharedDir string, upstreams []types.Upstream) error { // 删除现有的负载均衡配置文件 if err := clearBalancerFiles(sharedDir); err != nil { return err } - // 写入新的配置文件 - num := 100 - for name, upstream := range upstreams { - fileName := fmt.Sprintf("%03d-balancer-%s.conf", num, name) + // 写入新的配置文件,保持顺序 + for i, upstream := range upstreams { + num := 100 + i + fileName := fmt.Sprintf("%03d-balancer-%s.conf", num, upstream.Name) filePath := filepath.Join(sharedDir, fileName) - content := generateBalancerConfig(name, upstream) + content := generateBalancerConfig(upstream) if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { return fmt.Errorf("failed to write balancer config: %w", err) } - num++ } return nil @@ -391,12 +390,12 @@ func clearBalancerFiles(sharedDir string) error { } // generateBalancerConfig 生成负载均衡配置内容 -func generateBalancerConfig(name string, upstream types.Upstream) string { +func generateBalancerConfig(upstream types.Upstream) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("# Load balancer: %s\n", name)) + sb.WriteString(fmt.Sprintf("# Load balancer: %s\n", upstream.Name)) sb.WriteString("\n") - sb.WriteString(fmt.Sprintf(" \n", name)) + sb.WriteString(fmt.Sprintf(" \n", upstream.Name)) // 服务器列表 for addr, options := range upstream.Servers { diff --git a/pkg/webserver/apache/vhost.go b/pkg/webserver/apache/vhost.go index 3aa329b3..0e5ed0e1 100644 --- a/pkg/webserver/apache/vhost.go +++ b/pkg/webserver/apache/vhost.go @@ -679,13 +679,13 @@ func (v *ProxyVhost) ClearProxies() error { return clearProxyFiles(siteDir) } -func (v *ProxyVhost) Upstreams() map[string]types.Upstream { +func (v *ProxyVhost) Upstreams() []types.Upstream { sharedDir := filepath.Join(v.configDir, "shared") upstreams, _ := parseBalancerFiles(sharedDir) return upstreams } -func (v *ProxyVhost) SetUpstreams(upstreams map[string]types.Upstream) error { +func (v *ProxyVhost) SetUpstreams(upstreams []types.Upstream) error { sharedDir := filepath.Join(v.configDir, "shared") return writeBalancerFiles(sharedDir, upstreams) } diff --git a/pkg/webserver/nginx/upstream.go b/pkg/webserver/nginx/upstream.go index f9bc05c8..2b192664 100644 --- a/pkg/webserver/nginx/upstream.go +++ b/pkg/webserver/nginx/upstream.go @@ -7,6 +7,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/acepanel/panel/pkg/webserver/types" ) @@ -15,7 +16,7 @@ import ( var upstreamFilePattern = regexp.MustCompile(`^(\d{3})-(.+)\.conf$`) // parseUpstreamFiles 从 shared 目录解析所有 upstream 配置 -func parseUpstreamFiles(sharedDir string) (map[string]types.Upstream, error) { +func parseUpstreamFiles(sharedDir string) ([]types.Upstream, error) { entries, err := os.ReadDir(sharedDir) if err != nil { if os.IsNotExist(err) { @@ -24,7 +25,7 @@ func parseUpstreamFiles(sharedDir string) (map[string]types.Upstream, error) { return nil, err } - upstreams := make(map[string]types.Upstream) + var upstreams []types.Upstream for _, entry := range entries { if entry.IsDir() { continue @@ -40,14 +41,13 @@ func parseUpstreamFiles(sharedDir string) (map[string]types.Upstream, error) { continue } - name := matches[2] filePath := filepath.Join(sharedDir, entry.Name()) - upstream, err := parseUpstreamFile(filePath, name) + upstream, err := parseUpstreamFile(filePath, matches[2]) if err != nil { continue // 跳过解析失败的文件 } if upstream != nil { - upstreams[name] = *upstream + upstreams = append(upstreams, *upstream) } } @@ -82,7 +82,9 @@ func parseUpstreamFile(filePath string, expectedName string) (*types.Upstream, e blockContent := matches[2] upstream := &types.Upstream{ - Servers: make(map[string]string), + Name: name, + Servers: make(map[string]string), + Resolver: []string{}, } // 解析负载均衡算法 @@ -95,7 +97,8 @@ func parseUpstreamFile(filePath string, expectedName string) (*types.Upstream, e } // 解析 server 指令 - serverPattern := regexp.MustCompile(`server\s+(\S+)(?:\s+([^;]+))?;`) + // 匹配: server 127.0.0.1:8080; 或 server 127.0.0.1:8080 weight=5; + serverPattern := regexp.MustCompile(`server\s+([^\s;]+)(?:\s+([^;]+))?;`) serverMatches := serverPattern.FindAllStringSubmatch(blockContent, -1) for _, sm := range serverMatches { addr := sm[1] @@ -112,27 +115,48 @@ func parseUpstreamFile(filePath string, expectedName string) (*types.Upstream, e upstream.Keepalive, _ = strconv.Atoi(km[1]) } + // 解析 resolver + resolverPattern := regexp.MustCompile(`resolver\s+([^;]+);`) + if rm := resolverPattern.FindStringSubmatch(blockContent); rm != nil { + parts := strings.Fields(rm[1]) + upstream.Resolver = parts + } + + // 解析 resolver_timeout + resolverTimeoutPattern := regexp.MustCompile(`resolver_timeout\s+(\d+)([smh]?);`) + if rtm := resolverTimeoutPattern.FindStringSubmatch(blockContent); rtm != nil { + value, _ := strconv.Atoi(rtm[1]) + unit := rtm[2] + switch unit { + case "m": + upstream.ResolverTimeout = time.Duration(value) * time.Minute + case "h": + upstream.ResolverTimeout = time.Duration(value) * time.Hour + default: + upstream.ResolverTimeout = time.Duration(value) * time.Second + } + } + return upstream, nil } // writeUpstreamFiles 将 upstream 配置写入文件 -func writeUpstreamFiles(sharedDir string, upstreams map[string]types.Upstream) error { +func writeUpstreamFiles(sharedDir string, upstreams []types.Upstream) error { // 删除现有的 upstream 配置文件 if err := clearUpstreamFiles(sharedDir); err != nil { return err } - // 写入新的配置文件 - num := UpstreamStartNum - for name, upstream := range upstreams { - fileName := fmt.Sprintf("%03d-%s.conf", num, name) + // 写入新的配置文件,保持顺序 + for i, upstream := range upstreams { + num := UpstreamStartNum + i + fileName := fmt.Sprintf("%03d-%s.conf", num, upstream.Name) filePath := filepath.Join(sharedDir, fileName) - content := generateUpstreamConfig(name, upstream) + content := generateUpstreamConfig(upstream) if err := os.WriteFile(filePath, []byte(content), 0644); err != nil { return fmt.Errorf("failed to write upstream config: %w", err) } - num++ } return nil @@ -171,17 +195,25 @@ func clearUpstreamFiles(sharedDir string) error { } // generateUpstreamConfig 生成 upstream 配置内容 -func generateUpstreamConfig(name string, upstream types.Upstream) string { +func generateUpstreamConfig(upstream types.Upstream) string { var sb strings.Builder - sb.WriteString(fmt.Sprintf("# Upstream: %s\n", name)) - sb.WriteString(fmt.Sprintf("upstream %s {\n", name)) + sb.WriteString(fmt.Sprintf("# Upstream: %s\n", upstream.Name)) + sb.WriteString(fmt.Sprintf("upstream %s {\n", upstream.Name)) // 负载均衡算法 if upstream.Algo != "" { sb.WriteString(fmt.Sprintf(" %s;\n", upstream.Algo)) } + // resolver 配置 + if len(upstream.Resolver) > 0 { + sb.WriteString(fmt.Sprintf(" resolver %s;\n", strings.Join(upstream.Resolver, " "))) + if upstream.ResolverTimeout > 0 { + sb.WriteString(fmt.Sprintf(" resolver_timeout %ds;\n", int(upstream.ResolverTimeout.Seconds()))) + } + } + // 服务器列表 for addr, options := range upstream.Servers { if options != "" { diff --git a/pkg/webserver/nginx/vhost.go b/pkg/webserver/nginx/vhost.go index eef75084..5c6a68f1 100644 --- a/pkg/webserver/nginx/vhost.go +++ b/pkg/webserver/nginx/vhost.go @@ -730,13 +730,13 @@ func (v *ProxyVhost) ClearProxies() error { return clearProxyFiles(siteDir) } -func (v *ProxyVhost) Upstreams() map[string]types.Upstream { +func (v *ProxyVhost) Upstreams() []types.Upstream { sharedDir := filepath.Join(v.configDir, "shared") upstreams, _ := parseUpstreamFiles(sharedDir) return upstreams } -func (v *ProxyVhost) SetUpstreams(upstreams map[string]types.Upstream) error { +func (v *ProxyVhost) SetUpstreams(upstreams []types.Upstream) error { sharedDir := filepath.Join(v.configDir, "shared") return writeUpstreamFiles(sharedDir, upstreams) } diff --git a/pkg/webserver/types/proxy.go b/pkg/webserver/types/proxy.go index 126596b1..9002207c 100644 --- a/pkg/webserver/types/proxy.go +++ b/pkg/webserver/types/proxy.go @@ -17,6 +17,7 @@ type Proxy struct { // Upstream 上游服务器配置 type Upstream struct { + Name string `form:"name" json:"name" validate:"required"` // 上游名称,如: "backend" Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["server1"] = "weight=5 resolve" Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "ip_hash" Keepalive int `form:"keepalive" json:"keepalive"` // 保持连接数,如: 32 diff --git a/pkg/webserver/types/vhost.go b/pkg/webserver/types/vhost.go index 659ffb72..381645d5 100644 --- a/pkg/webserver/types/vhost.go +++ b/pkg/webserver/types/vhost.go @@ -133,9 +133,9 @@ type VhostProxy interface { ClearProxies() error // Upstreams 取上游服务器配置 - Upstreams() map[string]Upstream + Upstreams() []Upstream // SetUpstreams 设置上游服务器配置 - SetUpstreams(upstreams map[string]Upstream) error + SetUpstreams(upstreams []Upstream) error // ClearUpstreams 清除所有上游服务器配置 ClearUpstreams() error } diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue index 2563490d..5eb4a27c 100644 --- a/web/src/views/website/EditView.vue +++ b/web/src/views/website/EditView.vue @@ -46,7 +46,7 @@ const { data: setting, send: fetchSetting } = useRequest(website.config(Number(i php: 0, rewrite: '', open_basedir: false, - upstreams: {}, + upstreams: [], proxies: [] } }) @@ -182,54 +182,48 @@ const hasArg = (args: string[], arg: string) => { } // ========== Upstreams 相关 ========== -const upstreamAlgoOptions = [ - { label: $gettext('Round Robin (default)'), value: '' }, - { label: 'least_conn', value: 'least_conn' }, - { label: 'ip_hash', value: 'ip_hash' }, - { label: 'hash', value: 'hash' }, - { label: 'random', value: 'random' } -] - // 添加新的上游 const addUpstream = () => { - const name = `backend_${Date.now()}` + const name = `${setting.value.name.replace(/-/g, '_')}_upstream_${(setting.value.upstreams?.length || 0) + 1}` if (!setting.value.upstreams) { - setting.value.upstreams = {} + setting.value.upstreams = [] } - setting.value.upstreams[name] = { + setting.value.upstreams.push({ + name, servers: {}, algo: '', - keepalive: 32 - } + keepalive: 32, + resolver: [], + resolver_timeout: 5 * 1000000000 // 5秒,以纳秒为单位 + }) } // 删除上游 -const removeUpstream = (name: string) => { +const removeUpstream = (index: number) => { if (setting.value.upstreams) { - delete setting.value.upstreams[name] + setting.value.upstreams.splice(index, 1) } } -// 重命名上游 -const renameUpstream = (oldName: string, newName: string) => { - if (!setting.value.upstreams || oldName === newName) return - if (setting.value.upstreams[newName]) { - window.$message.error($gettext('Upstream name already exists')) - return - } - const upstream = setting.value.upstreams[oldName] - delete setting.value.upstreams[oldName] - setting.value.upstreams[newName] = upstream -} - // 为上游添加服务器 -const addServerToUpstream = (upstreamName: string) => { - if (!setting.value.upstreams[upstreamName].servers) { - setting.value.upstreams[upstreamName].servers = {} +const addServerToUpstream = (index: number) => { + const upstream = setting.value.upstreams[index] + if (!upstream.servers) { + upstream.servers = {} } - setting.value.upstreams[upstreamName].servers[ - `127.0.0.1:${8080 + Object.keys(setting.value.upstreams[upstreamName].servers).length}` - ] = '' + upstream.servers[`127.0.0.1:${8080 + Object.keys(upstream.servers).length}`] = '' +} + +// 更新上游超时时间值 +const updateUpstreamTimeoutValue = (upstream: any, value: number) => { + const parsed = parseDuration(upstream.resolver_timeout) + upstream.resolver_timeout = buildDuration(value, parsed.unit) +} + +// 更新上游超时时间单位 +const updateUpstreamTimeoutUnit = (upstream: any, unit: string) => { + const parsed = parseDuration(upstream.resolver_timeout) + upstream.resolver_timeout = buildDuration(parsed.value, unit) } // ========== Proxies 相关 ========== @@ -245,7 +239,6 @@ const locationMatchTypes = [ // 解析 location 字符串,返回匹配类型和表达式 const parseLocation = (location: string): { type: string; expression: string } => { if (!location) return { type: '', expression: '/' } - // 精确匹配 = if (location.startsWith('= ')) { return { type: '=', expression: location.slice(2) } @@ -277,11 +270,9 @@ const buildLocation = (type: string, expression: string): string => { const extractHostFromUrl = (url: string): string => { try { const urlObj = new URL(url) - return urlObj.host + return urlObj.hostname } catch { - // 如果不是有效的 URL,尝试简单提取 - const match = url.match(/^https?:\/\/([^/]+)/) - return match ? match[1] : '' + return '' } } @@ -476,92 +467,136 @@ const updateTimeoutUnit = (proxy: any, unit: string) => { - - {{ - $gettext( - 'Upstreams define backend server groups for load balancing. You can reference an upstream in proxy settings using "http://upstream_name".' - ) - }} - - - - - - - - - - - - - + + + - + {{ $gettext('No upstreams configured') }} + {{ $gettext('Add Upstream') }}