同步历史版本代码

This commit is contained in:
2026-03-03 09:32:22 -08:00
parent 7d46b31866
commit 4d8516b2e1
140 changed files with 8399 additions and 489 deletions
@@ -116,7 +116,7 @@ VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-2
'2025-04-22 14:25:16.641'),
(37, 'currency', 'AccessKey', '', 'string', 'Exchangerate Access Key', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
(38, 'verify_code', 'VerifyCodeExpireTime', '300', 'int', 'Verify code expire time', '2025-04-22 14:25:16.641',
(38, 'verify_code', 'VerifyCodeExpireTime', '900', 'int', 'Verify code expire time', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
(39, 'verify_code', 'VerifyCodeLimit', '15', 'int', 'limits of verify code', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
@@ -1,2 +1,17 @@
-- Remove status column from redemption_code table
ALTER TABLE `redemption_code` DROP COLUMN `status`;
SET @dbname = DATABASE();
SET @tablename = 'redemption_code';
SET @colname = 'status';
SET @sql = (
SELECT IF(
COUNT(*) > 0,
'ALTER TABLE `redemption_code` DROP COLUMN `status`;',
'SELECT "Column `status` does not exist";'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = @tablename
AND COLUMN_NAME = @colname
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -1,2 +1,17 @@
-- Add status column to redemption_code table
ALTER TABLE `redemption_code` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=enabled, 0=disabled' AFTER `quantity`;
SET @dbname = DATABASE();
SET @tablename = 'redemption_code';
SET @colname = 'status';
SET @sql = (
SELECT IF(
COUNT(*) = 0,
'ALTER TABLE `redemption_code` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT ''Status: 1=enabled, 0=disabled'' AFTER `quantity`;',
'SELECT "Column `status` already exists";'
)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @dbname
AND TABLE_NAME = @tablename
AND COLUMN_NAME = @colname
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS `log_message`;
@@ -0,0 +1,27 @@
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;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS `apple_iap_transactions`;
@@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS `apple_iap_transactions` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT 'User ID',
`original_transaction_id` varchar(255) NOT NULL COMMENT 'Original Transaction ID',
`transaction_id` varchar(255) NOT NULL COMMENT 'Transaction ID',
`product_id` varchar(255) NOT NULL COMMENT 'Product ID',
`purchase_at` datetime NOT NULL COMMENT 'Purchase Time',
`revocation_at` datetime DEFAULT NULL COMMENT 'Revocation Time',
`jws_hash` varchar(255) NOT NULL COMMENT 'JWS Hash',
`created_at` datetime DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_original` (`original_transaction_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
@@ -0,0 +1 @@
ALTER TABLE user DROP COLUMN last_login_time;
@@ -0,0 +1 @@
ALTER TABLE user ADD COLUMN last_login_time DATETIME DEFAULT NULL COMMENT 'Last Login Time';
@@ -0,0 +1 @@
ALTER TABLE auth_method MODIFY config TEXT NOT NULL COMMENT 'Auth Configuration';
@@ -0,0 +1 @@
ALTER TABLE auth_method MODIFY config MEDIUMTEXT NOT NULL COMMENT 'Auth Configuration';
@@ -0,0 +1,2 @@
DROP TABLE IF EXISTS `user_family_member`;
DROP TABLE IF EXISTS `user_family`;
@@ -0,0 +1,31 @@
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;
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;
+184
View File
@@ -0,0 +1,184 @@
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)
}
+38 -19
View File
@@ -7,7 +7,6 @@ import (
"github.com/perfect-panel/server/pkg/logger"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/logic/telegram"
"github.com/perfect-panel/server/internal/model/auth"
"github.com/perfect-panel/server/internal/svc"
@@ -15,33 +14,49 @@ import (
)
func Telegram(svc *svc.ServiceContext) {
if !svc.Config.Telegram.Enable {
logger.Info("Telegram disabled, skipping initialization")
return
}
// Prefer BotToken from DB auth method, fallback to config file
var usedToken string
var webHookDomain string
method, err := svc.AuthModel.FindOneByMethod(context.Background(), "telegram")
if err != nil {
logger.Errorf("[Init Telegram Config] Get Telegram Config Error: %s", err.Error())
return
if err == nil {
tgConfig := new(auth.TelegramAuthConfig)
if err = tgConfig.Unmarshal(method.Config); err == nil {
usedToken = tgConfig.BotToken
webHookDomain = tgConfig.WebHookDomain
} else {
logger.Errorf("[Init Telegram Config] Unmarshal Telegram Config Error: %s", err.Error())
}
} else {
logger.Debugf("[Init Telegram Config] No Telegram method in DB, fallback to file config: %s", err.Error())
}
var tg config.Telegram
tgConfig := new(auth.TelegramAuthConfig)
if err = tgConfig.Unmarshal(method.Config); err != nil {
logger.Errorf("[Init Telegram Config] Unmarshal Telegram Config Error: %s", err.Error())
return
if usedToken == "" {
usedToken = svc.Config.Telegram.BotToken
}
if tgConfig.BotToken == "" {
if webHookDomain == "" {
webHookDomain = svc.Config.Telegram.WebHookDomain
}
if usedToken == "" {
logger.Debug("[Init Telegram Config] Telegram Token is empty")
return
}
bot, err := tgbotapi.NewBotAPI(tg.BotToken)
bot, err := tgbotapi.NewBotAPI(usedToken)
if err != nil {
logger.Error("[Init Telegram Config] New Bot API Error: ", logger.Field("error", err.Error()))
return
}
if tgConfig.WebHookDomain == "" || svc.Config.Debug {
// set Long Polling mode
if webHookDomain == "" || svc.Config.Debug {
// Ensure webhook is removed to avoid long polling conflict
if _, derr := bot.MakeRequest("deleteWebhook", tgbotapi.Params{}); derr != nil {
logger.Errorf("[Init Telegram Config] Delete webhook failed: %s", derr.Error())
}
// Long Polling mode
updateConfig := tgbotapi.NewUpdate(0)
updateConfig.Timeout = 60
updates := bot.GetUpdatesChan(updateConfig)
@@ -55,7 +70,7 @@ func Telegram(svc *svc.ServiceContext) {
}
}()
} else {
wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", tgConfig.WebHookDomain, tool.Md5Encode(tgConfig.BotToken, false)))
wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", webHookDomain, tool.Md5Encode(usedToken, false)))
if err != nil {
logger.Errorf("[Init Telegram Config] New Webhook Error: %s", err.Error())
return
@@ -74,9 +89,13 @@ func Telegram(svc *svc.ServiceContext) {
}
svc.Config.Telegram.BotID = user.ID
svc.Config.Telegram.BotName = user.UserName
svc.Config.Telegram.EnableNotify = tg.EnableNotify
svc.Config.Telegram.WebHookDomain = tg.WebHookDomain
svc.Config.Telegram.BotToken = usedToken
svc.Config.Telegram.WebHookDomain = webHookDomain
svc.TelegramBot = bot
logger.Info("[Init Telegram Config] Webhook set success")
if webHookDomain == "" || svc.Config.Debug {
logger.Info("[Init Telegram Config] Long polling mode initialized")
} else {
logger.Info("[Init Telegram Config] Webhook set success")
}
}
+30 -4
View File
@@ -4,6 +4,7 @@ import (
"errors"
"time"
gomigrate "github.com/golang-migrate/migrate/v4"
"github.com/perfect-panel/server/internal/model/user"
"gorm.io/gorm"
@@ -18,16 +19,41 @@ func Migrate(ctx *svc.ServiceContext) {
Config: ctx.Config.MySQL,
}
now := time.Now()
if err := migrate.Migrate(mc.Dsn()).Up(); err != nil {
migrator := migrate.Migrate(mc.Dsn())
if err := migrator.Up(); err != nil {
if errors.Is(err, migrate.NoChange) {
logger.Info("[Migrate] database not change")
return
} else {
var dirtyErr gomigrate.ErrDirty
if errors.As(err, &dirtyErr) {
logger.Errorf("[Migrate] Dirty database version %d detected, trying auto-recovery", dirtyErr.Version)
forceVersion := int(dirtyErr.Version) - 1
if forceVersion < 0 {
forceVersion = 0
}
if forceErr := migrator.Force(forceVersion); forceErr != nil {
logger.Errorf("[Migrate] Force version error: %v", forceErr.Error())
panic(forceErr)
}
logger.Infof("[Migrate] Force version to %d, retrying migration", forceVersion)
if retryErr := migrator.Up(); retryErr != nil && !errors.Is(retryErr, migrate.NoChange) {
logger.Errorf("[Migrate] Retry Up error: %v", retryErr.Error())
panic(retryErr)
}
} else {
logger.Errorf("[Migrate] Up error: %v", err.Error())
panic(err)
}
}
logger.Errorf("[Migrate] Up error: %v", err.Error())
panic(err)
} else {
logger.Info("[Migrate] Database change, took " + time.Since(now).String())
}
if err := EnsureSchemaCompatibility(ctx); err != nil {
logger.Errorf("[SchemaCompat] repair failed: %v", err.Error())
panic(err)
}
// if not found admin user
err := ctx.DB.Transaction(func(tx *gorm.DB) error {
var count int64