feat: standardize REST and RPC routes

This commit is contained in:
2026-08-11 18:55:36 +08:00
parent 53e953c3fc
commit eed82bcf38
8 changed files with 89 additions and 22 deletions

View File

@@ -7,7 +7,7 @@ Server:
BindIP: 0.0.0.0 BindIP: 0.0.0.0
Port: 12001 Port: 12001
# Dynamic JSON-to-protobuf unary RPC endpoint: POST /rpc/{full.service}.{method} # Dynamic JSON-to-protobuf unary RPC endpoint: POST /rpc/{module}/{service}/{method}
# An empty allow list denies every dynamic call. Use "*" to expose all # An empty allow list denies every dynamic call. Use "*" to expose all
# reflection-visible unary methods, or list full service/method names. # reflection-visible unary methods, or list full service/method names.
DynamicRPC: DynamicRPC:

View File

@@ -70,12 +70,15 @@ func reflectionTarget(addr string) string {
func (g *dynamicGateway) Close() error { return g.conn.Close() } func (g *dynamicGateway) Close() error { return g.conn.Close() }
func (g *dynamicGateway) handle(c *gin.Context) { func (g *dynamicGateway) handle(c *gin.Context) {
fullMethod := strings.TrimSpace(c.Param("method")) moduleName := strings.TrimSpace(c.Param("module"))
serviceName, methodName, ok := splitFullMethod(fullMethod) serviceShortName := strings.TrimSpace(c.Param("service"))
if !ok { methodName := strings.TrimSpace(c.Param("method"))
writeDynamicError(c, status.Error(codes.InvalidArgument, "method must be {full.service}.{method}")) if moduleName == "" || serviceShortName == "" || methodName == "" {
writeDynamicError(c, status.Error(codes.InvalidArgument, "path must be /rpc/{module}/{service}/{method}"))
return return
} }
serviceName := moduleName + "." + serviceShortName
fullMethod := serviceName + "." + methodName
if !g.isAllowed(serviceName, fullMethod) { if !g.isAllowed(serviceName, fullMethod) {
writeDynamicError(c, status.Error(codes.PermissionDenied, "dynamic RPC method is not allowed")) writeDynamicError(c, status.Error(codes.PermissionDenied, "dynamic RPC method is not allowed"))
return return
@@ -122,14 +125,6 @@ func (g *dynamicGateway) handle(c *gin.Context) {
c.JSON(http.StatusOK, dynamicRPCResponse{Code: int32(codes.OK), Message: codes.OK.String(), Data: data}) c.JSON(http.StatusOK, dynamicRPCResponse{Code: int32(codes.OK), Message: codes.OK.String(), Data: data})
} }
func splitFullMethod(value string) (string, string, bool) {
separator := strings.LastIndexByte(value, '.')
if separator <= 0 || separator == len(value)-1 {
return "", "", false
}
return value[:separator], value[separator+1:], true
}
func (g *dynamicGateway) isAllowed(serviceName, fullMethod string) bool { func (g *dynamicGateway) isAllowed(serviceName, fullMethod string) bool {
for _, key := range []string{"*", serviceName, fullMethod} { for _, key := range []string{"*", serviceName, fullMethod} {
if _, ok := g.allow[key]; ok { if _, ok := g.allow[key]; ok {

View File

@@ -41,8 +41,8 @@ func TestDynamicGatewayInvokesUnaryRPC(t *testing.T) {
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
engine := gin.New() engine := gin.New()
engine.POST("/rpc/:method", gateway.handle) engine.POST("/rpc/:module/:service/:method", gateway.handle)
request := httptest.NewRequest(http.MethodPost, "/rpc/grpc.health.v1.Health.Check", strings.NewReader(`{"service":""}`)) request := httptest.NewRequest(http.MethodPost, "/rpc/grpc.health.v1/Health/Check", strings.NewReader(`{"service":""}`))
response := httptest.NewRecorder() response := httptest.NewRecorder()
engine.ServeHTTP(response, request) engine.ServeHTTP(response, request)
@@ -65,8 +65,8 @@ func TestDynamicGatewayDeniesMethodsByDefault(t *testing.T) {
gateway := &dynamicGateway{allow: map[string]struct{}{}} gateway := &dynamicGateway{allow: map[string]struct{}{}}
gin.SetMode(gin.TestMode) gin.SetMode(gin.TestMode)
engine := gin.New() engine := gin.New()
engine.POST("/rpc/:method", gateway.handle) engine.POST("/rpc/:module/:service/:method", gateway.handle)
request := httptest.NewRequest(http.MethodPost, "/rpc/grpc.health.v1.Health.Check", strings.NewReader(`{}`)) request := httptest.NewRequest(http.MethodPost, "/rpc/grpc.health.v1/Health/Check", strings.NewReader(`{}`))
response := httptest.NewRecorder() response := httptest.NewRecorder()
engine.ServeHTTP(response, request) engine.ServeHTTP(response, request)
if response.Code != http.StatusOK { if response.Code != http.StatusOK {

View File

@@ -52,7 +52,7 @@ func (s *Server) Start(grpcAddr, httpAddr string, allow []string) error {
_ = httpListener.Close() _ = httpListener.Close()
return err return err
} }
s.HTTP.POST("/rpc/:method", s.dynamic.handle) s.HTTP.POST("/rpc/:module/:service/:method", s.dynamic.handle)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
recorder := newBufferedResponse() recorder := newBufferedResponse()

View File

@@ -1,16 +1,17 @@
package routers package routers
import ( import (
"fmt" "path"
"bsm/full/module/base/fts/internal/logic" "bsm/full/module/base/fts/internal/logic"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files" swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger" ginSwagger "github.com/swaggo/gin-swagger"
) )
func Register(srvKey string, engine *gin.Engine) { func Register(srvKey string, engine *gin.Engine) {
v1_key := fmt.Sprintf("/%s/%s", srvKey, "v1") v1_key := path.Join("/rest", srvKey)
registerAnonymous(v1_key, engine) registerAnonymous(v1_key, engine)
registerUploader(v1_key, engine) registerUploader(v1_key, engine)
} }

View File

@@ -0,0 +1,34 @@
package routers
import (
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func TestRoutesUseRESTModulePrefix(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
Register("fts", engine)
expected := map[string]bool{
"GET /rest/fts/v1/ping": false,
"GET /rest/fts/v1/config": false,
"POST /rest/fts/v1/uploader": false,
}
for _, route := range engine.Routes() {
key := route.Method + " " + route.Path
if _, ok := expected[key]; ok {
expected[key] = true
}
if strings.HasPrefix(route.Path, "/fts/") {
t.Fatalf("legacy route remains registered: %s", route.Path)
}
}
for route, found := range expected {
if !found {
t.Errorf("route not registered: %s", route)
}
}
}

View File

@@ -1,6 +1,8 @@
package routers package routers
import ( import (
"path"
"bsm/full/module/base/mgt/internal/logic/application" "bsm/full/module/base/mgt/internal/logic/application"
"bsm/full/module/base/mgt/internal/logic/department" "bsm/full/module/base/mgt/internal/logic/department"
"bsm/full/module/base/mgt/internal/logic/hello" "bsm/full/module/base/mgt/internal/logic/hello"
@@ -14,9 +16,9 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// 完成请求地址: ip:port/srvKey/group/xxx // 完成请求地址: ip:port/rest/srvKey/group/xxx
func Register(srvKey string, engine *gin.Engine) { func Register(srvKey string, engine *gin.Engine) {
base := "/" + srvKey base := path.Join("/rest", srvKey)
registerAnonymous(base, engine) registerAnonymous(base, engine)
registerRouters(base, engine) registerRouters(base, engine)
} }

View File

@@ -0,0 +1,35 @@
package routers
import (
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func TestRoutesUseRESTModulePrefix(t *testing.T) {
gin.SetMode(gin.TestMode)
engine := gin.New()
Register("mgt", engine)
expected := map[string]bool{
"GET /rest/mgt/ping": false,
"POST /rest/mgt/login": false,
"POST /rest/mgt/user/create": false,
"POST /rest/mgt/app/fetch": false,
}
for _, route := range engine.Routes() {
key := route.Method + " " + route.Path
if _, ok := expected[key]; ok {
expected[key] = true
}
if route.Path == "/mgt" || strings.HasPrefix(route.Path, "/mgt/") {
t.Fatalf("legacy route remains registered: %s", route.Path)
}
}
for route, found := range expected {
if !found {
t.Errorf("route not registered: %s", route)
}
}
}