init: 1.0.0

This commit is contained in:
Chang lue Tsen
2025-04-25 12:08:29 +09:00
commit 8addcc584b
1031 changed files with 76472 additions and 0 deletions
+273
View File
@@ -0,0 +1,273 @@
package initialize
import (
"database/sql"
"embed"
"fmt"
"html/template"
"log"
"net/http"
"os"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"gorm.io/driver/mysql"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/perfect-panel/ppanel-server/initialize/migrate"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/pkg/conf"
"github.com/perfect-panel/ppanel-server/pkg/orm"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
)
//go:embed templates/*.html
var templateFS embed.FS
var initStatus = make(chan bool)
var configPath string
func Config(path string) (chan bool, *http.Server) {
// Set the configuration file path
configPath = path
// Create a new Gin instance
r := gin.Default()
// Create a new HTTP server
server := &http.Server{
Addr: ":8080",
Handler: r,
}
// Load templates
tmpl := template.Must(template.ParseFS(templateFS, "templates/*.html"))
r.SetHTMLTemplate(tmpl)
r.GET("/init", handleInit)
r.POST("/init/config", handleInitConfig)
r.POST("/init/mysql/test", HandleMySQLTest)
r.POST("/init/redis/test", HandleRedisTest)
// Handle 404
r.NoRoute(func(c *gin.Context) {
c.Redirect(http.StatusFound, "/init")
})
go func(server *http.Server) {
// Start the server
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %s\n", err)
}
}(server)
return initStatus, server
}
func handleInit(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
}
func handleInitConfig(c *gin.Context) {
// Load configuration file
var cfg config.File
conf.MustLoad(configPath, &cfg)
var request struct {
AdminEmail string `json:"adminEmail"`
AdminPassword string `json:"adminPassword"`
MysqlHost string `json:"mysqlHost"`
MysqlPort string `json:"mysqlPort"`
MysqlDatabase string `json:"mysqlDatabase"`
MysqlUser string `json:"mysqlUser"`
MysqlPassword string `json:"mysqlPassword"`
RedisHost string `json:"redisHost"`
RedisPort string `json:"redisPort"`
RedisPassword string `json:"redisPassword"`
}
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "Invalid request",
"data": nil,
})
c.Abort()
return
}
cfg.Debug = false
// jwt secret
cfg.JwtAuth.AccessSecret = uuid.New().String()
// mysql
cfg.MySQL.Addr = fmt.Sprintf("%s:%s", request.MysqlHost, request.MysqlPort)
cfg.MySQL.Dbname = request.MysqlDatabase
cfg.MySQL.Username = request.MysqlUser
cfg.MySQL.Password = request.MysqlPassword
// redis
cfg.Redis.Host = fmt.Sprintf("%s:%s", request.RedisHost, request.RedisPort)
cfg.Redis.Pass = request.RedisPassword
// save config
fileData, err := yaml.Marshal(cfg)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Configuration initialization failed",
"data": nil,
})
c.Abort()
return
}
// create mysql connection
db, err := orm.ConnectMysql(orm.Mysql{
Config: orm.Config{
Addr: fmt.Sprintf("%s:%s", request.MysqlHost, request.MysqlPort),
Username: request.MysqlUser,
Password: request.MysqlPassword,
Dbname: request.MysqlDatabase,
Config: "charset%3Dutf8mb4%26parseTime%3Dtrue%26loc%3DLocal",
MaxIdleConns: 10,
MaxOpenConns: 10,
SlowThreshold: 1000,
},
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "MySQL connection failed",
"data": nil,
})
c.Abort()
return
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", request.MysqlUser, request.MysqlPassword, request.MysqlHost, request.MysqlPort, request.MysqlDatabase)
// migrate database
if err = migrate.Migrate(dsn).Up(); err != nil {
logger.Errorf("[Init Mysql] Migrate failed: %v", err.Error())
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "Database migration failed",
"data": nil,
})
c.Abort()
return
}
// create admin user
if err = migrate.CreateAdminUser(request.AdminEmail, request.AdminPassword, db); err != nil {
logger.Errorf("[Init Mysql] Create admin user failed: %v", err.Error())
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "Admin user creation failed",
"data": nil,
})
c.Abort()
return
}
// write to file
if err = os.WriteFile(configPath, fileData, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "Configuration initialization failed",
"data": nil,
})
c.Abort()
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "Configuration initialized",
"status": true,
})
initStatus <- true
}
func HandleMySQLTest(c *gin.Context) {
var request struct {
Host string `json:"host"`
Port string `json:"port"`
Database string `json:"database"`
User string `json:"user"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "Invalid request",
"data": nil,
})
c.Abort()
return
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", request.User, request.Password, request.Host, request.Port, request.Database)
var status = true
var message string
var tx *sql.DB
var tables []string
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
logger.Errorf("connect mysql failed, err: %v\n", err.Error())
status = false
message = "MySQL connection failed"
goto result
}
tx, _ = db.DB()
if err := tx.Ping(); err != nil {
logger.Errorf("ping mysql failed, err: %v\n", err.Error())
status = false
message = "MySQL connection failed"
}
tables, err = db.Migrator().GetTables()
if err != nil {
logger.Errorf("database table check failed, err: %v\n", err.Error())
status = false
message = "Database table check failed"
goto result
}
if len(tables) > 0 {
status = false
message = "The database contains existing data. Please clear it before proceeding with the installation."
goto result
}
result:
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": message,
"status": status,
})
}
func HandleRedisTest(c *gin.Context) {
var request struct {
Host string `json:"host"`
Port string `json:"port"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "Invalid request",
"data": nil,
})
c.Abort()
return
}
if err := tool.RedisPing(fmt.Sprintf("%s:%s", request.Host, request.Port), request.Password, 0); err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": nil,
"status": false,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": nil,
"status": true,
})
}
+33
View File
@@ -0,0 +1,33 @@
package initialize
import (
"context"
"encoding/json"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/model/auth"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
// Email get email smtp config
func Email(ctx *svc.ServiceContext) {
logger.Debug("Email config initialization")
method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "email")
if err != nil {
panic(fmt.Sprintf("failed to find email auth method: %v", err.Error()))
}
var cfg config.EmailConfig
var emailConfig = new(auth.EmailAuthConfig)
if err := emailConfig.Unmarshal(method.Config); err != nil {
panic(fmt.Sprintf("failed to unmarshal email auth config: %v", err.Error()))
}
tool.DeepCopy(&cfg, emailConfig)
cfg.Enable = *method.Enabled
value, _ := json.Marshal(emailConfig.PlatformConfig)
cfg.PlatformConfig = string(value)
ctx.Config.Email = cfg
}
+22
View File
@@ -0,0 +1,22 @@
package initialize
import (
"github.com/perfect-panel/ppanel-server/internal/svc"
)
func StartInitSystemConfig(svc *svc.ServiceContext) {
Migrate(svc)
Site(svc)
Node(svc)
Email(svc)
Invite(svc)
Verify(svc)
Subscribe(svc)
Register(svc)
Mobile(svc)
TrafficDataToRedis(svc)
if !svc.Config.Debug {
Telegram(svc)
}
}
+23
View File
@@ -0,0 +1,23 @@
package initialize
import (
"context"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func Invite(ctx *svc.ServiceContext) {
// Initialize the system configuration
logger.Debug("Register config initialization")
configs, err := ctx.SystemModel.GetInviteConfig(context.Background())
if err != nil {
logger.Error("[Init Invite Config] Get Invite Config Error: ", logger.Field("error", err.Error()))
return
}
var inviteConfig config.InviteConfig
tool.SystemConfigSliceReflectToStruct(configs, &inviteConfig)
ctx.Config.Invite = inviteConfig
}
@@ -0,0 +1,36 @@
-- 000001_init_schema.down.sql
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `user_subscribe_log`;
DROP TABLE IF EXISTS `user_subscribe`;
DROP TABLE IF EXISTS `user_login_log`;
DROP TABLE IF EXISTS `user_gift_amount_log`;
DROP TABLE IF EXISTS `user_device`;
DROP TABLE IF EXISTS `user_commission_log`;
DROP TABLE IF EXISTS `user_balance_log`;
DROP TABLE IF EXISTS `user_auth_methods`;
DROP TABLE IF EXISTS `user`;
DROP TABLE IF EXISTS `traffic_log`;
DROP TABLE IF EXISTS `ticket_follow`;
DROP TABLE IF EXISTS `ticket`;
DROP TABLE IF EXISTS `system`;
DROP TABLE IF EXISTS `subscribe_type`;
DROP TABLE IF EXISTS `subscribe_group`;
DROP TABLE IF EXISTS `subscribe`;
DROP TABLE IF EXISTS `sms`;
DROP TABLE IF EXISTS `server_rule_group`;
DROP TABLE IF EXISTS `server_group`;
DROP TABLE IF EXISTS `server`;
DROP TABLE IF EXISTS `payment`;
DROP TABLE IF EXISTS `order`;
DROP TABLE IF EXISTS `message_log`;
DROP TABLE IF EXISTS `document`;
DROP TABLE IF EXISTS `coupon`;
DROP TABLE IF EXISTS `auth_method`;
DROP TABLE IF EXISTS `application_version`;
DROP TABLE IF EXISTS `application_config`;
DROP TABLE IF EXISTS `application`;
DROP TABLE IF EXISTS `announcement`;
DROP TABLE IF EXISTS `ads`;
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,559 @@
-- 000001_init_schema.up.sql
SET FOREIGN_KEY_CHECKS = 0;
CREATE TABLE IF NOT EXISTS `ads`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Ads title',
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Ads type',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Ads content',
`target_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Ads target url',
`start_time` datetime DEFAULT NULL COMMENT 'Ads start time',
`end_time` datetime DEFAULT NULL COMMENT 'Ads end time',
`status` tinyint(1) DEFAULT '0' COMMENT 'Ads status,0 disable,1 enable',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `announcement`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content',
`show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show',
`pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Pinned',
`popup` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Popup',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `application`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用名称',
`icon` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用图标',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '更新描述',
`subscribe_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `application_config`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`app_id` bigint NOT NULL DEFAULT '0' COMMENT 'App id',
`encryption_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Encryption Key',
`encryption_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Encryption Method',
`domains` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
`startup_picture` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
`startup_picture_skip_time` bigint NOT NULL DEFAULT '0' COMMENT 'Startup Picture Skip Time',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `application_version`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用地址',
`version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用版本',
`platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用平台',
`is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认版本',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '更新描述',
`application_id` bigint DEFAULT NULL COMMENT '所属应用',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `fk_application_application_versions` (`application_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `auth_method`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'method',
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OAuth Configuration',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_auth_method` (`method`)
) ENGINE = InnoDB
AUTO_INCREMENT = 9
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `coupon`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Name',
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Code',
`count` bigint NOT NULL DEFAULT '0' COMMENT 'Count Limit',
`type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Coupon Type: 1: Percentage 2: Fixed Amount',
`discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount',
`start_time` bigint NOT NULL DEFAULT '0' COMMENT 'Start Time',
`expire_time` bigint NOT NULL DEFAULT '0' COMMENT 'Expire Time',
`user_limit` bigint NOT NULL DEFAULT '0' COMMENT 'User Limit',
`subscribe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Limit',
`used_count` bigint NOT NULL DEFAULT '0' COMMENT 'Used Count',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enable',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_coupon_code` (`code`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `document`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Title',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Document Content',
`tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Tags',
`show` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Show',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `message_log`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type',
`platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform',
`to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To',
`subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `order`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`parent_id` bigint DEFAULT NULL COMMENT 'Parent Order Id',
`user_id` bigint NOT NULL DEFAULT '0' COMMENT 'User Id',
`order_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Order No',
`type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge',
`quantity` bigint NOT NULL DEFAULT '1' COMMENT 'Quantity',
`price` bigint NOT NULL DEFAULT '0' COMMENT 'Original price',
`amount` bigint NOT NULL DEFAULT '0' COMMENT 'Order Amount',
`gift_amount` bigint NOT NULL DEFAULT '0' COMMENT 'User Gift Amount',
`discount` bigint NOT NULL DEFAULT '0' COMMENT 'Discount Amount',
`coupon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Coupon',
`coupon_discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount Amount',
`commission` bigint NOT NULL DEFAULT '0' COMMENT 'Order Commission',
`payment_id` bigint NOT NULL DEFAULT '-1' COMMENT 'Payment Id',
`method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Method',
`fee_amount` bigint NOT NULL DEFAULT '0' COMMENT 'Fee Amount',
`trade_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Trade No',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished',
`subscribe_id` bigint NOT NULL DEFAULT '0' COMMENT 'Subscribe Id',
`subscribe_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Renewal Subscribe Token',
`is_new` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is New Order',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_order_order_no` (`order_no`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `payment`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Name',
`platform` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Platform',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Payment Description',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Payment Icon',
`domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Notification Domain',
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Configuration',
`fee_mode` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Fee Mode: 0: No Fee 1: Percentage 2: Fixed Amount 3: Percentage + Fixed Amount',
`fee_percent` bigint DEFAULT '0' COMMENT 'Fee Percentage',
`fee_amount` bigint DEFAULT '0' COMMENT 'Fixed Fee Amount',
`enable` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled',
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Payment Token',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_payment_token` (`token`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `server`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name',
`tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags',
`country` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country',
`city` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City',
`latitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude',
`longitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude',
`server_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address',
`relay_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode',
`relay_node` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Relay Node',
`speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit',
`traffic_ratio` decimal(4, 2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio',
`group_id` bigint DEFAULT NULL COMMENT 'Group ID',
`protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol',
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Config',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled',
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
`last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
KEY `idx_group_id` (`group_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `server_group`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Group Description',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
-- if `sms` not exist, create it
CREATE TABLE IF NOT EXISTS `sms`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
`platform` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`area_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`telephone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`status` tinyint(1) DEFAULT '1',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `subscribe`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Name',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Subscribe Description',
`unit_price` bigint NOT NULL DEFAULT '0' COMMENT 'Unit Price',
`unit_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Unit Time',
`discount` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Discount',
`replacement` bigint NOT NULL DEFAULT '0' COMMENT 'Replacement',
`inventory` bigint NOT NULL DEFAULT '0' COMMENT 'Inventory',
`traffic` bigint NOT NULL DEFAULT '0' COMMENT 'Traffic',
`speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit',
`device_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Device Limit',
`quota` bigint NOT NULL DEFAULT '0' COMMENT 'Quota',
`group_id` bigint DEFAULT NULL COMMENT 'Group Id',
`server_group` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server Group',
`server` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server',
`show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show portal page',
`sell` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Sell',
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
`deduction_ratio` bigint DEFAULT '0' COMMENT 'Deduction Ratio',
`allow_deduction` tinyint(1) DEFAULT '1' COMMENT 'Allow deduction',
`reset_cycle` bigint DEFAULT '0' COMMENT 'Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly',
`renewal_reset` tinyint(1) DEFAULT '0' COMMENT 'Renew Reset',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `subscribe_group`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Group Description',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `subscribe_type`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型',
`mark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅标识',
`created_at` datetime(3) DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 15
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `system`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Category',
`key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Key Name',
`value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Key Value',
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Type',
`desc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Description',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_system_key` (`key`),
KEY `index_key` (`key`)
) ENGINE = InnoDB
AUTO_INCREMENT = 42
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `ticket`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Description',
`user_id` bigint NOT NULL DEFAULT '0' COMMENT 'UserId',
`status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Status',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `ticket_follow`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`ticket_id` bigint NOT NULL DEFAULT '0' COMMENT 'TicketId',
`from` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'From',
`type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Type: 1 text, 2 image',
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `traffic_log`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`server_id` bigint NOT NULL COMMENT 'Server ID',
`user_id` bigint NOT NULL COMMENT 'User ID',
`subscribe_id` bigint NOT NULL COMMENT 'Subscription ID',
`download` bigint DEFAULT '0' COMMENT 'Download Traffic',
`upload` bigint DEFAULT '0' COMMENT 'Upload Traffic',
`timestamp` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Traffic Log Time',
PRIMARY KEY (`id`),
KEY `idx_subscribe_id` (`subscribe_id`),
KEY `idx_server_id` (`server_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'User Password',
`avatar` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'User Avatar',
`balance` bigint DEFAULT '0' COMMENT 'User Balance',
`telegram` bigint DEFAULT NULL COMMENT 'Telegram Account',
`refer_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Referral Code',
`referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID',
`commission` bigint DEFAULT '0' COMMENT 'Commission',
`gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled',
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin',
`valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified',
`enable_email_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Email Notifications',
`enable_telegram_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Telegram Notifications',
`enable_balance_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Balance Change Notifications',
`enable_login_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Login Notifications',
`enable_subscribe_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Subscription Notifications',
`enable_trade_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Trade Notifications',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
`deleted_at` datetime(3) DEFAULT NULL COMMENT 'Deletion Time',
`is_del` bigint unsigned DEFAULT NULL COMMENT '1: Normal 0: Deleted',
PRIMARY KEY (`id`),
KEY `idx_referer` (`referer_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 2
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_auth_methods`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`auth_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Type 1: apple 2: google 3: github 4: facebook 5: telegram 6: email 7: phone',
`auth_identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Identifier',
`verified` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Verified',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_auth_identifier` (`auth_identifier`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 2
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_balance_log`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`amount` bigint NOT NULL COMMENT 'Amount',
`type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward',
`order_id` bigint DEFAULT NULL COMMENT 'Order ID',
`balance` bigint NOT NULL COMMENT 'Balance',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_commission_log`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.',
`amount` bigint NOT NULL COMMENT 'Amount',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_device`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID',
`ip` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.',
`Identifier` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.',
`user_agent` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.',
`online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_gift_amount_log`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID',
`order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.',
`type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce',
`amount` bigint NOT NULL COMMENT 'Amount',
`balance` bigint NOT NULL COMMENT 'Balance',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_login_log`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP',
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent',
`success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_subscribe`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`order_id` bigint NOT NULL COMMENT 'Order ID',
`subscribe_id` bigint NOT NULL COMMENT 'Subscription ID',
`start_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Subscription Start Time',
`expire_time` datetime(3) DEFAULT NULL COMMENT 'Subscription Expire Time',
`traffic` bigint DEFAULT '0' COMMENT 'Traffic',
`download` bigint DEFAULT '0' COMMENT 'Download Traffic',
`upload` bigint DEFAULT '0' COMMENT 'Upload Traffic',
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Token',
`uuid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'UUID',
`status` tinyint(1) DEFAULT '0' COMMENT 'Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_user_subscribe_token` (`token`),
UNIQUE KEY `uni_user_subscribe_uuid` (`uuid`),
KEY `idx_user_id` (`user_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_subscribe_id` (`subscribe_id`),
KEY `idx_token` (`token`),
KEY `idx_uuid` (`uuid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `user_subscribe_log`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT 'User ID',
`user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID',
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP',
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_user_subscribe_id` (`user_subscribe_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
CREATE TABLE IF NOT EXISTS `server_rule_group`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name',
`icon` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Rule Group Description',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Rule Group Enable',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_name` (`name`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,21 @@
-- 000002_init_data.down.sql
SET
FOREIGN_KEY_CHECKS = 0;
DELETE
FROM `auth_method`
WHERE `id` IN (1, 2, 3, 4, 5, 6, 7, 8);
DELETE
FROM `payment`
WHERE `id` = -1;
DELETE
FROM `subscribe_type`
WHERE `id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
DELETE
FROM `system`
WHERE `id` IN
(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41);
SET
FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,127 @@
-- 000002_init_data.up.sql
SET FOREIGN_KEY_CHECKS = 0;
-- auth_method
INSERT IGNORE INTO `auth_method` (`id`, `method`, `config`, `enabled`, `created_at`, `updated_at`)
VALUES (1, 'email',
'{"platform":"smtp","platform_config":{"host":"","port":0,"user":"","pass":"","from":"","ssl":false},"enable_verify":false,"enable_notify":false,"enable_domain_suffix":false,"domain_suffix_list":"","verify_email_template":"","expiration_email_template":"","maintenance_email_template":"","traffic_exceed_email_template":""}',
1, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
(2, 'mobile',
'{"platform":"AlibabaCloud","platform_config":{"access":"","secret":"","sign_name":"","endpoint":"","template_code":""},"enable_whitelist":false,"whitelist":[]}',
0, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
(3, 'apple', '{"team_id":"","key_id":"","client_id":"","client_secret":"","redirect_url":""}', 0,
'2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'),
(4, 'google', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642',
'2025-04-22 14:25:16.642'),
(5, 'github', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642',
'2025-04-22 14:25:16.642'),
(6, 'facebook', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642',
'2025-04-22 14:25:16.642'),
(7, 'telegram', '{"bot_token":"","enable_notify":false,"webhook_domain":""}', 0, '2025-04-22 14:25:16.642',
'2025-04-22 14:25:16.642'),
(8, 'device', '{"show_ads":false,"only_real_device":false,"enable_security":false,"security_secret":""}', 0,
'2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642');
-- payment
INSERT IGNORE INTO `payment` (`id`, `name`, `platform`, `description`, `icon`, `domain`, `config`, `fee_mode`,
`fee_percent`, `fee_amount`, `enable`, `token`)
VALUES (-1, 'Balance', 'balance', '', '', '', '', 0, 0, 0, 1, '');
-- subscribe_type
INSERT IGNORE INTO `subscribe_type` (`id`, `name`, `mark`, `created_at`, `updated_at`)
VALUES (1, 'Clash', 'Clash', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(2, 'Hiddify', 'Hiddify', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(3, 'Loon', 'Loon', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(4, 'NekoBox', 'NekoBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(5, 'NekoRay', 'NekoRay', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(6, 'Netch', 'Netch', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(7, 'Quantumult', 'Quantumult', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(8, 'Shadowrocket', 'Shadowrocket', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(9, 'Singhandle', 'Singhandle', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(10, 'Surfboard', 'Surfboard', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(11, 'Surge', 'Surge', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(12, 'V2box', 'V2box', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(13, 'V2rayN', 'V2rayN', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'),
(14, 'V2rayNg', 'V2rayNg', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648');
-- system
INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-22 14:25:16.637',
'2025-04-22 14:25:16.637'),
(2, 'site', 'SiteName', 'Perfect Panel', 'string', 'Site Name', '2025-04-22 14:25:16.637',
'2025-04-22 14:25:16.637'),
(3, 'site', 'SiteDesc',
'PPanel is a pure, professional, and perfect open-source proxy panel tool, designed to be your ideal choice for learning and practical use.',
'string', 'Site Description', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
(4, 'site', 'Host', '', 'string', 'Site Host', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
(5, 'site', 'Keywords', 'Perfect Panel,PPanel', 'string', 'Site Keywords', '2025-04-22 14:25:16.637',
'2025-04-22 14:25:16.637'),
(6, 'site', 'CustomHTML', '', 'string', 'Custom HTML', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
(7, 'tos', 'TosContent', 'Welcome to use Perfect Panel', 'string', 'Terms of Service', '2025-04-22 14:25:16.637',
'2025-04-22 14:25:16.637'),
(8, 'tos', 'PrivacyPolicy', '', 'string', 'PrivacyPolicy', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'),
(9, 'ad', 'WebAD', 'false', 'bool', 'Display ad on the web', '2025-04-22 14:25:16.637',
'2025-04-22 14:25:16.637'),
(10, 'subscribe', 'SingleModel', 'false', 'bool', '是否单订阅模式', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(11, 'subscribe', 'SubscribePath', '/api/subscribe', 'string', '订阅路径', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(12, 'subscribe', 'SubscribeDomain', '', 'string', '订阅域名', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(13, 'subscribe', 'PanDomain', 'false', 'bool', '是否使用泛域名', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(14, 'verify', 'TurnstileSiteKey', '', 'string', 'TurnstileSiteKey', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(15, 'verify', 'TurnstileSecret', '', 'string', 'TurnstileSecret', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(16, 'verify', 'EnableLoginVerify', 'false', 'bool', 'is enable login verify', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(17, 'verify', 'EnableRegisterVerify', 'false', 'bool', 'is enable register verify', '2025-04-22 14:25:16.639',
'2025-04-22 14:25:16.639'),
(18, 'verify', 'EnableResetPasswordVerify', 'false', 'bool', 'is enable reset password verify',
'2025-04-22 14:25:16.639', '2025-04-22 14:25:16.639'),
(19, 'server', 'NodeSecret', '12345678', 'string', 'node secret', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(20, 'server', 'NodePullInterval', '10', 'int', 'node pull interval', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(21, 'server', 'NodePushInterval', '60', 'int', 'node push interval', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(22, 'server', 'NodeMultiplierConfig', '[]', 'string', 'node multiplier config', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(23, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(24, 'invite', 'ReferralPercentage', '20', 'int', 'Referral percentage', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(25, 'invite', 'OnlyFirstPurchase', 'false', 'bool', 'Only first purchase', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(26, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(27, 'register', 'EnableTrial', 'false', 'bool', 'is enable trial', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(28, 'register', 'TrialSubscribe', '', 'int', 'Trial subscription', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(29, 'register', 'TrialTime', '24', 'int', 'Trial time', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
(30, 'register', 'TrialTimeUnit', 'Hour', 'string', 'Trial time unit', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(31, 'register', 'EnableIpRegisterLimit', 'false', 'bool', 'is enable IP register limit',
'2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
(32, 'register', 'IpRegisterLimit', '3', 'int', 'IP Register Limit', '2025-04-22 14:25:16.640',
'2025-04-22 14:25:16.640'),
(33, 'register', 'IpRegisterLimitDuration', '64', 'int', 'IP Register Limit Duration (minutes)',
'2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'),
(34, 'currency', 'Currency', 'USD', 'string', 'Currency', '2025-04-22 14:25:16.641', '2025-04-22 14:25:16.641'),
(35, 'currency', 'CurrencySymbol', '$', 'string', 'Currency Symbol', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
(36, 'currency', 'CurrencyUnit', 'USD', 'string', 'Currency Unit', '2025-04-22 14:25:16.641',
'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',
'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'),
(40, 'verify_code', 'VerifyCodeInterval', '60', 'int', 'Interval of verify code', '2025-04-22 14:25:16.641',
'2025-04-22 14:25:16.641'),
(41, 'system', 'Version', '0.2.0(02002)', 'string', 'System Version', '2025-04-22 14:25:16.642',
'2025-04-22 14:25:16.642');
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,72 @@
-- migrations/02003_update_payment.down.sql
-- Purpose: Revert updates to payment and order tables
-- Author: PPanel Team, 2025-04-21
SET FOREIGN_KEY_CHECKS = 0;
-- Drop payment_id column from order table (if exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'order'
AND COLUMN_NAME = 'payment_id');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `order` DROP COLUMN `payment_id`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Drop platform column from payment table (if exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'platform');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `payment` DROP COLUMN `platform`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Drop description column from payment table (if exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'description');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `payment` DROP COLUMN `description`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Drop token column from payment table (if exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'token');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `payment` DROP COLUMN `token`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Optionally restore mark column (if needed, adjust definition as per original schema)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'mark');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `payment` ADD COLUMN `mark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT \'Payment Mark\' AFTER `name`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,72 @@
-- 2025-04-22 16:16:00
-- Purpose: Update payment table
-- Author: PPanel Team, 2025-04-21
SET FOREIGN_KEY_CHECKS = 0;
-- Alter the order table to add a payment_id column (if not exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'order'
AND COLUMN_NAME = 'payment_id');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `order` ADD COLUMN `payment_id` bigint NOT NULL DEFAULT \'-1\' COMMENT \'Payment Id\' AFTER `commission`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Alter the payment table to add a platform column (if not exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'platform');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `payment` ADD COLUMN `platform` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT \'Payment Platform\' AFTER `name`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Drop the mark column from the payment table (only if exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'mark');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `payment` DROP COLUMN `mark`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Alter the payment table to add a description column (if not exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'description');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `payment` ADD COLUMN `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT \'Payment Description\' AFTER `platform`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Alter the payment table to add a token column (if not exists)
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'payment'
AND COLUMN_NAME = 'token');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `payment` ADD COLUMN `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT \'Payment Token\' AFTER `description`',
'SELECT 1');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET FOREIGN_KEY_CHECKS = 1;
@@ -0,0 +1,4 @@
-- migrations/02003_rebuild_rule.up.sql
-- Purpose: Back rebuilding server rule table
-- Author: PPanel Team, 2025-04-21
DROP TABLE IF EXISTS server_rule_group;
@@ -0,0 +1,22 @@
-- migrations/02003_rebuild_rule.up.sql
-- Purpose: rebuilding server rule table
-- Author: PPanel Team, 2025-04-21
DROP TABLE IF EXISTS `server_rule_group`;
CREATE TABLE `server_rule_group`
(
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name',
`icon` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon',
`tags` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Selected Node Tags',
`rules` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rules',
`enable` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Rule Group Enable',
`created_at` DATETIME(3) COMMENT 'Creation Time',
`updated_at` DATETIME(3) COMMENT 'Update Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uni_server_rule_group_name` (`name`),
INDEX `idx_enable` (`enable`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
@@ -0,0 +1,52 @@
-- migrations/02004_create_user_device_online_record.down.sql
-- Purpose: Drop user device online record table
-- Author: PPanel Team, 2025-04-22
DROP TABLE IF EXISTS `user_device_online_record`;
-- User subscribe table migration for removing finished_at column
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'user_subscribe'
AND COLUMN_NAME = 'finished_at');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `user_subscribe` DROP COLUMN `finished_at`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Application config table migration for removing invitation_link column
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'application_config'
AND COLUMN_NAME = 'invitation_link');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `application_config` DROP COLUMN `invitation_link`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Application config table migration for removing kr_website_id column
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'application_config'
AND COLUMN_NAME = 'kr_website_id');
SET @sql = IF(@column_exists > 0,
'ALTER TABLE `application_config` DROP COLUMN `kr_website_id`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -0,0 +1,69 @@
-- migrations/02005_create_user_device_online_record.up.sql
-- Purpose: Create table for tracking user device online records
-- Author: PPanel Team, 2025-04-22
CREATE TABLE IF NOT EXISTS `user_device_online_record`
(
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT NOT NULL COMMENT 'User ID',
`identifier` VARCHAR(255) NOT NULL COMMENT 'Device Identifier',
`online_time` DATETIME COMMENT 'Online Time',
`offline_time` DATETIME COMMENT 'Offline Time',
`online_seconds` BIGINT COMMENT 'Offline Seconds',
`duration_days` BIGINT COMMENT 'Duration Days',
`created_at` DATETIME COMMENT 'Creation Time'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
-- User subscribe table migration for adding finished_at column
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'user_subscribe'
AND COLUMN_NAME = 'finished_at');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `user_subscribe` ADD COLUMN `finished_at` DATETIME NULL COMMENT ''Subscribe Finished Time'' AFTER `expire_time`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Application config table migration for adding Link column
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'application_config'
AND COLUMN_NAME = 'invitation_link');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `application_config` ADD COLUMN `invitation_link` TEXT NULL DEFAULT NULL COMMENT ''Invitation Link'' AFTER `startup_picture_skip_time`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Application config table migration for adding kr_website_id column
SET @column_exists = (SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'application_config'
AND COLUMN_NAME = 'kr_website_id');
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `application_config` ADD COLUMN `kr_website_id` VARCHAR(255) NULL DEFAULT NULL COMMENT ''KR Website ID'' AFTER `invitation_link`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -0,0 +1,5 @@
-- migrations/02008_create_user_reset_subscribe_log.down.sql
-- Purpose: Drop user_reset_subscribe_log table
-- Author: PPanel Team, 2025-04-22
DROP TABLE IF EXISTS `user_reset_subscribe_log`;
@@ -0,0 +1,17 @@
-- migrations/02008_create_user_reset_subscribe_log.up.sql
-- Purpose: Create user_reset_subscribe_log table
-- Author: PPanel Team, 2025-04-22
CREATE TABLE IF NOT EXISTS `user_reset_subscribe_log`
(
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT NOT NULL COMMENT 'User ID',
`type` TINYINT(1) NOT NULL COMMENT 'Type: 1: Auto 2: Advance 3: Paid',
`order_no` VARCHAR(255) DEFAULT NULL COMMENT 'Order No.',
`user_subscribe_id` BIGINT NOT NULL COMMENT 'User Subscribe ID',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
INDEX `idx_user_id` (`user_id`),
INDEX `idx_user_subscribe_id` (`user_subscribe_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;
+42
View File
@@ -0,0 +1,42 @@
package migrate
import (
"time"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/uuidx"
"gorm.io/gorm"
)
// CreateAdminUser create admin user
func CreateAdminUser(email, password string, tx *gorm.DB) error {
enable := true
return tx.Transaction(func(tx *gorm.DB) error {
// Prevent duplicate creation
if tx.Model(&user.User{}).Find(&user.User{}).RowsAffected != 0 {
logger.Info("User already exists, skip creating administrator account")
return nil
}
u := user.User{
Password: tool.EncodePassWord(password),
IsAdmin: &enable,
ReferCode: uuidx.UserInviteCode(time.Now().Unix()),
}
if err := tx.Model(&user.User{}).Save(&u).Error; err != nil {
return err
}
method := user.AuthMethods{
UserId: u.Id,
AuthType: "email",
AuthIdentifier: email,
Verified: true,
}
if err := tx.Model(&user.AuthMethods{}).Save(&method).Error; err != nil {
return err
}
return nil
})
}
+1
View File
@@ -0,0 +1 @@
package migrate
+29
View File
@@ -0,0 +1,29 @@
package migrate
import (
"embed"
"fmt"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/mysql"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/perfect-panel/ppanel-server/pkg/logger"
)
//go:embed database/*.sql
var sqlFiles embed.FS
var NoChange = migrate.ErrNoChange
func Migrate(dsn string) *migrate.Migrate {
d, err := iofs.New(sqlFiles, "database")
if err != nil {
logger.Errorf("[Migrate] iofs.New error: %v", err.Error())
panic(err)
}
client, err := migrate.NewWithSourceInstance("iofs", d, fmt.Sprintf("mysql://%s", dsn))
if err != nil {
logger.Errorf("[Migrate] NewWithSourceInstance error: %v", err.Error())
panic(err)
}
return client
}
+32
View File
@@ -0,0 +1,32 @@
package migrate
import (
"testing"
"github.com/perfect-panel/ppanel-server/pkg/orm"
)
func getDSN() string {
cfg := orm.Config{
Addr: "127.0.0.1",
Username: "root",
Password: "mylove520",
Dbname: "vpnboard",
}
mc := orm.Mysql{
Config: cfg,
}
return mc.Dsn()
}
func TestMigrate(t *testing.T) {
t.Skipf("skip test")
m := Migrate(getDSN())
err := m.Migrate(2004)
if err != nil {
t.Errorf("failed to migrate: %v", err)
} else {
t.Log("migrate success")
}
}
+101
View File
@@ -0,0 +1,101 @@
package migrate
import (
"fmt"
"strings"
"gorm.io/gorm"
)
// ExecuteSQLFile 执行嵌入的 SQL 文件,去除注释
func ExecuteSQLFile(tx *gorm.DB, path string) error {
// 读取 SQL 文件内容
sqlContent, err := sqlFiles.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read embedded SQL file: %v", err)
}
// 清除注释内容
cleanedSQL := removeComments(string(sqlContent))
// 将清除注释后的内容按分号分割成多个 SQL 语句
sqlStatements := splitSQLStatements(cleanedSQL)
// 遍历 SQL 语句并执行
for _, stmt := range sqlStatements {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
// 执行 SQL 语句
if err := tx.Exec(stmt).Error; err != nil {
return fmt.Errorf("failed to execute SQL statement: %v \nSQL: %s", err.Error(), stmt)
}
}
return nil
}
// removeComments 去除 SQL 代码中的注释
func removeComments(sql string) string {
var result strings.Builder
inSingleLineComment := false
inMultiLineComment := false
length := len(sql)
for i := 0; i < length; i++ {
// 处理 -- 单行注释
if !inMultiLineComment && !inSingleLineComment && i+1 < length && sql[i] == '-' && sql[i+1] == '-' {
inSingleLineComment = true
i++ // 跳过 '-'
continue
}
// 结束单行注释(支持 \r\n 和 \n)
if inSingleLineComment && (sql[i] == '\n' || sql[i] == '\r') {
inSingleLineComment = false
}
// 处理 /* 多行注释 */
if !inSingleLineComment && !inMultiLineComment && i+1 < length && sql[i] == '/' && sql[i+1] == '*' {
inMultiLineComment = true
i++ // 跳过 '*'
continue
}
// 结束多行注释
if inMultiLineComment && i+1 < length && sql[i] == '*' && sql[i+1] == '/' {
inMultiLineComment = false
i++ // 跳过 '/'
continue
}
// 不是注释内容时,将字符添加到结果中
if !inSingleLineComment && !inMultiLineComment {
result.WriteByte(sql[i])
}
}
// 去除多余的空白行
lines := strings.Split(result.String(), "\n")
var cleanedLines []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
cleanedLines = append(cleanedLines, trimmed)
}
}
return strings.Join(cleanedLines, "\n")
}
// splitSQLStatements 更安全地分割 SQL 语句
func splitSQLStatements(sql string) []string {
statements := strings.Split(sql, ";")
var results []string
for _, stmt := range statements {
trimmed := strings.TrimSpace(stmt)
if trimmed != "" {
results = append(results, trimmed)
}
}
return results
}
+32
View File
@@ -0,0 +1,32 @@
package initialize
import (
"context"
"encoding/json"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/model/auth"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func Mobile(ctx *svc.ServiceContext) {
logger.Debug("Mobile config initialization")
method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "mobile")
if err != nil {
panic(err)
}
var cfg config.MobileConfig
var mobileConfig auth.MobileAuthConfig
if err := mobileConfig.Unmarshal(method.Config); err != nil {
panic(fmt.Sprintf("failed to unmarshal mobile auth config: %v", err.Error()))
}
tool.DeepCopy(&cfg, mobileConfig)
cfg.Enable = *method.Enabled
value, _ := json.Marshal(mobileConfig.PlatformConfig)
cfg.PlatformConfig = string(value)
ctx.Config.Mobile = cfg
}
+1
View File
@@ -0,0 +1 @@
package initialize
+51
View File
@@ -0,0 +1,51 @@
package initialize
import (
"context"
"encoding/json"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/model/system"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/nodeMultiplier"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func Node(ctx *svc.ServiceContext) {
logger.Debug("Node config initialization")
configs, err := ctx.SystemModel.GetNodeConfig(context.Background())
if err != nil {
panic(err)
}
var nodeConfig config.NodeConfig
tool.SystemConfigSliceReflectToStruct(configs, &nodeConfig)
ctx.Config.Node = nodeConfig
// Manager initialization
if ctx.DB.Model(&system.System{}).Where("`key` = ?", "NodeMultiplierConfig").Find(&system.System{}).RowsAffected == 0 {
if err := ctx.DB.Model(&system.System{}).Create(&system.System{
Key: "NodeMultiplierConfig",
Value: "[]",
Type: "string",
Desc: "Node Multiplier Config",
Category: "server",
}).Error; err != nil {
logger.Errorf("Create Node Multiplier Config Error: %s", err.Error())
}
return
}
nodeMultiplierData, err := ctx.SystemModel.FindNodeMultiplierConfig(context.Background())
if err != nil {
logger.Error("Get Node Multiplier Config Error: ", logger.Field("error", err.Error()))
return
}
var periods []nodeMultiplier.TimePeriod
if err := json.Unmarshal([]byte(nodeMultiplierData.Value), &periods); err != nil {
logger.Error("Unmarshal Node Multiplier Config Error: ", logger.Field("error", err.Error()), logger.Field("value", nodeMultiplierData.Value))
}
ctx.NodeMultiplierManager = nodeMultiplier.NewNodeMultiplierManager(periods)
}
+11
View File
@@ -0,0 +1,11 @@
package initialize
import (
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/logger"
)
func OAuth(svc *svc.ServiceContext) {
logger.Debug("OAuth config initialization")
}
+23
View File
@@ -0,0 +1,23 @@
package initialize
import (
"context"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func Register(ctx *svc.ServiceContext) {
logger.Debug("Register config initialization")
configs, err := ctx.SystemModel.GetRegisterConfig(context.Background())
if err != nil {
logger.Errorf("[Init Register Config] Get Register Config Error: %s", err.Error())
return
}
var registerConfig config.RegisterConfig
tool.SystemConfigSliceReflectToStruct(configs, &registerConfig)
ctx.Config.Register = registerConfig
}
+22
View File
@@ -0,0 +1,22 @@
package initialize
import (
"context"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func Site(ctx *svc.ServiceContext) {
logger.Debug("initialize site config")
configs, err := ctx.SystemModel.GetSiteConfig(context.Background())
if err != nil {
panic(err)
}
var siteConfig config.SiteConfig
tool.SystemConfigSliceReflectToStruct(configs, &siteConfig)
ctx.Config.Site = siteConfig
}
+57
View File
@@ -0,0 +1,57 @@
package initialize
import (
"context"
"time"
"github.com/perfect-panel/ppanel-server/internal/model/cache"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/logger"
)
func TrafficDataToRedis(svcCtx *svc.ServiceContext) {
ctx := context.Background()
// 统计昨天的节点流量数据排行榜前10
nodeData, err := svcCtx.TrafficLogModel.TopServersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10)
if err != nil {
logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error()))
}
var nodeCacheData []cache.NodeTodayTrafficRank
for _, node := range nodeData {
serverInfo, err := svcCtx.ServerModel.FindOne(ctx, node.ServerId)
if err != nil {
logger.Errorw("查询节点信息失败", logger.Field("error", err.Error()))
continue
}
nodeCacheData = append(nodeCacheData, cache.NodeTodayTrafficRank{
ID: node.ServerId,
Name: serverInfo.Name,
Upload: node.Upload,
Download: node.Download,
Total: node.Upload + node.Download,
})
}
// 写入缓存
if err = svcCtx.NodeCache.UpdateYesterdayNodeTotalTrafficRank(ctx, nodeCacheData); err != nil {
logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error()))
}
// 统计昨天的用户流量数据排行榜前10
userData, err := svcCtx.TrafficLogModel.TopUsersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10)
if err != nil {
logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error()))
}
var userCacheData []cache.UserTodayTrafficRank
for _, user := range userData {
userCacheData = append(userCacheData, cache.UserTodayTrafficRank{
SID: user.SubscribeId,
Upload: user.Upload,
Download: user.Download,
Total: user.Upload + user.Download,
})
}
// 写入缓存
if err = svcCtx.NodeCache.UpdateYesterdayUserTotalTrafficRank(ctx, userCacheData); err != nil {
logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error()))
}
logger.Infow("初始化昨天的流量数据到缓存成功")
}
+24
View File
@@ -0,0 +1,24 @@
package initialize
import (
"context"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func Subscribe(svc *svc.ServiceContext) {
logger.Debug("Subscribe config initialization")
configs, err := svc.SystemModel.GetSubscribeConfig(context.Background())
if err != nil {
logger.Error("[Init Subscribe Config] Get Subscribe Config Error: ", logger.Field("error", err.Error()))
return
}
var subscribeConfig config.SubscribeConfig
tool.SystemConfigSliceReflectToStruct(configs, &subscribeConfig)
svc.Config.Subscribe = subscribeConfig
}
+82
View File
@@ -0,0 +1,82 @@
package initialize
import (
"context"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/logger"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/logic/telegram"
"github.com/perfect-panel/ppanel-server/internal/model/auth"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
func Telegram(svc *svc.ServiceContext) {
method, err := svc.AuthModel.FindOneByMethod(context.Background(), "telegram")
if err != nil {
logger.Errorf("[Init Telegram Config] Get Telegram Config Error: %s", err.Error())
return
}
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 tgConfig.BotToken == "" {
logger.Debug("[Init Telegram Config] Telegram Token is empty")
return
}
bot, err := tgbotapi.NewBotAPI(tg.BotToken)
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
updateConfig := tgbotapi.NewUpdate(0)
updateConfig.Timeout = 60
updates := bot.GetUpdatesChan(updateConfig)
go func() {
for update := range updates {
if update.Message != nil {
ctx := context.Background()
l := telegram.NewTelegramLogic(ctx, svc)
l.TelegramLogic(&update)
}
}
}()
} else {
wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", tgConfig.WebHookDomain, tool.Md5Encode(tgConfig.BotToken, false)))
if err != nil {
logger.Errorf("[Init Telegram Config] New Webhook Error: %s", err.Error())
return
}
_, err = bot.Request(wh)
if err != nil {
logger.Errorf("[Init Telegram Config] Request Webhook Error: %s", err.Error())
return
}
}
user, err := bot.GetMe()
if err != nil {
logger.Error("[Init Telegram Config] Get Bot Info Error: ", logger.Field("error", err.Error()))
return
}
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.TelegramBot = bot
logger.Info("[Init Telegram Config] Webhook set success")
}
+469
View File
@@ -0,0 +1,469 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 头部内容 -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PPanel - Application Initialization</title>
<!-- 引入 Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Tailwind CSS 配置 -->
<script>
tailwind.config = {
darkMode: ["class"],
theme: {
container: { center: true, padding: "2rem" },
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))" },
},
borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)" },
},
},
}
</script>
<!-- 自定义样式 -->
<style>
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--destructive: 346.8 77.2% 49.8%;
--destructive-foreground: 355.7 100% 97.3%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
</style>
</head>
<body class="min-h-screen font-sans antialiased">
<div id="appContainer" class="container mx-auto p-4 flex flex-col items-center min-h-screen">
<div class="w-full max-w-2xl space-y-6">
<!-- 设置卡片 -->
<div id="setupCard" class="p-6 space-y-6 bg-white rounded-lg border">
<div class="flex justify-between items-center">
<h1 id="title" class="text-3xl font-bold">Welcome to PPanel Setup</h1>
<button onclick="switchLanguage()" id="langSwitch" class="h-10 px-4 py-2 border rounded-md text-sm font-medium bg-background hover:bg-accent hover:text-accent-foreground">中文</button>
</div>
<p id="description" class="text-gray-600 !mt-0">Let's get your PPanel application up and running. Please provide the necessary information below.</p>
<form id="setupForm" class="space-y-6" onsubmit="handleSubmit(event)">
<!-- 管理员详情 -->
<div class="space-y-4">
<h2 id="adminInfoTitle" class="font-bold text-xl">Administrator Details</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="adminEmail" id="labelAdminEmail" class="text-sm font-medium">Administrator Email</label>
<input type="email" id="adminEmail" name="adminEmail" required class="input-field mt-1 block w-full rounded-md border px-3 py-2" placeholder="Enter administrator email" data-placeholder-en="Enter administrator email" data-placeholder-zh="请输入管理员邮箱" oninput="validateInput(this)">
<span id="adminEmailError" class="text-red-500 text-sm mt-1 hidden">Invalid email address.</span>
</div>
<div>
<label for="adminPassword" id="labelAdminPassword" class="text-sm font-medium">Administrator Password</label>
<input type="password" id="adminPassword" name="adminPassword" required minlength="6" class="input-field mt-1 block w-full rounded-md border px-3 py-2" placeholder="Enter administrator password" data-placeholder-en="Enter administrator password" data-placeholder-zh="请输入管理员密码" oninput="validateInput(this)">
<span id="adminPasswordError" class="text-red-500 text-sm mt-1 hidden">Password must be at least 6 characters.</span>
</div>
</div>
</div>
<!-- MySQL 数据库设置 -->
<div class="space-y-4">
<div class="flex justify-between items-center">
<h2 id="mysqlInfoTitle" class="font-bold text-xl">MySQL Database Setup</h2>
<button type="button" onclick="testConnection('mysql')" id="testMySQLButton" class="h-10 px-4 py-2 border rounded-md text-sm font-medium bg-background hover:bg-accent hover:text-accent-foreground">Test MySQL Connection</button>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="mysqlHost" id="labelMysqlHost" class="text-sm font-medium">Database Host</label>
<input type="text" id="mysqlHost" name="mysqlHost" required class="input-field mt-1 block w-full rounded-md border px-3 py-2" value="localhost" placeholder="Enter database host" data-placeholder-en="Enter database host" data-placeholder-zh="请输入数据库主机">
<span id="mysqlHostError" class="text-red-500 text-sm mt-1 hidden">Please enter a valid host.</span>
</div>
<div>
<label for="mysqlPort" id="labelMysqlPort" class="text-sm font-medium">Database Port</label>
<input type="number" id="mysqlPort" name="mysqlPort" required min="1" max="65535" class="input-field mt-1 block w-full rounded-md border px-3 py-2" value="3306" placeholder="Enter database port" data-placeholder-en="Enter database port" data-placeholder-zh="请输入数据库端口">
<span id="mysqlPortError" class="text-red-500 text-sm mt-1 hidden">Please enter a valid port number.</span>
</div>
<div>
<label for="mysqlUser" id="labelMysqlUser" class="text-sm font-medium">Database User</label>
<input type="text" id="mysqlUser" name="mysqlUser" required class="input-field mt-1 block w-full rounded-md border px-3 py-2" value="root" placeholder="Enter database user" data-placeholder-en="Enter database user" data-placeholder-zh="请输入数据库用户名">
<span id="mysqlUserError" class="text-red-500 text-sm mt-1 hidden">Please enter a username.</span>
</div>
<div>
<label for="mysqlPassword" id="labelMysqlPassword" class="text-sm font-medium">Database Password</label>
<input type="password" id="mysqlPassword" name="mysqlPassword" class="input-field mt-1 block w-full rounded-md border px-3 py-2" placeholder="Enter database password" data-placeholder-en="Enter database password" data-placeholder-zh="请输入数据库密码">
</div>
<div>
<label for="mysqlDatabase" id="labelMysqlDatabase" class="text-sm font-medium">Database Name</label>
<input type="text" id="mysqlDatabase" name="mysqlDatabase" required class="input-field mt-1 block w-full rounded-md border px-3 py-2" value="ppanel_db" placeholder="Enter database name" data-placeholder-en="Enter database name" data-placeholder-zh="请输入数据库名称">
<span id="mysqlDatabaseError" class="text-red-500 text-sm mt-1 hidden">Please enter a database name.</span>
</div>
</div>
</div>
<!-- Redis 缓存设置 -->
<div class="space-y-4">
<div class="flex justify-between items-center">
<h2 id="redisInfoTitle" class="font-bold text-xl">Redis Cache Setup</h2>
<button type="button" onclick="testConnection('redis')" id="testRedisButton" class="h-10 px-4 py-2 border rounded-md text-sm font-medium bg-background hover:bg-accent hover:text-accent-foreground">Test Redis Connection</button>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="redisHost" id="labelRedisHost" class="text-sm font-medium">Redis Host</label>
<input type="text" id="redisHost" name="redisHost" required class="input-field mt-1 block w-full rounded-md border px-3 py-2" value="127.0.0.1" placeholder="Enter Redis host" data-placeholder-en="Enter Redis host" data-placeholder-zh="请输入 Redis 主机">
<span id="redisHostError" class="text-red-500 text-sm mt-1 hidden">Please enter a valid host.</span>
</div>
<div>
<label for="redisPort" id="labelRedisPort" class="text-sm font-medium">Redis Port</label>
<input type="number" id="redisPort" name="redisPort" required min="1" max="65535" class="input-field mt-1 block w-full rounded-md border px-3 py-2" value="6379" placeholder="Enter Redis port" data-placeholder-en="Enter Redis port" data-placeholder-zh="请输入 Redis 端口">
<span id="redisPortError" class="text-red-500 text-sm mt-1 hidden">Please enter a valid port number.</span>
</div>
<div>
<label for="redisPassword" id="labelRedisPassword" class="text-sm font-medium">Redis Password</label>
<input type="password" id="redisPassword" name="redisPassword" class="input-field mt-1 block w-full rounded-md border px-3 py-2" placeholder="Enter Redis password (optional)" data-placeholder-en="Enter Redis password (optional)" data-placeholder-zh="请输入 Redis 密码(可选)">
</div>
</div>
</div>
<!-- 提交按钮 -->
<button type="submit" id="submitButton" disabled class="w-full h-10 px-4 py-2 bg-primary text-white rounded-md opacity-50 cursor-not-allowed">Start Initialization</button>
</form>
</div>
</div>
</div>
<!-- 安装中卡片 -->
<div id="installingCard" class="fixed inset-0 flex items-center justify-center bg-white p-6 hidden">
<div class="w-full max-w-md space-y-6 text-center">
<h1 id="installingTitle" class="text-3xl font-bold">Installing...</h1>
<p id="installingMessage">Please wait while we initialize your application.</p>
<div class="flex justify-center">
<svg class="animate-spin h-10 w-10 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
</div>
</div>
</div>
<!-- 安装完成卡片 -->
<div id="installationCompleteCard" class="fixed inset-0 flex items-center justify-center bg-white p-6 hidden">
<div class="w-full max-w-md space-y-6 text-center">
<h1 id="installationCompleteTitle" class="text-3xl font-bold">Installation Complete</h1>
<p id="installationCompleteMessage">You can now close this page.</p>
</div>
</div>
<!-- 已初始化页面 -->
<div id="alreadyInitializedCard" class="fixed inset-0 flex items-center justify-center bg-white p-6 hidden">
<div class="w-full max-w-md space-y-6 text-center">
<h1 id="alreadyInitializedTitle" class="text-3xl font-bold">Already Initialized</h1>
<p id="alreadyInitializedMessage">Your PPanel application has already been initialized.</p>
</div>
</div>
<!-- 对话框 -->
<div id="dialog" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
<div class="bg-white rounded-lg p-6 w-96">
<h2 id="dialogTitle" class="text-xl font-bold mb-4"></h2>
<p id="dialogMessage" class="mb-4"></p>
<div class="flex justify-end">
<button onclick="closeDialog()" class="h-10 px-4 py-2 bg-primary text-white rounded-md">Close</button>
</div>
</div>
</div>
<!-- JavaScript -->
<script>
let currentLang = 'en';
const translations = {
en: {
// 英文翻译内容
title: 'Welcome to PPanel Setup',
description: "Let's get your PPanel application up and running. Please provide the necessary information below.",
adminInfoTitle: 'Administrator Details',
mysqlInfoTitle: 'MySQL Database Setup',
redisInfoTitle: 'Redis Cache Setup',
adminEmail: 'Administrator Email',
adminPassword: 'Administrator Password',
mysqlHost: 'Database Host',
mysqlPort: 'Database Port',
mysqlUser: 'Database User',
mysqlPassword: 'Database Password',
mysqlDatabase: 'Database Name',
redisHost: 'Redis Host',
redisPort: 'Redis Port',
redisPassword: 'Redis Password',
submit: 'Start Initialization',
switchLang: '中文',
testMySQLConnection: 'Test MySQL Connection',
testRedisConnection: 'Test Redis Connection',
connectionSuccess: 'Connection Successful',
connectionError: 'Connection Failed',
initializationError: 'Initialization Error',
initializationErrorMessage: 'An error occurred during initialization. Please try again.',
installingTitle: 'Installing...',
installingMessage: 'Please wait while we initialize your application.',
installationCompleteTitle: 'Installation Complete',
installationCompleteMessage: 'You can now close this page.',
proceedButton: 'Go to Application',
mysqlConnectionSuccess: 'Successfully connected to the MySQL database.',
mysqlConnectionError: 'Failed to connect to the MySQL database. Please check your settings.',
redisConnectionSuccess: 'Successfully connected to the Redis server.',
redisConnectionError: 'Failed to connect to the Redis server. Please check your settings.',
alreadyInitializedTitle: 'Already Initialized',
alreadyInitializedMessage: 'Your PPanel application has already been initialized.',
invalidEmail: 'Invalid email address.',
invalidPassword: 'Password must be at least 6 characters.',
},
zh: {
// 中文翻译内容
title: '欢迎使用 PPanel 设置向导',
description: '让我们一起启动您的 PPanel 应用程序。请提供以下必要信息。',
adminInfoTitle: '管理员详情',
mysqlInfoTitle: 'MySQL 数据库设置',
redisInfoTitle: 'Redis 缓存设置',
adminEmail: '管理员邮箱',
adminPassword: '管理员密码',
mysqlHost: '数据库主机',
mysqlPort: '数据库端口',
mysqlUser: '数据库用户',
mysqlPassword: '数据库密码',
mysqlDatabase: '数据库名称',
redisHost: 'Redis 主机',
redisPort: 'Redis 端口',
redisPassword: 'Redis 密码',
submit: '开始初始化',
switchLang: 'English',
testMySQLConnection: '测试 MySQL 连接',
testRedisConnection: '测试 Redis 连接',
connectionSuccess: '连接成功',
connectionError: '连接失败',
initializationError: '初始化错误',
initializationErrorMessage: '初始化过程中发生错误。请重试。',
installingTitle: '正在安装...',
installingMessage: '请稍候,我们正在初始化您的应用程序。',
installationCompleteTitle: '安装完成',
installationCompleteMessage: '您现在可以关闭此页面。',
proceedButton: '前往应用程序',
mysqlConnectionSuccess: '成功连接到 MySQL 数据库。',
mysqlConnectionError: '无法连接到 MySQL 数据库。请检查您的设置。',
redisConnectionSuccess: '成功连接到 Redis 服务器。',
redisConnectionError: '无法连接到 Redis 服务器。请检查您的设置。',
alreadyInitializedTitle: '已初始化',
alreadyInitializedMessage: '您的 PPanel 应用程序已被初始化。',
invalidEmail: '无效的邮箱地址。',
invalidPassword: '密码至少需要6个字符。',
}
};
function switchLanguage() {
currentLang = currentLang === 'en' ? 'zh' : 'en';
updateTexts();
}
function updateTexts() {
const t = translations[currentLang];
document.getElementById('title').textContent = t.title;
document.getElementById('description').textContent = t.description;
document.getElementById('adminInfoTitle').textContent = t.adminInfoTitle;
document.getElementById('mysqlInfoTitle').textContent = t.mysqlInfoTitle;
document.getElementById('redisInfoTitle').textContent = t.redisInfoTitle;
document.getElementById('labelAdminEmail').textContent = t.adminEmail;
document.getElementById('labelAdminPassword').textContent = t.adminPassword;
document.getElementById('labelMysqlHost').textContent = t.mysqlHost;
document.getElementById('labelMysqlPort').textContent = t.mysqlPort;
document.getElementById('labelMysqlUser').textContent = t.mysqlUser;
document.getElementById('labelMysqlPassword').textContent = t.mysqlPassword;
document.getElementById('labelMysqlDatabase').textContent = t.mysqlDatabase;
document.getElementById('labelRedisHost').textContent = t.redisHost;
document.getElementById('labelRedisPort').textContent = t.redisPort;
document.getElementById('labelRedisPassword').textContent = t.redisPassword;
document.getElementById('submitButton').textContent = t.submit;
document.getElementById('langSwitch').textContent = t.switchLang;
document.getElementById('testMySQLButton').textContent = t.testMySQLConnection;
document.getElementById('testRedisButton').textContent = t.testRedisConnection;
document.getElementById('installingTitle').textContent = t.installingTitle;
document.getElementById('installingMessage').textContent = t.installingMessage;
document.getElementById('installationCompleteTitle').textContent = t.installationCompleteTitle;
document.getElementById('installationCompleteMessage').textContent = t.installationCompleteMessage;
document.getElementById('alreadyInitializedTitle').textContent = t.alreadyInitializedTitle;
document.getElementById('alreadyInitializedMessage').textContent = t.alreadyInitializedMessage;
// 更新 placeholder 文本
const inputs = document.querySelectorAll('.input-field');
inputs.forEach((input) => {
const placeholder = input.getAttribute(`data-placeholder-${currentLang}`);
if (placeholder) {
input.setAttribute('placeholder', placeholder);
}
});
}
function checkInitialization() {
const isInitialized = false; // 需要从服务器获取实际状态
if (isInitialized) {
document.getElementById('appContainer').classList.add('hidden');
document.getElementById('alreadyInitializedCard').classList.remove('hidden');
}
}
function testConnection(type) {
const t = translations[currentLang];
let data = {};
let url = '';
if (type === 'mysql') {
data = {
host: document.getElementById('mysqlHost').value,
port: document.getElementById('mysqlPort').value,
user: document.getElementById('mysqlUser').value,
password: document.getElementById('mysqlPassword').value,
database: document.getElementById('mysqlDatabase').value,
};
url = '/init/mysql/test';
} else if (type === 'redis') {
data = {
host: document.getElementById('redisHost').value,
port: document.getElementById('redisPort').value,
password: document.getElementById('redisPassword').value,
};
url = '/init/redis/test';
}
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(result => {
console.log(result);
if (result.code === 200 && result.status) {
showDialog(t.connectionSuccess, result.msg || (type === 'mysql' ? t.mysqlConnectionSuccess : t.redisConnectionSuccess));
} else {
showDialog(t.connectionError, result.msg || (type === 'mysql' ? t.mysqlConnectionError : t.redisConnectionError));
}
})
.catch(error => {
console.error('Error:', error);
showDialog(t.connectionError, t.initializationErrorMessage);
});
}
function handleSubmit(event) {
event.preventDefault();
const form = document.getElementById('setupForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const t = translations[currentLang];
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// 显示安装中界面
document.getElementById('setupCard').classList.add('hidden');
document.getElementById('installingCard').classList.remove('hidden');
// 发送初始化请求
fetch('/init/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(result => {
document.getElementById('installingCard').classList.add('hidden');
if (result.code === 200 && result.status) {
document.getElementById('installationCompleteCard').classList.remove('hidden');
} else {
showDialog(t.initializationError, result.msg || t.initializationErrorMessage);
document.getElementById('setupCard').classList.remove('hidden');
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('installingCard').classList.add('hidden');
showDialog(t.initializationError, t.initializationErrorMessage);
document.getElementById('setupCard').classList.remove('hidden');
});
}
function showDialog(title, message) {
document.getElementById('dialogTitle').textContent = title;
document.getElementById('dialogMessage').textContent = message;
document.getElementById('dialog').classList.remove('hidden');
}
function closeDialog() {
document.getElementById('dialog').classList.add('hidden');
}
// 实时表单校验
function validateInput(input) {
const errorSpan = document.getElementById(input.id + 'Error');
if (input.type === 'email') {
// 邮箱格式验证
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(input.value)) {
errorSpan.textContent = translations[currentLang].invalidEmail;
errorSpan.classList.remove('hidden');
input.setCustomValidity(translations[currentLang].invalidEmail);
} else {
input.setCustomValidity('');
errorSpan.classList.add('hidden');
}
} else if (input.validity.valid) {
errorSpan.classList.add('hidden');
input.setCustomValidity('');
} else {
errorSpan.classList.remove('hidden');
}
updateSubmitButtonState();
}
function updateSubmitButtonState() {
const form = document.getElementById('setupForm');
const submitButton = document.getElementById('submitButton');
if (form.checkValidity()) {
submitButton.disabled = false;
submitButton.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
submitButton.disabled = true;
submitButton.classList.add('opacity-50', 'cursor-not-allowed');
}
}
// 添加事件监听器
document.addEventListener('DOMContentLoaded', () => {
const inputs = document.querySelectorAll('.input-field');
inputs.forEach((input) => {
input.addEventListener('input', () => validateInput(input));
});
updateSubmitButtonState();
updateTexts();
checkInitialization();
});
</script>
</body>
</html>
+48
View File
@@ -0,0 +1,48 @@
package initialize
import (
"context"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/tool"
)
type verifyConfig struct {
TurnstileSiteKey string
TurnstileSecret string
EnableLoginVerify bool
EnableRegisterVerify bool
EnableResetPasswordVerify bool
}
func Verify(svc *svc.ServiceContext) {
logger.Debug("Verify config initialization")
configs, err := svc.SystemModel.GetVerifyConfig(context.Background())
if err != nil {
logger.Error("[Init Verify Config] Get Verify Config Error: ", logger.Field("error", err.Error()))
return
}
var verify verifyConfig
tool.SystemConfigSliceReflectToStruct(configs, &verify)
svc.Config.Verify = config.Verify{
TurnstileSiteKey: verify.TurnstileSiteKey,
TurnstileSecret: verify.TurnstileSecret,
LoginVerify: verify.EnableLoginVerify,
RegisterVerify: verify.EnableRegisterVerify,
ResetPasswordVerify: verify.EnableResetPasswordVerify,
}
logger.Debug("Verify code config initialization")
var verifyCodeConfig config.VerifyCode
cfg, err := svc.SystemModel.GetVerifyCodeConfig(context.Background())
if err != nil {
logger.Errorf("[Init Verify Config] Get Verify Code Config Error: %s", err.Error())
return
}
tool.SystemConfigSliceReflectToStruct(cfg, &verifyCodeConfig)
svc.Config.VerifyCode = verifyCodeConfig
}
+45
View File
@@ -0,0 +1,45 @@
package initialize
import (
"errors"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"gorm.io/gorm"
"github.com/perfect-panel/ppanel-server/initialize/migrate"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/orm"
)
func Migrate(ctx *svc.ServiceContext) {
mc := orm.Mysql{
Config: ctx.Config.MySQL,
}
if err := migrate.Migrate(mc.Dsn()).Up(); err != nil {
if errors.Is(err, migrate.NoChange) {
logger.Info("[Migrate] database not change")
return
}
logger.Errorf("[Migrate] Up error: %v", err.Error())
panic(err)
}
// if not found admin user
err := ctx.DB.Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&user.User{}).Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := migrate.CreateAdminUser(ctx.Config.Administrator.Email, ctx.Config.Administrator.Password, tx); err != nil {
logger.Errorf("[Migrate] CreateAdminUser error: %v", err.Error())
return err
}
logger.Info("[Migrate] Create admin user success")
}
return nil
})
if err != nil {
panic(err)
}
}