fix: harden platform runtime and menu navigation

This commit is contained in:
david
2026-07-29 18:38:01 +08:00
parent fa21507a68
commit 5af729326c
7 changed files with 131 additions and 12 deletions

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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)
}

View 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()
}
}

View 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)
}
}

View File

@@ -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{}) }