hi-server/initialize/schema_compat.go

185 lines
5.6 KiB
Go

package initialize
import (
"fmt"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type schemaTablePatch struct {
table string
ddl string
}
type schemaColumnPatch struct {
table string
column string
ddl string
}
func EnsureSchemaCompatibility(ctx *svc.ServiceContext) error {
tablePatches := []schemaTablePatch{
{
table: "log_message",
ddl: `CREATE TABLE IF NOT EXISTS log_message (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
platform VARCHAR(32) NOT NULL,
app_version VARCHAR(32) NULL,
os_name VARCHAR(32) NULL,
os_version VARCHAR(32) NULL,
device_id VARCHAR(64) NULL,
user_id BIGINT NULL DEFAULT NULL,
session_id VARCHAR(64) NULL,
level TINYINT UNSIGNED NOT NULL DEFAULT 3,
error_code VARCHAR(64) NULL,
message TEXT NOT NULL,
stack MEDIUMTEXT NULL,
context JSON NULL,
client_ip VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL,
locale VARCHAR(16) NULL,
digest VARCHAR(64) NULL,
occurred_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uniq_digest (digest),
KEY idx_platform_time (platform, created_at),
KEY idx_user_time (user_id, created_at),
KEY idx_device_time (device_id, created_at),
KEY idx_error_code (error_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`,
},
{
table: "user_family",
ddl: `CREATE TABLE IF NOT EXISTS user_family (
id BIGINT NOT NULL AUTO_INCREMENT,
owner_user_id BIGINT NOT NULL COMMENT 'Owner User ID',
max_members INT NOT NULL DEFAULT 5 COMMENT 'Max members in family',
status TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=active, 0=disabled',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
deleted_at DATETIME(3) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_owner_user_id (owner_user_id),
KEY idx_status (status),
KEY idx_deleted_at (deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`,
},
{
table: "user_family_member",
ddl: `CREATE TABLE IF NOT EXISTS user_family_member (
id BIGINT NOT NULL AUTO_INCREMENT,
family_id BIGINT NOT NULL COMMENT 'Family ID',
user_id BIGINT NOT NULL COMMENT 'Member User ID',
role TINYINT NOT NULL DEFAULT 2 COMMENT 'Role: 1=owner, 2=member',
status TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=active, 2=left, 3=removed',
join_source VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Join source',
joined_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
left_at DATETIME(3) DEFAULT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
deleted_at DATETIME(3) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uniq_user_id (user_id),
KEY idx_family_status (family_id, status),
KEY idx_deleted_at (deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`,
},
}
columnPatches := []schemaColumnPatch{
{
table: "user",
column: "rules",
ddl: "ALTER TABLE `user` ADD COLUMN `rules` TEXT NULL COMMENT 'User rules for subscription';",
},
{
table: "user",
column: "last_login_time",
ddl: "ALTER TABLE `user` ADD COLUMN `last_login_time` DATETIME DEFAULT NULL COMMENT 'Last Login Time';",
},
{
table: "user",
column: "member_status",
ddl: "ALTER TABLE `user` ADD COLUMN `member_status` VARCHAR(20) NOT NULL DEFAULT '' COMMENT 'Member Status';",
},
{
table: "user",
column: "remark",
ddl: "ALTER TABLE `user` ADD COLUMN `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Remark';",
},
{
table: "user_subscribe",
column: "note",
ddl: "ALTER TABLE `user_subscribe` ADD COLUMN `note` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'User note for subscription';",
},
{
table: "redemption_code",
column: "status",
ddl: "ALTER TABLE `redemption_code` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=enabled, 0=disabled';",
},
}
for _, patch := range tablePatches {
exists, err := tableExists(ctx.DB, patch.table)
if err != nil {
return errors.Wrapf(err, "check table %s failed", patch.table)
}
if exists {
continue
}
if err = ctx.DB.Exec(patch.ddl).Error; err != nil {
return errors.Wrapf(err, "create table %s failed", patch.table)
}
logger.Infof("[SchemaCompat] created missing table: %s", patch.table)
}
for _, patch := range columnPatches {
exists, err := columnExists(ctx.DB, patch.table, patch.column)
if err != nil {
return errors.Wrapf(err, "check column %s.%s failed", patch.table, patch.column)
}
if exists {
continue
}
if err = ctx.DB.Exec(patch.ddl).Error; err != nil {
return errors.Wrapf(err, "add column %s.%s failed", patch.table, patch.column)
}
logger.Infof("[SchemaCompat] added missing column: %s.%s", patch.table, patch.column)
}
return nil
}
func tableExists(db *gorm.DB, table string) (bool, error) {
var count int64
err := db.Raw("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", table).Scan(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func columnExists(db *gorm.DB, table, column string) (bool, error) {
var count int64
err := db.Raw(
"SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
table,
column,
).Scan(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func _schemaCompatDebug(table, column string) string {
if column == "" {
return table
}
return fmt.Sprintf("%s.%s", table, column)
}