Import x-panel source

This commit is contained in:
2026-05-03 11:34:48 +08:00
commit e98e780360
312 changed files with 90189 additions and 0 deletions

218
database/db.go Normal file
View File

@@ -0,0 +1,218 @@
package database
import (
"bytes"
"io"
"io/fs"
"log"
"os"
"path"
"slices"
"time"
"x-ui/config"
"x-ui/database/model"
"x-ui/util/crypto"
"x-ui/xray"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var db *gorm.DB
const (
defaultUsername = "admin"
defaultPassword = "admin"
)
func initModels() error {
models := []any{
&model.User{},
&model.Inbound{},
&model.OutboundTraffics{},
&model.Setting{},
&model.InboundClientIps{},
&xray.ClientTraffic{},
&model.HistoryOfSeeders{},
&LinkHistory{}, // 把 LinkHistory 表也迁移
&model.LotteryWin{}, // 新增 抽奖游戏LotteryWin 数据模型
}
for _, model := range models {
if err := db.AutoMigrate(model); err != nil {
log.Printf("Error auto migrating model: %v", err)
return err
}
}
return nil
}
func initUser() error {
empty, err := isTableEmpty("users")
if err != nil {
log.Printf("Error checking if users table is empty: %v", err)
return err
}
if empty {
hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword)
if err != nil {
log.Printf("Error hashing default password: %v", err)
return err
}
user := &model.User{
Username: defaultUsername,
Password: hashedPassword,
}
return db.Create(user).Error
}
return nil
}
func runSeeders(isUsersEmpty bool) error {
empty, err := isTableEmpty("history_of_seeders")
if err != nil {
log.Printf("Error checking if users table is empty: %v", err)
return err
}
if empty && isUsersEmpty {
hashSeeder := &model.HistoryOfSeeders{
SeederName: "UserPasswordHash",
}
return db.Create(hashSeeder).Error
} else {
var seedersHistory []string
db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory)
if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty {
var users []model.User
db.Find(&users)
for _, user := range users {
hashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password)
if err != nil {
log.Printf("Error hashing password for user '%s': %v", user.Username, err)
return err
}
db.Model(&user).Update("password", hashedPassword)
}
hashSeeder := &model.HistoryOfSeeders{
SeederName: "UserPasswordHash",
}
return db.Create(hashSeeder).Error
}
}
return nil
}
func isTableEmpty(tableName string) (bool, error) {
var count int64
err := db.Table(tableName).Count(&count).Error
return count == 0, err
}
func InitDB(dbPath string) error {
dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModePerm)
if err != nil {
return err
}
var gormLogger logger.Interface
if config.IsDebug() {
gormLogger = logger.Default
} else {
gormLogger = logger.Discard
}
c := &gorm.Config{
Logger: gormLogger,
}
db, err = gorm.Open(sqlite.Open(dbPath), c)
if err != nil {
return err
}
if err := initModels(); err != nil {
return err
}
isUsersEmpty, err := isTableEmpty("users")
if err := initUser(); err != nil {
return err
}
return runSeeders(isUsersEmpty)
}
func CloseDB() error {
if db != nil {
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
return nil
}
func GetDB() *gorm.DB {
return db
}
func IsNotFound(err error) bool {
return err == gorm.ErrRecordNotFound
}
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.ReadAt(buf, 0)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}
func Checkpoint() error {
// Update WAL
err := db.Exec("PRAGMA wal_checkpoint;").Error
if err != nil {
return err
}
return nil
}
// HasUserWonToday 检查指定用户今天是否已经中过奖
// 〔中文注释〕:【修正】将 gorm.DB() 替换为全局变量 db
func HasUserWonToday(userID int64) (bool, error) {
now := time.Now()
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
endOfDay := startOfDay.Add(24 * time.Hour)
var count int64
// 在 lottery_wins 表中查找符合条件用户ID匹配且中奖日期在今天之内的记录数量
err := db.Model(&model.LotteryWin{}).Where("user_id = ? AND win_date >= ? AND win_date < ?", userID, startOfDay, endOfDay).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// RecordUserWin 记录用户的中奖信息
// 〔中文注释〕:【修正】将 gorm.DB() 替换为全局变量 db
func RecordUserWin(userID int64, prize string) error {
winRecord := &model.LotteryWin{
UserID: userID,
Prize: prize,
WinDate: time.Now(),
}
// 在 lottery_wins 表中创建一条新的记录
return db.Create(winRecord).Error
}

65
database/history.go Normal file
View File

@@ -0,0 +1,65 @@
package database
import (
"time"
"gorm.io/gorm" // 【中文注释】: 确保 gorm 被导入,以便在函数签名中使用
)
// LinkHistory GORM aodel for link_history table
type LinkHistory struct {
Id int `gorm:"primaryKey"`
Type string `gorm:"type:varchar(255);not null"`
Link string `gorm:"type:text;not null"`
CreatedAt time.Time `gorm:"not null"`
}
// AddLinkHistory 在一个事务中添加新链接记录并修剪旧记录。
// 它确保了操作的原子性:所有更改要么全部应用,要么全部回滚。
func AddLinkHistory(record *LinkHistory) error {
// 【核心修正】: 使用 GORM 的事务功能来包装所有的数据库写入和删除操作。
// 这样可以确保数据的一致性。
return db.Transaction(func(tx *gorm.DB) error {
// 1. 添加新记录
// 【重要】: 在事务内部,必须使用 tx 变量,而不是全局的 db 变量。
if err := tx.Create(record).Error; err != nil {
return err // 如果这里出错,事务将自动回滚
}
// 2. 修剪旧记录,仅保留最近的 10 条
var count int64
// 【重要】: 使用 tx 进行计数
if err := tx.Model(&LinkHistory{}).Count(&count).Error; err != nil {
return err
}
if count > 10 {
limit := int(count) - 10
var recordsToDelete []LinkHistory
// 【重要】: 使用 tx 查找要删除的记录
if err := tx.Order("created_at asc").Limit(limit).Find(&recordsToDelete).Error; err != nil {
return err
}
if len(recordsToDelete) > 0 {
// 【重要】: 使用 tx 删除记录
if err := tx.Delete(&recordsToDelete).Error; err != nil {
return err
}
}
}
// 【核心修正】: 从此函数中移除了 Checkpoint() 调用。
// 事务成功后返回 nilGORM 会自动提交事务。
return nil
})
}
// GetLinkHistory retrieves the 10 most recent link records
func GetLinkHistory() ([]*LinkHistory, error) {
var histories []*LinkHistory
err := db.Order("created_at desc").Limit(10).Find(&histories).Error
if err != nil {
return nil, err
}
return histories, nil
}

View File

@@ -0,0 +1,11 @@
package model
import "time"
// LotteryWin 用于记录用户的中奖历史
type LotteryWin struct {
ID int64 `gorm:"primaryKey"`
UserID int64 `gorm:"index"` // Telegram 用户 ID
Prize string // 奖品等级,如 "一等奖"
WinDate time.Time // 中奖日期
}

124
database/model/model.go Normal file
View File

@@ -0,0 +1,124 @@
package model
import (
"fmt"
"x-ui/util/json_util"
"x-ui/xray"
)
type Protocol string
const (
VMESS Protocol = "vmess"
VLESS Protocol = "vless"
Tunnel Protocol = "tunnel"
HTTP Protocol = "http"
Trojan Protocol = "trojan"
Shadowsocks Protocol = "shadowsocks"
Socks Protocol = "socks"
WireGuard Protocol = "wireguard"
)
type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"`
Password string `json:"password"`
}
type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey"`
UserId int `json:"-"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"`
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"`
Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
// 中文注释: 新增设备限制字段,用于存储每个入站的设备数限制。
// gorm:"column:device_limit;default:0" 定义了数据库中的字段名和默认值。
DeviceLimit int `json:"deviceLimit" form:"deviceLimit" gorm:"column:device_limit;default:0"`
ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
// config part
Listen string `json:"listen" form:"listen"`
Port int `json:"port" form:"port"`
Protocol Protocol `json:"protocol" form:"protocol"`
Settings string `json:"settings" form:"settings"`
StreamSettings string `json:"streamSettings" form:"streamSettings"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"`
}
type OutboundTraffics struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Up int64 `json:"up" form:"up" gorm:"default:0"`
Down int64 `json:"down" form:"down" gorm:"default:0"`
Total int64 `json:"total" form:"total" gorm:"default:0"`
}
type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
Ips string `json:"ips" form:"ips"`
}
type HistoryOfSeeders struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
SeederName string `json:"seederName"`
}
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen
if listen != "" {
listen = fmt.Sprintf("\"%v\"", listen)
}
return &xray.InboundConfig{
Listen: json_util.RawMessage(listen),
Port: i.Port,
Protocol: string(i.Protocol),
Settings: json_util.RawMessage(i.Settings),
StreamSettings: json_util.RawMessage(i.StreamSettings),
Tag: i.Tag,
Sniffing: json_util.RawMessage(i.Sniffing),
}
}
type Setting struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Key string `json:"key" form:"key"`
Value string `json:"value" form:"value"`
}
type Client struct {
ID string `json:"id"`
Security string `json:"security"`
Password string `json:"password"`
// 中文注释: 新增“限速”字段,单位 KB/s0 表示不限速。
SpeedLimit int `json:"speedLimit" form:"speedLimit"`
Flow string `json:"flow"`
Email string `json:"email"`
LimitIP int `json:"limitIp"`
TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Enable bool `json:"enable" form:"enable"`
TgID int64 `json:"tgId" form:"tgId"`
SubID string `json:"subId" form:"subId"`
Comment string `json:"comment" form:"comment"`
Reset int `json:"reset" form:"reset"`
CreatedAt int64 `json:"created_at,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
}
type VLESSSettings struct {
Clients []Client `json:"clients"`
Decryption string `json:"decryption"`
Encryption string `json:"encryption"`
Fallbacks []any `json:"fallbacks"`
}