Repository: gogf/gf

Issue:
net/ghttp: Server Domain if is empty str, bind handler pattern will add @ which is not expect
### Go version

go version go1.22.6 linux/amd64

### GoFrame version

v2.7.1

### Can this bug be reproduced with the latest release?

Option Yes

### What did you do?

```go
s.Domain(strings.Join(config.AppConfig().Server.Domains, ",")).Group()
```
if config.AppConfig().Server.Domains is empty, strings.Join(config.AppConfig().Server.Domains, ",") will be a empty str (""),
and in Bind logic 
```
// Domain creates and returns a domain object for management for one or more domains.
func (s *Server) Domain(domains string) *Domain {
	d := &Domain{
		server:  s,
		domains: make(map[string]struct{}),
	}
	for _, v := range strings.Split(domains, ",") {
		d.domains[strings.TrimSpace(v)] = struct{}{}
	}
	return d
}

// BindHandler binds the handler for the specified pattern.
func (d *Domain) BindHandler(pattern string, handler interface{}) {
	for domain := range d.domains {
		d.server.BindHandler(pattern+"@"+domain, handler)
	}
}
```

for range domains, actually d.server.BindHandler(pattern+"@"+"", handler)
eg:
``` default | :8008   | POST   | /api/db/sql/queryStream@  ```

### What did you see happen?

<img width="516" alt="image" src="https://github.com/user-attachments/assets/ec8fe064-9c03-4566-9d79-86ddc979bdb2" />


### What did you expect to see?

if domains is emtpy, this domain setting is not needed. 
binding url is  ``` default | :8008   | POST   | /api/db/sql/queryStream``` not queryStream@

Hints from issue comments:

Solution patch:
diff --git a/net/ghttp/ghttp_server_domain.go b/net/ghttp/ghttp_server_domain.go
index 10a98d780..1e49a4529 100644
--- a/net/ghttp/ghttp_server_domain.go
+++ b/net/ghttp/ghttp_server_domain.go
@@ -32,7 +32,7 @@ func (s *Server) Domain(domains string) *Domain {
 // BindHandler binds the handler for the specified pattern.
 func (d *Domain) BindHandler(pattern string, handler any) {
 	for domain := range d.domains {
-		d.server.BindHandler(pattern+"@"+domain, handler)
+		d.server.BindHandler(patternBindDomain(pattern, domain), handler)
 	}
 }
 
@@ -40,7 +40,7 @@ func (d *Domain) doBindHandler(ctx context.Context, in doBindHandlerInput) {
 	for domain := range d.domains {
 		d.server.doBindHandler(ctx, doBindHandlerInput{
 			Prefix:     in.Prefix,
-			Pattern:    in.Pattern + "@" + domain,
+			Pattern:    patternBindDomain(in.Pattern, domain),
 			FuncInfo:   in.FuncInfo,
 			Middleware: in.Middleware,
 			Source:     in.Source,
@@ -51,7 +51,7 @@ func (d *Domain) doBindHandler(ctx context.Context, in doBindHandlerInput) {
 // BindObject binds the object for the specified pattern.
 func (d *Domain) BindObject(pattern string, obj any, methods ...string) {
 	for domain := range d.domains {
-		d.server.BindObject(pattern+"@"+domain, obj, methods...)
+		d.server.BindObject(patternBindDomain(pattern, domain), obj, methods...)
 	}
 }
 
@@ -59,7 +59,7 @@ func (d *Domain) doBindObject(ctx context.Context, in doBindObjectInput) {
 	for domain := range d.domains {
 		d.server.doBindObject(ctx, doBindObjectInput{
 			Prefix:     in.Prefix,
-			Pattern:    in.Pattern + "@" + domain,
+			Pattern:    patternBindDomain(in.Pattern, domain),
 			Object:     in.Object,
 			Method:     in.Method,
 			Middleware: in.Middleware,
@@ -71,7 +71,7 @@ func (d *Domain) doBindObject(ctx context.Context, in doBindObjectInput) {
 // BindObjectMethod binds the method for the specified pattern.
 func (d *Domain) BindObjectMethod(pattern string, obj any, method string) {
 	for domain := range d.domains {
-		d.server.BindObjectMethod(pattern+"@"+domain, obj, method)
+		d.server.BindObjectMethod(patternBindDomain(pattern, domain), obj, method)
 	}
 }
 
@@ -79,7 +79,7 @@ func (d *Domain) doBindObjectMethod(ctx context.Context, in doBindObjectMethodIn
 	for domain := range d.domains {
 		d.server.doBindObjectMethod(ctx, doBindObjectMethodInput{
 			Prefix:     in.Prefix,
-			Pattern:    in.Pattern + "@" + domain,
+			Pattern:    patternBindDomain(in.Pattern, domain),
 			Object:     in.Object,
 			Method:     in.Method,
 			Middleware: in.Middleware,
@@ -91,7 +91,7 @@ func (d *Domain) doBindObjectMethod(ctx context.Context, in doBindObjectMethodIn
 // BindObjectRest binds the RESTful API for the specified pattern.
 func (d *Domain) BindObjectRest(pattern string, obj any) {
 	for domain := range d.domains {
-		d.server.BindObjectRest(pattern+"@"+domain, obj)
+		d.server.BindObjectRest(patternBindDomain(pattern, domain), obj)
 	}
 }
 
@@ -99,7 +99,7 @@ func (d *Domain) doBindObjectRest(ctx context.Context, in doBindObjectInput) {
 	for domain := range d.domains {
 		d.server.doBindObjectRest(ctx, doBindObjectInput{
 			Prefix:     in.Prefix,
-			Pattern:    in.Pattern + "@" + domain,
+			Pattern:    patternBindDomain(in.Pattern, domain),
 			Object:     in.Object,
 			Method:     in.Method,
 			Middleware: in.Middleware,
@@ -111,7 +111,7 @@ func (d *Domain) doBindObjectRest(ctx context.Context, in doBindObjectInput) {
 // BindHookHandler binds the hook handler for the specified pattern.
 func (d *Domain) BindHookHandler(pattern string, hook HookName, handler HandlerFunc) {
 	for domain := range d.domains {
-		d.server.BindHookHandler(pattern+"@"+domain, hook, handler)
+		d.server.BindHookHandler(patternBindDomain(pattern, domain), hook, handler)
 	}
 }
 
@@ -119,7 +119,7 @@ func (d *Domain) doBindHookHandler(ctx context.Context, in doBindHookHandlerInpu
 	for domain := range d.domains {
 		d.server.doBindHookHandler(ctx, doBindHookHandlerInput{
 			Prefix:   in.Prefix,
-			Pattern:  in.Pattern + "@" + domain,
+			Pattern:  patternBindDomain(in.Pattern, domain),
 			HookName: in.HookName,
 			Handler:  in.Handler,
 			Source:   in.Source,
@@ -130,7 +130,7 @@ func (d *Domain) doBindHookHandler(ctx context.Context, in doBindHookHandlerInpu
 // BindHookHandlerByMap binds the hook handler for the specified pattern.
 func (d *Domain) BindHookHandlerByMap(pattern string, hookMap map[HookName]HandlerFunc) {
 	for domain := range d.domains {
-		d.server.BindHookHandlerByMap(pattern+"@"+domain, hookMap)
+		d.server.BindHookHandlerByMap(patternBindDomain(pattern, domain), hookMap)
 	}
 }
 
@@ -151,14 +151,14 @@ func (d *Domain) BindStatusHandlerByMap(handlerMap map[int]HandlerFunc) {
 // BindMiddleware binds the middleware for the specified pattern.
 func (d *Domain) BindMiddleware(pattern string, handlers ...HandlerFunc) {
 	for domain := range d.domains {
-		d.server.BindMiddleware(pattern+"@"+domain, handlers...)
+		d.server.BindMiddleware(patternBindDomain(pattern, domain), handlers...)
 	}
 }
 
 // BindMiddlewareDefault binds the default middleware for the specified pattern.
 func (d *Domain) BindMiddlewareDefault(handlers ...HandlerFunc) {
 	for domain := range d.domains {
-		d.server.BindMiddleware(defaultMiddlewarePattern+"@"+domain, handlers...)
+		d.server.BindMiddleware(patternBindDomain(defaultMiddlewarePattern, domain), handlers...)
 	}
 }
 
@@ -166,3 +166,10 @@ func (d *Domain) BindMiddlewareDefault(handlers ...HandlerFunc) {
 func (d *Domain) Use(handlers ...HandlerFunc) {
 	d.BindMiddlewareDefault(handlers...)
 }
+
+func patternBindDomain(pattern, domain string) string {
+	if domain != "" {
+		return pattern + "@" + domain
+	}
+	return pattern
+}



Test patch:
diff --git a/net/ghttp/ghttp_z_unit_feature_router_domain_basic_test.go b/net/ghttp/ghttp_z_unit_feature_router_domain_basic_test.go
index d9c91428f..f0c1f36b3 100644
--- a/net/ghttp/ghttp_z_unit_feature_router_domain_basic_test.go
+++ b/net/ghttp/ghttp_z_unit_feature_router_domain_basic_test.go
@@ -363,3 +363,37 @@ func Test_Router_DomainGroup(t *testing.T) {
 		t.Assert(client2.DeleteContent(ctx, "/app/comment/20"), "Not Found")
 	})
 }
+
+// issue#4100
+func TestIssue4100(t *testing.T) {
+	s := g.Server(guid.S())
+	d := s.Domain("")
+	d.BindHandler("/:name", func(r *ghttp.Request) {
+		r.Response.Write("/:name")
+	})
+	d.BindHandler("/:name/update", func(r *ghttp.Request) {
+		r.Response.Write(r.Get("name"))
+	})
+	d.BindHandler("/:name/:action", func(r *ghttp.Request) {
+		r.Response.Write(r.Get("action"))
+	})
+	d.BindHandler("/:name/*any", func(r *ghttp.Request) {
+		r.Response.Write(r.Get("any"))
+	})
+	d.BindHandler("/user/list/{field}.html", func(r *ghttp.Request) {
+		r.Response.Write(r.Get("field"))
+	})
+	s.SetDumpRouterMap(false)
+	s.Start()
+	defer s.Shutdown()
+
+	time.Sleep(100 * time.Millisecond)
+	gtest.C(t, func(t *gtest.T) {
+		client := g.Client()
+		client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", s.GetListenedPort()))
+		t.Assert(client.GetContent(ctx, "/john"), "")
+		t.Assert(client.GetContent(ctx, "/john/update"), "john")
+		t.Assert(client.GetContent(ctx, "/john/edit"), "edit")
+		t.Assert(client.GetContent(ctx, "/user/list/100.html"), "100")
+	})
+}