fix: harden platform runtime and menu navigation
This commit is contained in:
@@ -12,9 +12,11 @@ import (
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/initdb"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/platform"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/routers"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/seed"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const serviceKey = "heqi"
|
||||
@@ -38,6 +40,12 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("mock data written successfully")
|
||||
case "migrate":
|
||||
if err := migrateDatabase(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("database migrated successfully")
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||
printUsage()
|
||||
@@ -46,7 +54,7 @@ func main() {
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|mock-data>")
|
||||
fmt.Fprintln(os.Stderr, "usage: platform-cli <version|resource-contract|migrate|mock-data>")
|
||||
}
|
||||
|
||||
type route struct {
|
||||
@@ -110,3 +118,40 @@ func writeMockData() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateDatabase() error {
|
||||
config.New("platform")
|
||||
if config.Spec.Databases == nil {
|
||||
return fmt.Errorf("database configuration is required")
|
||||
}
|
||||
var migrationDatabase interface {
|
||||
Migrator() gorm.Migrator
|
||||
}
|
||||
var err error
|
||||
switch strings.ToLower(config.Spec.Databases.Driver) {
|
||||
case "postgres":
|
||||
migrationDatabase, err = database.NewPostgres(config.Spec.Databases.Source, nil)
|
||||
case "mysql":
|
||||
migrationDatabase, err = database.NewMysql(config.Spec.Databases.Source, nil)
|
||||
default:
|
||||
return fmt.Errorf("unsupported database driver: %s", config.Spec.Databases.Driver)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect database before migration: %w", err)
|
||||
}
|
||||
const legacyPhoneIndex = "idx_platform_account_phone"
|
||||
if migrationDatabase.Migrator().HasIndex(&models.PlatformAccount{}, legacyPhoneIndex) {
|
||||
if err := migrationDatabase.Migrator().DropIndex(&models.PlatformAccount{}, legacyPhoneIndex); err != nil {
|
||||
return fmt.Errorf("drop legacy platform account phone index: %w", err)
|
||||
}
|
||||
}
|
||||
databaseService, err := database.NewDatabase(
|
||||
config.Spec.Databases.Driver,
|
||||
config.Spec.Databases.Source,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate database: %w", err)
|
||||
}
|
||||
return initdb.New(databaseService)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
sdkmiddleware "git.apinb.com/bsm-sdk/core/middleware"
|
||||
"git.apinb.com/bsm-sdk/core/printer"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
appmiddleware "git.apinb.com/heqiapp/platforms/backend/api/internal/middleware"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/routers"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -31,8 +32,8 @@ func main() {
|
||||
// }
|
||||
|
||||
app := gin.Default()
|
||||
middleware.Mode(app)
|
||||
app.Use(middleware.Cors())
|
||||
sdkmiddleware.Mode(app)
|
||||
app.Use(appmiddleware.Cors())
|
||||
app.Use(gin.Recovery())
|
||||
app.HEAD("/", infra.Health)
|
||||
routers.Register(serviceKey, app)
|
||||
|
||||
@@ -3,6 +3,7 @@ package impl
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/cache/redis"
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"git.apinb.com/bsm-sdk/core/logger"
|
||||
"git.apinb.com/bsm-sdk/core/with"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
@@ -20,6 +21,11 @@ var (
|
||||
func NewImpl() {
|
||||
MemoryService = with.Memory(nil)
|
||||
RedisService = with.RedisCache(config.Spec.Cache)
|
||||
// HTTP 服务启动只建立连接。表结构迁移由 platform-cli migrate 显式执行,
|
||||
// 避免每次重启都对全部远程表执行耗时的元数据扫描。
|
||||
migrateTables := database.MigrateTables
|
||||
database.MigrateTables = nil
|
||||
defer func() { database.MigrateTables = migrateTables }()
|
||||
DBService = with.Databases(config.Spec.Databases, nil)
|
||||
logger.New(nil)
|
||||
}
|
||||
|
||||
27
backend/api/internal/middleware/cors.go
Normal file
27
backend/api/internal/middleware/cors.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Package middleware 定义平台 API 自有的 HTTP 中间件。
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
allowedHeaders = "Origin, Content-Length, Content-Type, Workspace, Request-Id, Authorization, Token"
|
||||
allowedMethods = "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"
|
||||
)
|
||||
|
||||
// Cors 允许平台前端跨域调用 API,包括状态更新使用的 PATCH 方法。
|
||||
func Cors() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
ctx.Header("Access-Control-Allow-Origin", "*")
|
||||
ctx.Header("Access-Control-Allow-Headers", allowedHeaders)
|
||||
ctx.Header("Access-Control-Allow-Methods", allowedMethods)
|
||||
if ctx.Request.Method == http.MethodOptions {
|
||||
ctx.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
}
|
||||
32
backend/api/internal/middleware/cors_test.go
Normal file
32
backend/api/internal/middleware/cors_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestCorsAllowsPatchPreflight(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
engine.Use(Cors())
|
||||
engine.PATCH("/resource/:identity/status", func(ctx *gin.Context) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
})
|
||||
|
||||
request := httptest.NewRequest(http.MethodOptions, "/resource/example/status", nil)
|
||||
request.Header.Set("Origin", "http://localhost:5173")
|
||||
request.Header.Set("Access-Control-Request-Method", http.MethodPatch)
|
||||
recorder := httptest.NewRecorder()
|
||||
engine.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusNoContent {
|
||||
t.Fatalf("preflight status = %d, want %d", recorder.Code, http.StatusNoContent)
|
||||
}
|
||||
if methods := recorder.Header().Get("Access-Control-Allow-Methods"); !strings.Contains(methods, http.MethodPatch) {
|
||||
t.Fatalf("allowed methods %q do not include PATCH", methods)
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import "git.apinb.com/bsm-sdk/core/database"
|
||||
// PlatformAccount 对应 platform_account,表示平台总后台登录账号。
|
||||
type PlatformAccount struct {
|
||||
Entity // 公共实体字段
|
||||
Username string `gorm:"column:username;type:varchar(64);uniqueIndex;not null" json:"username"` // 登录用户名
|
||||
DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` // 用户展示名称
|
||||
Avatar string `gorm:"column:avatar;type:varchar(512);not null;default:''" json:"avatar"` // 头像资源地址
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null;default:''" json:"-"` // 密码哈希值
|
||||
PlatformRoleCode string `gorm:"column:platform_role_code;type:varchar(64);not null;index" json:"platform_role_code"` // 平台角色编码
|
||||
Phone string `gorm:"column:phone;type:varchar(32);uniqueIndex;not null;default:''" json:"phone"` // 手机号
|
||||
Username string `gorm:"column:username;type:varchar(64);uniqueIndex;not null" json:"username"` // 登录用户名
|
||||
DisplayName string `gorm:"column:display_name;type:varchar(64);not null;default:''" json:"display_name"` // 用户展示名称
|
||||
Avatar string `gorm:"column:avatar;type:varchar(512);not null;default:''" json:"avatar"` // 头像资源地址
|
||||
PasswordHash string `gorm:"column:password_hash;type:varchar(255);not null;default:''" json:"-"` // 密码哈希值
|
||||
PlatformRoleCode string `gorm:"column:platform_role_code;type:varchar(64);not null;index" json:"platform_role_code"` // 平台角色编码
|
||||
Phone string `gorm:"column:phone;type:varchar(32);not null;default:'';uniqueIndex:idx_platform_account_phone_nonempty,where:phone <> ''" json:"phone"` // 手机号,非空值唯一
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&PlatformAccount{}) }
|
||||
|
||||
Reference in New Issue
Block a user