优化结构
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# Data Layer
|
||||
|
||||
`data` owns database clients, persistence models, migrations, configuration
|
||||
watching, and repository implementations.
|
||||
`data` owns database clients, persistence models, migrations, explicit runtime
|
||||
configuration reloads, and repository implementations. File watching and
|
||||
immutable configuration snapshots belong to `internal/config`.
|
||||
|
||||
- `system/`: system repositories and system table persistence
|
||||
- `task/`: timed-task tables and task persistence
|
||||
@@ -28,5 +29,5 @@ reload locks. Do not split them into packages only to reduce file count.
|
||||
- `payment` 只拥有 `pay_orders` 等支付持久化,通过 `biz/integration.PaymentConfigReader`
|
||||
读取支付配置,不感知 integration 的 PO 或表结构。
|
||||
|
||||
配置文件迁移、数据库切换、Redis/Mongo/对象存储重载仍属于根 data 的生命周期编排,
|
||||
配置文件持久化、数据库切换、Redis/Mongo/对象存储重载仍属于根 data 的生命周期编排,
|
||||
不等同于某个业务模块的表仓储。
|
||||
|
||||
@@ -123,7 +123,26 @@ func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig,
|
||||
for _, item := range DefaultIgnoredAPIs("uploads/file") {
|
||||
ignoredAPIs = append(ignoredAPIs, ignoredAPIPO{Method: item.Method, Path: item.Path})
|
||||
}
|
||||
// The local storage prefix is configurable. Add the concrete catch-all
|
||||
// routes supplied by the initialization request so API sync and policy
|
||||
// generation keep every static file endpoint public, regardless of the
|
||||
// configured prefix.
|
||||
for _, item := range input.APIs {
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
candidate := ignoredAPIPO{Method: strings.ToUpper(item.Method), Path: item.Path}
|
||||
if isStaticFileAPI(apiPO{Method: candidate.Method, Path: candidate.Path}) {
|
||||
ignoredAPIs = append(ignoredAPIs, candidate)
|
||||
}
|
||||
}
|
||||
seenIgnored := make(map[string]struct{}, len(ignoredAPIs))
|
||||
for _, ignored := range ignoredAPIs {
|
||||
key := ignored.Method + "\x00" + ignored.Path
|
||||
if _, exists := seenIgnored[key]; exists {
|
||||
continue
|
||||
}
|
||||
seenIgnored[key] = struct{}{}
|
||||
if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -137,7 +156,7 @@ func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig,
|
||||
return err
|
||||
}
|
||||
for _, api := range apiRows {
|
||||
if _, ignored := ignoreSet[api.Method+"\x00"+api.Path]; ignored {
|
||||
if _, ignored := ignoreSet[api.Method+"\x00"+api.Path]; ignored || isStaticFileAPI(api) {
|
||||
continue
|
||||
}
|
||||
exists, err := policyExists(tx, 888, api.Path, api.Method)
|
||||
@@ -155,6 +174,14 @@ func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig,
|
||||
})
|
||||
}
|
||||
|
||||
// isStaticFileAPI keeps dynamically configured local-storage routes public.
|
||||
// The route prefix is runtime configuration, so it cannot be hard-coded in
|
||||
// DefaultIgnoredAPIs; initialization already receives the concrete Gin route
|
||||
// list and can identify the catch-all file handlers directly.
|
||||
func isStaticFileAPI(api apiPO) bool {
|
||||
return (api.Method == "GET" || api.Method == "HEAD") && strings.HasSuffix(strings.TrimSpace(api.Path), "/*filepath")
|
||||
}
|
||||
|
||||
func seedAdminSurface(tx *gorm.DB, surface platformmodule.Surface) error {
|
||||
for _, item := range surface.Menus {
|
||||
parentID := uint(0)
|
||||
|
||||
@@ -20,7 +20,11 @@ func TestSeedSystemCreatesInitialDataAndModuleSurface(t *testing.T) {
|
||||
Menus: []platformmodule.Menu{{Name: "orders", Path: "orders", ParentName: "extensions", Component: "view/orders.vue", Title: "订单", Sort: 6}},
|
||||
APIs: []platformmodule.API{{Path: "/orders", Method: "GET", Group: "订单", Description: "订单列表"}},
|
||||
}
|
||||
input := &system.DatabaseConfig{AdminPassword: "admin-password", APIs: []*system.API{{Path: "/healthz", Method: "GET", APIGroup: "系统"}}}
|
||||
input := &system.DatabaseConfig{AdminPassword: "admin-password", APIs: []*system.API{
|
||||
{Path: "/healthz", Method: "GET", APIGroup: "系统"},
|
||||
{Path: "/files/*filepath", Method: "GET", APIGroup: "静态文件"},
|
||||
{Path: "/files/*filepath", Method: "HEAD", APIGroup: "静态文件"},
|
||||
}}
|
||||
if err = SeedSystem(context.Background(), db, input, surface); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -53,4 +57,19 @@ func TestSeedSystemCreatesInitialDataAndModuleSurface(t *testing.T) {
|
||||
if policies != 1 {
|
||||
t.Fatalf("root policies = %d, want 1", policies)
|
||||
}
|
||||
for _, method := range []string{"GET", "HEAD"} {
|
||||
var ignored int64
|
||||
if err = db.Model(&ignoredAPIPO{}).Where("path = ? AND method = ?", "/files/*filepath", method).Count(&ignored).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ignored != 1 {
|
||||
t.Fatalf("dynamic static route %s ignored rows = %d, want 1", method, ignored)
|
||||
}
|
||||
if err = policyScope(db).Where("v0 = ? AND v1 = ? AND v2 = ?", "888", "/files/*filepath", method).Count(&policies).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if policies != 0 {
|
||||
t.Fatalf("dynamic static route %s received root policy", method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,3 +28,14 @@ func TestDefaultIgnoredAPIsDoNotHideAnnouncementDataSource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticFileAPIsAreIgnoredRegardlessOfConfiguredPrefix(t *testing.T) {
|
||||
for _, method := range []string{"GET", "HEAD"} {
|
||||
if !isStaticFileAPI(apiPO{Method: method, Path: "/files/*filepath"}) {
|
||||
t.Fatalf("%s static route was not recognized", method)
|
||||
}
|
||||
}
|
||||
if isStaticFileAPI(apiPO{Method: "POST", Path: "/files/*filepath"}) {
|
||||
t.Fatal("non-read static route was incorrectly ignored")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user