Import x-panel source
This commit is contained in:
218
database/db.go
Normal file
218
database/db.go
Normal 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
65
database/history.go
Normal 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() 调用。
|
||||
// 事务成功后返回 nil,GORM 会自动提交事务。
|
||||
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
|
||||
}
|
||||
11
database/model/lottery_win.go
Normal file
11
database/model/lottery_win.go
Normal 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
124
database/model/model.go
Normal 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/s,0 表示不限速。
|
||||
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"`
|
||||
}
|
||||
Reference in New Issue
Block a user