fix(admin): standardize resource initialization and lists
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"git.apinb.com/bsm-sdk/core/types"
|
||||
"git.apinb.com/bsm-sdk/core/vars"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/initdb"
|
||||
deliverylogic "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/delivery"
|
||||
@@ -165,7 +167,7 @@ func writeMockData() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect database: %w", err)
|
||||
}
|
||||
if err := initdb.New(databaseService); err != nil {
|
||||
if err := initdb.New(); err != nil {
|
||||
return fmt.Errorf("initialize platform data: %w", err)
|
||||
}
|
||||
if err := seed.MockData(databaseService); err != nil {
|
||||
@@ -175,45 +177,24 @@ func writeMockData() error {
|
||||
}
|
||||
|
||||
func migrateDatabase() error {
|
||||
config.New("heqi")
|
||||
if config.Spec.Databases == nil {
|
||||
return fmt.Errorf("database configuration is required")
|
||||
config.New(serviceKey)
|
||||
options := &types.SqlOptions{
|
||||
MaxIdleConns: vars.SqlOptionMaxIdleConns,
|
||||
MaxOpenConns: vars.SqlOptionMaxOpenConns,
|
||||
ConnMaxLifetime: vars.SqlOptionConnMaxLifetime,
|
||||
IsAutoMigrate: true,
|
||||
LogStdout: false,
|
||||
Debug: true,
|
||||
}
|
||||
var migrationDatabase *gorm.DB
|
||||
var err error
|
||||
driver := strings.ToLower(config.Spec.Databases.Driver)
|
||||
switch 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)
|
||||
}
|
||||
if err := prepareAdditiveMigrations(migrationDatabase, driver); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rejectLegacyPaymentSchema(migrationDatabase); err != nil {
|
||||
return 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(
|
||||
_, err := database.NewDatabase(
|
||||
config.Spec.Databases.Driver,
|
||||
config.Spec.Databases.Source,
|
||||
nil,
|
||||
options,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate database: %w", err)
|
||||
}
|
||||
return initdb.New(databaseService)
|
||||
return initdb.New()
|
||||
}
|
||||
|
||||
// rejectLegacyPaymentSchema 阻止通用迁移物理删除历史资金表。
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"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"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/initdb"
|
||||
appmiddleware "git.apinb.com/heqiapp/platforms/backend/api/internal/middleware"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/routers"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -27,9 +28,9 @@ func main() {
|
||||
impl.NewImpl()
|
||||
|
||||
// 初始化先注释
|
||||
// if err := initdb.New(impl.DBService); err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
if err := initdb.New(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app := gin.Default()
|
||||
sdkmiddleware.Mode(app)
|
||||
|
||||
@@ -2,6 +2,8 @@ module git.apinb.com/heqiapp/platforms/backend/api
|
||||
|
||||
go 1.26.1
|
||||
|
||||
replace git.apinb.com/bsm-sdk/core => D:/work/bsm-sdk/core
|
||||
|
||||
require (
|
||||
git.apinb.com/bsm-sdk/core v0.2.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
|
||||
@@ -3,7 +3,6 @@ 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"
|
||||
@@ -22,10 +21,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// Package initdb 提供应用启动后的基础数据初始化。
|
||||
package initdb
|
||||
|
||||
import "gorm.io/gorm"
|
||||
// New 是初始化入口,按依赖顺序在同一事务中编排所有必需的幂等初始化任务。
|
||||
func New() error {
|
||||
if err := InitPlatformAccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// New 在同一事务中初始化平台基础数据。
|
||||
func New(database *gorm.DB) error {
|
||||
return database.Transaction(func(tx *gorm.DB) error {
|
||||
if err := InitPlatformAccess(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
return InitPlatformRoot(tx)
|
||||
})
|
||||
if err := InitPlatformRoot(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -11,16 +12,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// PlatformRootUsername 是平台总后台的内置根账号名称。
|
||||
PlatformRootUsername = "root"
|
||||
// PlatformRootPassword 是仅用于首次启动的初始密码,首次登录后必须修改。
|
||||
PlatformRootPassword = "Heqi@Root2026"
|
||||
// PlatformRootRoleCode 表示根账号的平台角色。
|
||||
PlatformRootRoleCode = "root"
|
||||
)
|
||||
|
||||
// InitPlatformAccess 幂等初始化 root 角色;菜单定义位于逻辑层静态数据中。
|
||||
func InitPlatformAccess(database *gorm.DB) error {
|
||||
// InitPlatformAccess 幂等初始化平台根角色;菜单定义位于逻辑层静态数据中。
|
||||
func InitPlatformAccess() error {
|
||||
rootRole := models.PlatformRole{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
|
||||
RoleCode: PlatformRootRoleCode,
|
||||
@@ -28,13 +26,13 @@ func InitPlatformAccess(database *gorm.DB) error {
|
||||
LocationScope: "precise",
|
||||
IsSystem: true,
|
||||
}
|
||||
return database.Where("role_code = ?", rootRole.RoleCode).FirstOrCreate(&rootRole).Error
|
||||
return impl.DBService.Where("role_code = ?", rootRole.RoleCode).FirstOrCreate(&rootRole).Error
|
||||
}
|
||||
|
||||
// InitPlatformRoot 幂等创建平台总后台 root 账号。
|
||||
func InitPlatformRoot(database *gorm.DB) error {
|
||||
// InitPlatformRoot 幂等创建平台总后台 root 账户。
|
||||
func InitPlatformRoot() error {
|
||||
var account models.PlatformAccount
|
||||
err := database.Where("username = ?", PlatformRootUsername).First(&account).Error
|
||||
err := impl.DBService.Where("username = ?", PlatformRootUsername).First(&account).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -55,7 +53,7 @@ func InitPlatformRoot(database *gorm.DB) error {
|
||||
PlatformRoleCode: PlatformRootRoleCode,
|
||||
Phone: "",
|
||||
}
|
||||
return database.Create(&account).Error
|
||||
return impl.DBService.Create(&account).Error
|
||||
}
|
||||
|
||||
// platformRootPassword 优先读取部署环境传入的 root 初始密码。
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package initdb
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestInitPlatformAccessSeedsRootRole(t *testing.T) {
|
||||
sqlDatabase, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sqlDatabase.Close() })
|
||||
database, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDatabase}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platform_role" WHERE role_code = $1 AND "platform_role"."deleted_at" IS NULL ORDER BY "platform_role"."id" LIMIT $2`)).
|
||||
WithArgs("root", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "status", "role_code", "name", "location_scope", "is_system"}).
|
||||
AddRow(uint64(1), "root-role", 1, "root", "Root", "precise", true))
|
||||
|
||||
if err := InitPlatformAccess(database); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformRootPassword(t *testing.T) {
|
||||
t.Run("accepts six character environment password", func(t *testing.T) {
|
||||
t.Setenv("HEQI_PLATFORM_ROOT_PASSWORD", "123456")
|
||||
if got := platformRootPassword(); got != "123456" {
|
||||
t.Fatalf("platformRootPassword() = %q, want environment password", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("falls back when environment password is too short", func(t *testing.T) {
|
||||
t.Setenv("HEQI_PLATFORM_ROOT_PASSWORD", "12345")
|
||||
if got := platformRootPassword(); got != PlatformRootPassword {
|
||||
t.Fatalf("platformRootPassword() = %q, want default password", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -242,6 +242,7 @@ func PrepareResourceValues(ctx *gin.Context, model any, allowedFields []string,
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil || len(input) == 0 {
|
||||
return nil, errors.New("invalid resource payload")
|
||||
}
|
||||
stripClientManagedCreateFields(input)
|
||||
values, err := ResolveResourceRelations(input, allowedFields, relations, true)
|
||||
if err != nil || len(values) == 0 {
|
||||
return nil, errors.New("invalid resource payload")
|
||||
@@ -249,6 +250,13 @@ func PrepareResourceValues(ctx *gin.Context, model any, allowedFields []string,
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// stripClientManagedCreateFields ensures database IDs and public identities are
|
||||
// always generated by the logic layer for newly created records.
|
||||
func stripClientManagedCreateFields(input map[string]any) {
|
||||
delete(input, "id")
|
||||
delete(input, "identity")
|
||||
}
|
||||
|
||||
// ValidateResourceValues enforces invariants that database nullability and
|
||||
// frontend form metadata cannot express.
|
||||
func ValidateResourceValues(model any, values map[string]any, creating bool) error {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -18,6 +19,32 @@ func TestFilterFieldsKeepsOnlyAllowedKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripClientManagedCreateFields(t *testing.T) {
|
||||
input := map[string]any{"id": float64(99), "identity": "client-value", "name": "气站"}
|
||||
stripClientManagedCreateFields(input)
|
||||
if _, exists := input["id"]; exists {
|
||||
t.Fatal("client supplied database ID was retained")
|
||||
}
|
||||
if _, exists := input["identity"]; exists {
|
||||
t.Fatal("client supplied identity was retained")
|
||||
}
|
||||
if input["name"] != "气站" {
|
||||
t.Fatalf("business fields changed: %#v", input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEntityGeneratesUUIDV7Identity(t *testing.T) {
|
||||
first := NewEntity(StatusDraft)
|
||||
second := NewEntity(StatusDraft)
|
||||
if first.Identity == second.Identity {
|
||||
t.Fatal("generated identities must be unique")
|
||||
}
|
||||
parsed, err := uuid.Parse(first.Identity)
|
||||
if err != nil || parsed.Version() != 7 {
|
||||
t.Fatalf("identity = %q, want UUID V7", first.Identity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationalQueriesExcludeArchivedRecords(t *testing.T) {
|
||||
sqlDatabase, _, err := sqlmock.New()
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user