优化结构

This commit is contained in:
Yvan
2026-08-27 16:20:37 +08:00
parent 001e39da50
commit 653414a1da
54 changed files with 1257 additions and 3450 deletions
+98 -26
View File
@@ -235,6 +235,62 @@ var routes = map[string]routeValue{
"PUT /user/setUserInfo": {group: "系统用户", description: "设置用户信息", audit: true},
}
// compiled is one catalog entry with its path pre-split into segments so that
// per-request matching neither parses the map key nor allocates. wildcard marks
// a Gin catch-all tail ("/*any"); that segment is dropped from parts because it
// absorbs every remaining segment instead of matching one.
type compiled struct {
parts []string
wildcard bool
descriptor Descriptor
}
// exact answers a canonical "METHOD /path" in a single map hit. byMethod holds
// the same entries pre-split and bucketed by method, so the fallback needed for
// path parameters and router prefixes scans one method's routes rather than the
// whole catalog, and never re-splits a pattern.
var (
exact = make(map[string]Descriptor, len(routes))
byMethod = make(map[string][]compiled)
)
func init() {
for key, value := range routes {
descriptor := descriptorFrom(key, value)
exact[descriptorKey(descriptor.Method, descriptor.Path)] = descriptor
entry := compiled{parts: splitPath(descriptor.Path), descriptor: descriptor}
if last := len(entry.parts) - 1; last >= 0 && strings.HasPrefix(entry.parts[last], "*") {
entry.parts, entry.wildcard = entry.parts[:last], true
}
byMethod[descriptor.Method] = append(byMethod[descriptor.Method], entry)
}
// Suffix matching lets two patterns answer the same URL. Ordering the
// buckets most-specific-first makes the winner deterministic instead of
// dependent on map iteration order.
for method := range byMethod {
entries := byMethod[method]
sort.SliceStable(entries, func(i, j int) bool {
if len(entries[i].parts) != len(entries[j].parts) {
return len(entries[i].parts) > len(entries[j].parts)
}
if entries[i].wildcard != entries[j].wildcard {
return entries[j].wildcard
}
return literalCount(entries[i].parts) > literalCount(entries[j].parts)
})
}
}
func literalCount(parts []string) int {
count := 0
for _, part := range parts {
if !strings.HasPrefix(part, ":") {
count++
}
}
return count
}
func splitKey(key string) (string, string) {
parts := strings.SplitN(key, " ", 2)
if len(parts) != 2 {
@@ -267,20 +323,52 @@ func descriptorFrom(key string, value routeValue) Descriptor {
}
}
func pathMatches(pattern, path string) bool {
patternParts := strings.Split(strings.Trim(normalizePath(pattern), "/"), "/")
pathParts := strings.Split(strings.Trim(normalizePath(path), "/"), "/")
if len(patternParts) == 1 && patternParts[0] == "" {
return len(pathParts) == 1 && pathParts[0] == ""
func splitPath(path string) []string {
return strings.Split(strings.Trim(normalizePath(path), "/"), "/")
}
// Lookup accepts either a canonical Gin route template or an actual URL. A
// configured router prefix is tolerated by matching canonical paths by suffix.
// Every middleware resolves the descriptor on each request, so the pattern set
// is pre-split at init: a miss on the exact key costs one path split and a scan
// of the entries declared for that method, not a full catalog walk.
func Lookup(method, path string) (Descriptor, bool) {
if descriptor, ok := exact[descriptorKey(method, path)]; ok {
return descriptor, true
}
if len(pathParts) < len(patternParts) {
parts := splitPath(path)
for _, entry := range byMethod[strings.ToUpper(strings.TrimSpace(method))] {
if entry.matches(parts) {
return entry.descriptor, true
}
}
return Descriptor{}, false
}
// matches compares the pre-split pattern against a pre-split request path.
// Canonical patterns match by suffix so a router prefix is tolerated; a
// wildcard tail absorbs any number of trailing segments, so its fixed head is
// searched at every offset that leaves the wildcard something to consume.
func (c compiled) matches(pathParts []string) bool {
if len(c.parts) == 0 {
return !c.wildcard && len(pathParts) == 1 && pathParts[0] == ""
}
if len(pathParts) < len(c.parts) {
return false
}
offset := len(pathParts) - len(patternParts)
for index, patternPart := range patternParts {
if strings.HasPrefix(patternPart, "*") {
return true
if c.wildcard {
for offset := 0; offset+len(c.parts) < len(pathParts); offset++ {
if matchAt(c.parts, pathParts, offset) {
return true
}
}
return false
}
return matchAt(c.parts, pathParts, len(pathParts)-len(c.parts))
}
func matchAt(patternParts, pathParts []string, offset int) bool {
for index, patternPart := range patternParts {
actual := pathParts[offset+index]
if strings.HasPrefix(patternPart, ":") {
if actual == "" {
@@ -295,22 +383,6 @@ func pathMatches(pattern, path string) bool {
return true
}
// Lookup accepts either a canonical Gin route template or an actual URL. A
// configured router prefix is tolerated by matching canonical paths by suffix.
func Lookup(method, path string) (Descriptor, bool) {
key := descriptorKey(method, path)
if value, ok := routes[key]; ok {
return descriptorFrom(key, value), true
}
for key, value := range routes {
descriptor := descriptorFrom(key, value)
if strings.EqualFold(descriptor.Method, method) && pathMatches(descriptor.Path, path) {
return descriptor, true
}
}
return Descriptor{}, false
}
func Describe(method, path string) Descriptor {
if descriptor, ok := Lookup(method, path); ok {
return descriptor
+9
View File
@@ -28,6 +28,15 @@ func TestRoutePoliciesShareOneDescriptor(t *testing.T) {
}
}
func TestLookupMatchesWildcardTail(t *testing.T) {
if descriptor, ok := Lookup("GET", "/api/swagger/index.html"); !ok || !descriptor.Public || descriptor.Path != "/swagger/*any" {
t.Fatalf("wildcard descriptor = %#v, ok=%v", descriptor, ok)
}
if _, ok := Lookup("GET", "/api/swagger"); ok {
t.Fatal("wildcard route matched without a tail")
}
}
func TestEveryRouteHasAPIMetadata(t *testing.T) {
for _, descriptor := range Descriptors() {
if descriptor.Group == "" || descriptor.Description == "" {