Полная зачистка legacy + закрытие fix24.1/fix24.2 + обновление логотипа

This commit is contained in:
2026-05-08 23:16:25 +05:00
parent de4a65a959
commit 1bca73814e
85 changed files with 4119 additions and 1911 deletions
-119
View File
@@ -1,119 +0,0 @@
package dao
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/dto"
"hy2xs-admin/model/entity"
"time"
)
func SaveAccount(account entity.Account) (int64, error) {
if tx := sqliteDB.Save(&account); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return 0, errors.New(constant.SysError)
}
return *account.Id, nil
}
func DeleteAccount(ids []int64) error {
if tx := sqliteDB.Where("id in ?", ids).Delete(&entity.Account{}); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func UpdateAccount(ids []int64, updates map[string]interface{}) error {
if len(updates) > 0 {
updates["update_time"] = time.Now().Format("2006-01-02 15:04:05")
if tx := sqliteDB.Model(&entity.Account{}).
Where("id in ?", ids).
Updates(updates); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
}
return nil
}
func UpsertAccount(accounts []entity.Account) error {
if tx := sqliteDB.Model(&entity.Account{}).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "username"}},
DoUpdates: clause.AssignmentColumns([]string{"pass", "con_pass", "quota", "download", "upload", "expire_time", "kick_util_time", "device_no", "role", "deleted", "create_time", "update_time", "login_at", "con_at", "remark"}),
}).Create(accounts); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func UpdateAccountTraffic(username string, download int64, upload int64) error {
if upload != 0 || download != 0 {
updates := map[string]interface{}{}
if download != 0 {
updates["download"] = gorm.Expr("download + ?", download)
}
if upload != 0 {
updates["upload"] = gorm.Expr("upload + ?", upload)
}
updates["update_time"] = time.Now().Format("2006-01-02 15:04:05")
if tx := sqliteDB.Model(&entity.Account{}).
Where("username = ?", username).
Updates(updates); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
}
return nil
}
func GetAccount(query interface{}, args ...interface{}) (entity.Account, error) {
var account entity.Account
if tx := sqliteDB.Model(&entity.Account{}).
Where(query, args...).First(&account); tx.Error != nil {
if tx.Error == gorm.ErrRecordNotFound {
return account, errors.New(constant.WrongPassword)
}
logrus.Errorf("%v", tx.Error)
return account, errors.New(constant.SysError)
}
return account, nil
}
func PageAccount(accountPageDto dto.AccountPageDto) ([]entity.Account, int64, error) {
var accounts []entity.Account
var total int64
tx := sqliteDB.Model(&entity.Account{})
if accountPageDto.Username != nil && *accountPageDto.Username != "" {
tx.Where("username like ?", fmt.Sprintf("%%%s%%", *accountPageDto.Username))
}
if accountPageDto.Deleted != nil {
tx.Where("deleted = ?", *accountPageDto.Deleted)
}
if accountPageDto.Remark != nil && *accountPageDto.Remark != "" {
tx.Where("remark like ?", fmt.Sprintf("%%%s%%", *accountPageDto.Remark))
}
tx.Count(&total)
if tx.Scopes(Paginate(accountPageDto.PageNum, accountPageDto.PageSize)).
Order("role,create_time desc").
Find(&accounts); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return accounts, 0, errors.New(constant.SysError)
}
return accounts, total, nil
}
func ListAccount(query interface{}, args ...interface{}) ([]entity.Account, error) {
var accounts []entity.Account
if tx := sqliteDB.Model(&entity.Account{}).
Where(query, args...).Order("role,create_time desc").Find(&accounts); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return accounts, errors.New(constant.SysError)
}
return accounts, nil
}
+43
View File
@@ -0,0 +1,43 @@
package dao
import (
"errors"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/entity"
"time"
)
func GetAdminUser(query interface{}, args ...interface{}) (entity.AdminUser, error) {
var admin entity.AdminUser
if tx := sqliteDB.Model(&entity.AdminUser{}).Where(query, args...).First(&admin); tx.Error != nil {
if tx.Error == gorm.ErrRecordNotFound {
return admin, errors.New(constant.WrongPassword)
}
logrus.Errorf("%v", tx.Error)
return admin, errors.New(constant.SysError)
}
return admin, nil
}
func SaveAdminUser(admin entity.AdminUser) (int64, error) {
if tx := sqliteDB.Save(&admin); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return 0, errors.New(constant.SysError)
}
return *admin.Id, nil
}
func UpdateAdminUser(ids []int64, updates map[string]interface{}) error {
if len(updates) == 0 {
return nil
}
updates["update_time"] = time.Now().Format("2006-01-02 15:04:05")
if tx := sqliteDB.Model(&entity.AdminUser{}).Where("id in ?", ids).Updates(updates); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
+155
View File
@@ -0,0 +1,155 @@
package dao
import (
"errors"
"github.com/sirupsen/logrus"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/entity"
"hy2xs-admin/model/vo"
"time"
)
func SaveMetricSample(sample entity.MetricSample) error {
if tx := sqliteDB.Save(&sample); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func LastMetricSample() (entity.MetricSample, error) {
var sample entity.MetricSample
if tx := sqliteDB.Model(&entity.MetricSample{}).Order("sampled_at desc").Limit(1).Find(&sample); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return sample, errors.New(constant.SysError)
}
return sample, nil
}
func CleanupMetricSample(olderThanMs int64) error {
if !tableExists("metric_sample") {
return nil
}
if tx := sqliteDB.Exec("DELETE FROM metric_sample WHERE sampled_at < ?", olderThanMs); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func DashboardPeerSummary(nowMs int64) (vo.DashboardPeerVo, error) {
result := vo.DashboardPeerVo{}
type row struct {
Total int64
Enabled int64
Disabled int64
Expired int64
}
var r row
if tx := sqliteDB.Raw(`SELECT
COUNT(1) AS total,
COALESCE(SUM(CASE WHEN disabled = 0 THEN 1 ELSE 0 END),0) AS enabled,
COALESCE(SUM(CASE WHEN disabled = 1 THEN 1 ELSE 0 END),0) AS disabled,
COALESCE(SUM(CASE WHEN expires_at > 0 AND expires_at < ? THEN 1 ELSE 0 END),0) AS expired
FROM peer`, nowMs).Scan(&r); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return result, errors.New(constant.SysError)
}
result.Total = r.Total
result.Enabled = r.Enabled
result.Disabled = r.Disabled
result.Expired = r.Expired
return result, nil
}
func DashboardTrafficSummary() (vo.DashboardTrafficVo, error) {
result := vo.DashboardTrafficVo{}
type row struct {
Download int64
Upload int64
}
var r row
if tx := sqliteDB.Raw(`SELECT
COALESCE(SUM(download_bytes),0) AS download,
COALESCE(SUM(upload_bytes),0) AS upload
FROM peer`).Scan(&r); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return result, errors.New(constant.SysError)
}
result.DownloadBytes = r.Download
result.UploadBytes = r.Upload
result.TotalBytes = r.Download + r.Upload
result.SinceResetDownloadBytes = r.Download
result.SinceResetUploadBytes = r.Upload
now := time.Now().UnixMilli()
dayStart := now - (now % int64(24*time.Hour/time.Millisecond))
var today row
if tx := sqliteDB.Raw(`SELECT
COALESCE(SUM(rx_bytes),0) AS download,
COALESCE(SUM(tx_bytes),0) AS upload
FROM traffic_sample WHERE sampled_at >= ?`, dayStart).Scan(&today); tx.Error == nil {
result.TodayDownloadBytes = today.Download
result.TodayUploadBytes = today.Upload
}
return result, nil
}
func DashboardTopPeers(fromMs int64, toMs int64, limit int) ([]vo.DashboardTopPeerVo, error) {
if limit <= 0 {
limit = 10
}
rows := make([]vo.DashboardTopPeerVo, 0)
if tx := sqliteDB.Raw(`SELECT
p.id AS peer_id,
p.name AS name,
p.remark AS remark,
COALESCE(SUM(ts.rx_bytes),0) AS download,
COALESCE(SUM(ts.tx_bytes),0) AS upload,
COALESCE(SUM(ts.rx_bytes + ts.tx_bytes),0) AS total
FROM traffic_sample ts
JOIN peer p ON p.id = ts.peer_id
WHERE ts.sampled_at BETWEEN ? AND ?
GROUP BY p.id, p.name, p.remark
ORDER BY total DESC
LIMIT ?`, fromMs, toMs, limit).Scan(&rows); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return rows, errors.New(constant.SysError)
}
return rows, nil
}
func DashboardTrafficTimeseries(fromMs int64, toMs int64) ([]vo.DashboardSeriesPointVo, error) {
rows := make([]vo.DashboardSeriesPointVo, 0)
if tx := sqliteDB.Raw(`SELECT
hour_start AS ts,
COALESCE(SUM(rx_bytes),0) AS download,
COALESCE(SUM(tx_bytes),0) AS upload
FROM traffic_aggregate_hourly
WHERE hour_start BETWEEN ? AND ?
GROUP BY hour_start
ORDER BY hour_start ASC`, fromMs, toMs).Scan(&rows); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return rows, errors.New(constant.SysError)
}
return rows, nil
}
func DashboardSystemTimeseries(fromMs int64, toMs int64) ([]vo.DashboardSeriesPointVo, error) {
rows := make([]vo.DashboardSeriesPointVo, 0)
if !tableExists("metric_sample") {
return rows, nil
}
if tx := sqliteDB.Raw(`SELECT
sampled_at AS ts,
cpu_percent AS cpu,
mem_percent AS mem
FROM metric_sample
WHERE sampled_at BETWEEN ? AND ?
ORDER BY sampled_at ASC`, fromMs, toMs).Scan(&rows); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return rows, errors.New(constant.SysError)
}
return rows, nil
}
+102
View File
@@ -0,0 +1,102 @@
package dao
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/dto"
"hy2xs-admin/model/entity"
"time"
)
func SavePeer(peer entity.Peer) (int64, error) {
if tx := sqliteDB.Save(&peer); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return 0, errors.New(constant.SysError)
}
return *peer.Id, nil
}
func DeletePeer(ids []int64) error {
if tx := sqliteDB.Where("id in ?", ids).Delete(&entity.Peer{}); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func UpdatePeer(ids []int64, updates map[string]interface{}) error {
if len(updates) == 0 {
return nil
}
updates["update_time"] = time.Now().Format("2006-01-02 15:04:05")
if tx := sqliteDB.Model(&entity.Peer{}).Where("id in ?", ids).Updates(updates); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func GetPeer(query interface{}, args ...interface{}) (entity.Peer, error) {
var peer entity.Peer
if tx := sqliteDB.Model(&entity.Peer{}).Where(query, args...).First(&peer); tx.Error != nil {
if tx.Error == gorm.ErrRecordNotFound {
return peer, errors.New(constant.WrongPassword)
}
logrus.Errorf("%v", tx.Error)
return peer, errors.New(constant.SysError)
}
return peer, nil
}
func ListPeer(query interface{}, args ...interface{}) ([]entity.Peer, error) {
var peers []entity.Peer
if tx := sqliteDB.Model(&entity.Peer{}).Where(query, args...).Order("create_time desc").Find(&peers); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return peers, errors.New(constant.SysError)
}
return peers, nil
}
func PagePeer(peerPageDto dto.PeerPageDto) ([]entity.Peer, int64, error) {
var peers []entity.Peer
var total int64
tx := sqliteDB.Model(&entity.Peer{})
if peerPageDto.Username != nil && *peerPageDto.Username != "" {
tx.Where("name like ?", fmt.Sprintf("%%%s%%", *peerPageDto.Username))
}
if peerPageDto.Deleted != nil {
tx.Where("disabled = ?", *peerPageDto.Deleted)
}
if peerPageDto.Remark != nil && *peerPageDto.Remark != "" {
tx.Where("remark like ?", fmt.Sprintf("%%%s%%", *peerPageDto.Remark))
}
tx.Count(&total)
if tx.Scopes(Paginate(peerPageDto.PageNum, peerPageDto.PageSize)).Order("create_time desc").Find(&peers); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return peers, 0, errors.New(constant.SysError)
}
return peers, total, nil
}
func UpdatePeerTraffic(name string, download int64, upload int64) error {
if upload == 0 && download == 0 {
return nil
}
updates := map[string]interface{}{}
if download != 0 {
updates["download_bytes"] = gorm.Expr("download_bytes + ?", download)
}
if upload != 0 {
updates["upload_bytes"] = gorm.Expr("upload_bytes + ?", upload)
}
updates["update_time"] = time.Now().Format("2006-01-02 15:04:05")
if tx := sqliteDB.Model(&entity.Peer{}).Where("name = ?", name).Updates(updates); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
+342 -56
View File
@@ -2,6 +2,7 @@ package dao
import (
"errors"
"fmt"
"github.com/glebarez/sqlite"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
@@ -12,13 +13,13 @@ import (
"hy2xs-admin/util"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)
var sqlInitStr = "CREATE TABLE IF NOT EXISTS account\n(\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n username TEXT NOT NULL UNIQUE DEFAULT '',\n pass TEXT NOT NULL DEFAULT '',\n con_pass TEXT NOT NULL DEFAULT '',\n quota INTEGER NOT NULL DEFAULT 0,\n download INTEGER NOT NULL DEFAULT 0,\n upload INTEGER NOT NULL DEFAULT 0,\n expire_time INTEGER NOT NULL DEFAULT 0,\n kick_util_time INTEGER NOT NULL DEFAULT 0,\n device_no INTEGER NOT NULL DEFAULT 3,\n role TEXT NOT NULL DEFAULT 'user',\n deleted INTEGER NOT NULL DEFAULT 0,\n create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\nALTER TABLE account\n ADD COLUMN login_at INTEGER NOT NULL DEFAULT 0;\nALTER TABLE account\n ADD COLUMN con_at INTEGER NOT NULL DEFAULT 0;\nALTER TABLE account\n ADD COLUMN remark INTEGER NOT NULL DEFAULT '';\nALTER TABLE account\n ADD COLUMN force_password_change INTEGER NOT NULL DEFAULT 0;\nCREATE INDEX IF NOT EXISTS account_deleted_index ON account (deleted);\nCREATE INDEX IF NOT EXISTS account_username_index ON account (username);\nCREATE INDEX IF NOT EXISTS account_con_pass_index ON account (con_pass);\nCREATE INDEX IF NOT EXISTS account_pass_index ON account (pass);\nCREATE TABLE IF NOT EXISTS config\n(\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n key TEXT NOT NULL UNIQUE DEFAULT '',\n value TEXT NOT NULL DEFAULT '',\n remark TEXT NOT NULL DEFAULT '',\n create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n);\nCREATE INDEX IF NOT EXISTS config_key_index ON config (key);\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_WEB_PORT', '8081', 'HY2XS admin Web Port'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_PORT');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_WEB_CONTEXT', '/', 'HY2XS admin Web Context'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_WEB_CONTEXT');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_CRT_PATH', '', 'HY2XS admin CRT File Path'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_CRT_PATH');\nINSERT INTO config (key, value, remark)\nSELECT 'H_UI_KEY_PATH', '', 'HY2XS admin KEY File Path'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'H_UI_KEY_PATH');\nINSERT INTO config (key, value, remark)\nSELECT 'JWT_SECRET', hex(randomblob(10)), 'JWT Secret'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'JWT_SECRET');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_ENABLE', '0', 'Hysteria2 Switch'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_ENABLE');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_CONFIG', '', 'Hysteria2 Config'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_TRAFFIC_TIME', '1', 'Hysteria2 Traffic Time'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_TRAFFIC_TIME');\nINSERT INTO config (key, value, remark)\nSELECT 'HYSTERIA2_CONFIG_REMARK', '', 'Hysteria2 Config Remark'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'HYSTERIA2_CONFIG_REMARK');\nINSERT INTO config (key, value, remark)\nSELECT 'RESET_TRAFFIC_CRON', '', 'Reset Traffic Cron'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'RESET_TRAFFIC_CRON');\nINSERT INTO config (key, value, remark)\nSELECT 'CLASH_EXTENSION', '', 'Clash Subscription Extension'\n WHERE NOT EXISTS (SELECT 1 FROM config WHERE key = 'CLASH_EXTENSION');"
var sqliteDB *gorm.DB
func InitSqliteDB() error {
@@ -50,7 +51,7 @@ func InitSql(port string) error {
if err := InitSqliteDB(); err != nil {
return err
}
if err := sqliteInit(sqlInitStr); err != nil {
if err := runMigrations(); err != nil {
return err
}
if port != "" {
@@ -59,10 +60,6 @@ func InitSql(port string) error {
return errors.New("sqlite exec err")
}
}
if err := ensureAccountSchema(); err != nil {
return err
}
if err := ensureSecureBootstrapAdmin(); err != nil {
return err
}
@@ -96,14 +93,6 @@ func envBoolAsInt(name string, fallback int) int {
return fallback
}
func ensureAccountSchema() error {
if tx := sqliteDB.Exec("ALTER TABLE account ADD COLUMN force_password_change INTEGER NOT NULL DEFAULT 0"); tx.Error != nil && !strings.Contains(tx.Error.Error(), "duplicate column name") {
logrus.Errorf("sqlite exec err: %v", tx.Error)
return errors.New("sqlite exec err")
}
return nil
}
func ensureSecureBootstrapAdmin() error {
adminUser := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_USER"))
if adminUser == "" {
@@ -118,50 +107,363 @@ func ensureSecureBootstrapAdmin() error {
adminPassword = password
}
forcePasswordChange := envBoolAsInt("HY2XS_FORCE_PASSWORD_CHANGE", 1)
quota := int64(-1)
expireTime := int64(253370736000000)
deviceNo := int64(envInt("HY2XS_ADMIN_DEVICE_NO", 6))
role := "admin"
deleted := int64(0)
conPass := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_CON_PASS"))
if conPass == "" {
generated, genErr := util.RandomString(28)
if genErr != nil {
return genErr
}
conPass = generated
}
status := int64(1)
tokenVersion := int64(1)
passwordChangedAt := time.Now().UnixMilli()
hash, hashErr := util.HashPassword(adminPassword)
if hashErr != nil {
return hashErr
}
admin, err := GetAccount("role = 'admin' and deleted = 0")
admin, err := GetAdminUser("username = ?", adminUser)
if err != nil {
username := adminUser
account := entity.Account{
account := entity.AdminUser{
Username: &username,
Pass: &hash,
ConPass: &conPass,
Quota: &quota,
ExpireTime: &expireTime,
DeviceNo: &deviceNo,
Role: &role,
Deleted: &deleted,
PasswordHash: &hash,
Status: &status,
TokenVersion: &tokenVersion,
PasswordChangedAt: &passwordChangedAt,
ForcePasswordChange: func() *int64 { v := int64(forcePasswordChange); return &v }(),
}
if _, saveErr := SaveAccount(account); saveErr != nil {
if _, saveErr := SaveAdminUser(account); saveErr != nil {
return saveErr
}
return nil
}
if admin.Pass == nil {
if admin.PasswordHash == nil {
return nil
}
return nil
}
func runMigrations() error {
if tx := sqliteDB.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`); tx.Error != nil {
logrus.Errorf("sqlite migration init err: %v", tx.Error)
return errors.New("sqlite migration init err")
}
type migration struct {
version string
apply func() error
}
migrations := []migration{
{version: "001_admin_peer_split", apply: migrateAdminPeerSplit},
{version: "002_migrate_legacy_accounts", apply: migrateLegacyAccounts},
{version: "003_archive_legacy_account", apply: archiveLegacyAccount},
{version: "004_traffic_samples_and_aggregates", apply: migrateTrafficTables},
{version: "005_metric_sample", apply: migrateMetricSampleTable},
}
for _, m := range migrations {
if isApplied, err := migrationApplied(m.version); err != nil {
return err
} else if isApplied {
continue
}
if err := m.apply(); err != nil {
return err
}
if tx := sqliteDB.Exec("INSERT INTO schema_migrations(version) VALUES(?)", m.version); tx.Error != nil {
logrus.Errorf("sqlite migration mark err: %v", tx.Error)
return errors.New("sqlite migration mark err")
}
}
return nil
}
func migrationApplied(version string) (bool, error) {
var count int64
if tx := sqliteDB.Raw("SELECT COUNT(1) FROM schema_migrations WHERE version = ?", version).Scan(&count); tx.Error != nil {
logrus.Errorf("sqlite migration query err: %v", tx.Error)
return false, errors.New("sqlite migration query err")
}
return count > 0, nil
}
func migrateAdminPeerSplit() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS admin_user (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE DEFAULT '',
password_hash TEXT NOT NULL DEFAULT '',
status INTEGER NOT NULL DEFAULT 1,
force_password_change INTEGER NOT NULL DEFAULT 0,
last_login_at INTEGER NOT NULL DEFAULT 0,
password_changed_at INTEGER NOT NULL DEFAULT 0,
token_version INTEGER NOT NULL DEFAULT 1,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS admin_user_username_index ON admin_user (username)`,
`CREATE TABLE IF NOT EXISTS peer (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE DEFAULT '',
remark TEXT NOT NULL DEFAULT '',
auth_id TEXT NOT NULL UNIQUE DEFAULT '',
secret_digest TEXT NOT NULL UNIQUE DEFAULT '',
secret_ciphertext TEXT NOT NULL DEFAULT '',
quota_bytes INTEGER NOT NULL DEFAULT 0,
download_bytes INTEGER NOT NULL DEFAULT 0,
upload_bytes INTEGER NOT NULL DEFAULT 0,
expires_at INTEGER NOT NULL DEFAULT 0,
max_devices INTEGER NOT NULL DEFAULT 3,
disabled INTEGER NOT NULL DEFAULT 0,
banned_until INTEGER NOT NULL DEFAULT 0,
last_connection_at INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS peer_name_index ON peer (name)`,
`CREATE INDEX IF NOT EXISTS peer_auth_id_index ON peer (auth_id)`,
`CREATE INDEX IF NOT EXISTS peer_secret_digest_index ON peer (secret_digest)`,
}
for _, stmt := range stmts {
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
return errors.New("sqlite migration exec err")
}
}
return nil
}
func migrateLegacyAccounts() error {
if !tableExists("account") {
return nil
}
var accounts []entity.Account
if tx := sqliteDB.Model(&entity.Account{}).Order("id asc").Find(&accounts); tx.Error != nil {
logrus.Errorf("sqlite legacy account query err: %v", tx.Error)
return errors.New("sqlite legacy account query err")
}
nowMs := time.Now().UnixMilli()
for _, acc := range accounts {
if acc.Role != nil && *acc.Role == "admin" {
if acc.Username == nil || acc.Pass == nil {
continue
}
_, getErr := GetAdminUser("username = ?", *acc.Username)
if getErr == nil {
continue
}
status := int64(1)
if acc.Deleted != nil && *acc.Deleted == 1 {
status = 0
}
lastLogin := int64(0)
if acc.LoginAt != nil {
lastLogin = *acc.LoginAt
}
passwordChangedAt := nowMs
admin := entity.AdminUser{
Username: acc.Username,
PasswordHash: acc.Pass,
Status: &status,
ForcePasswordChange: acc.ForcePasswordChange,
LastLoginAt: &lastLogin,
PasswordChangedAt: &passwordChangedAt,
TokenVersion: func() *int64 { v := int64(1); return &v }(),
}
if _, saveErr := SaveAdminUser(admin); saveErr != nil {
return saveErr
}
continue
}
if acc.Username == nil || acc.ConPass == nil {
continue
}
_, getPeerErr := GetPeer("name = ?", *acc.Username)
if getPeerErr == nil {
continue
}
authId, authErr := util.RandomString(18)
if authErr != nil {
return authErr
}
secretDigest := util.PeerSecretDigest(*acc.ConPass)
secretCiphertext := *acc.ConPass
quota := int64(0)
if acc.Quota != nil {
quota = *acc.Quota
}
download := int64(0)
if acc.Download != nil {
download = *acc.Download
}
upload := int64(0)
if acc.Upload != nil {
upload = *acc.Upload
}
expires := int64(0)
if acc.ExpireTime != nil {
expires = *acc.ExpireTime
}
maxDevices := int64(3)
if acc.DeviceNo != nil {
maxDevices = *acc.DeviceNo
}
disabled := int64(0)
if acc.Deleted != nil {
disabled = *acc.Deleted
}
bannedUntil := int64(0)
if acc.KickUtilTime != nil {
bannedUntil = *acc.KickUtilTime
}
lastConnection := int64(0)
if acc.ConAt != nil {
lastConnection = *acc.ConAt
}
remark := ""
if acc.Remark != nil {
remark = *acc.Remark
}
peer := entity.Peer{
Name: acc.Username,
Remark: &remark,
AuthId: &authId,
SecretDigest: &secretDigest,
SecretCiphertext: &secretCiphertext,
QuotaBytes: &quota,
DownloadBytes: &download,
UploadBytes: &upload,
ExpiresAt: &expires,
MaxDevices: &maxDevices,
Disabled: &disabled,
BannedUntil: &bannedUntil,
LastConnectionAt: &lastConnection,
}
if _, saveErr := SavePeer(peer); saveErr != nil {
return saveErr
}
}
return nil
}
func archiveLegacyAccount() error {
if !tableExists("account") {
return nil
}
backupName := fmt.Sprintf("legacy_account_backup_%d", time.Now().Unix())
if tx := sqliteDB.Exec("ALTER TABLE account RENAME TO " + backupName); tx.Error != nil {
logrus.Errorf("sqlite legacy archive err: %v", tx.Error)
return errors.New("sqlite legacy archive err")
}
return nil
}
func migrateTrafficTables() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS traffic_sample (
id INTEGER PRIMARY KEY AUTOINCREMENT,
peer_id INTEGER NOT NULL,
auth_id TEXT NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
sampled_at INTEGER NOT NULL,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_sample_time ON traffic_sample(sampled_at)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_sample_peer_time ON traffic_sample(peer_id, sampled_at)`,
`CREATE TABLE IF NOT EXISTS traffic_aggregate_hourly (
peer_id INTEGER NOT NULL,
hour_start INTEGER NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(peer_id, hour_start)
)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_hourly_hour_start ON traffic_aggregate_hourly(hour_start)`,
`CREATE TABLE IF NOT EXISTS traffic_aggregate_daily (
peer_id INTEGER NOT NULL,
day_start INTEGER NOT NULL,
rx_bytes INTEGER NOT NULL DEFAULT 0,
tx_bytes INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(peer_id, day_start)
)`,
`CREATE INDEX IF NOT EXISTS idx_traffic_daily_day_start ON traffic_aggregate_daily(day_start)`,
}
for _, stmt := range stmts {
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
return errors.New("sqlite migration exec err")
}
}
return nil
}
func migrateMetricSampleTable() error {
stmts := []string{
`CREATE TABLE IF NOT EXISTS metric_sample (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sampled_at INTEGER NOT NULL,
cpu_percent REAL NOT NULL DEFAULT 0,
load1 REAL NOT NULL DEFAULT 0,
mem_used_bytes INTEGER NOT NULL DEFAULT 0,
mem_total_bytes INTEGER NOT NULL DEFAULT 0,
mem_percent REAL NOT NULL DEFAULT 0,
disk_path TEXT NOT NULL DEFAULT '/',
disk_used_bytes INTEGER NOT NULL DEFAULT 0,
disk_total_bytes INTEGER NOT NULL DEFAULT 0,
disk_percent REAL NOT NULL DEFAULT 0,
hysteria_running INTEGER NOT NULL DEFAULT 0,
online_peers INTEGER NOT NULL DEFAULT 0,
online_devices INTEGER NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_metric_sample_time ON metric_sample(sampled_at)`,
}
for _, stmt := range stmts {
if tx := sqliteDB.Exec(stmt); tx.Error != nil {
logrus.Errorf("sqlite migration exec err: %v", tx.Error)
return errors.New("sqlite migration exec err")
}
}
return nil
}
func tableExists(tableName string) bool {
var count int64
if tx := sqliteDB.Raw("SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count); tx.Error != nil {
return false
}
return count > 0
}
func listSQLMigrationFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
files := make([]string, 0)
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
if strings.HasSuffix(strings.ToLower(name), ".sql") {
files = append(files, filepath.Join(dir, name))
}
}
sort.Strings(files)
return files, nil
}
func ensureTrafficStatsSecret() error {
envSecret := strings.TrimSpace(os.Getenv("HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET"))
if envSecret != "" {
@@ -194,23 +496,6 @@ func ensureTrafficStatsSecret() error {
return nil
}
func sqliteInit(sqlStr string) error {
if sqliteDB != nil {
sqls := strings.Split(strings.Replace(sqlStr, "\r\n", "\n", -1), ";\n")
for _, s := range sqls {
s = strings.TrimSpace(s)
if s != "" {
tx := sqliteDB.Exec(s)
if tx.Error != nil && !strings.HasPrefix(tx.Error.Error(), "SQL logic error: duplicate column name") {
logrus.Errorf("sqlite exec err: %v", tx.Error)
return errors.New("sqlite exec err")
}
}
}
}
return nil
}
func CloseSqliteDB() error {
if sqliteDB != nil {
db, err := sqliteDB.DB()
@@ -253,3 +538,4 @@ func Paginate(pageNum *int64, pageSize *int64) func(db *gorm.DB) *gorm.DB {
return db.Offset(int((num - 1) * size)).Limit(int(size))
}
}
+75
View File
@@ -0,0 +1,75 @@
package dao
import (
"errors"
"fmt"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hy2xs-admin/model/constant"
"hy2xs-admin/model/entity"
"time"
)
func SaveTrafficSample(sample entity.TrafficSample) error {
if tx := sqliteDB.Save(&sample); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func UpsertTrafficAggregateHourly(peerId int64, hourStart int64, rxBytes int64, txBytes int64) error {
if rxBytes == 0 && txBytes == 0 {
return nil
}
agg := entity.TrafficAggregateHourly{
PeerId: &peerId,
HourStart: &hourStart,
RxBytes: &rxBytes,
TxBytes: &txBytes,
}
now := time.Now()
if tx := sqliteDB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "peer_id"}, {Name: "hour_start"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"rx_bytes": gormExprAdd("rx_bytes", rxBytes),
"tx_bytes": gormExprAdd("tx_bytes", txBytes),
"update_time": now,
}),
}).Create(&agg); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func UpsertTrafficAggregateDaily(peerId int64, dayStart int64, rxBytes int64, txBytes int64) error {
if rxBytes == 0 && txBytes == 0 {
return nil
}
agg := entity.TrafficAggregateDaily{
PeerId: &peerId,
DayStart: &dayStart,
RxBytes: &rxBytes,
TxBytes: &txBytes,
}
now := time.Now()
if tx := sqliteDB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "peer_id"}, {Name: "day_start"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"rx_bytes": gormExprAdd("rx_bytes", rxBytes),
"tx_bytes": gormExprAdd("tx_bytes", txBytes),
"update_time": now,
}),
}).Create(&agg); tx.Error != nil {
logrus.Errorf("%v", tx.Error)
return errors.New(constant.SysError)
}
return nil
}
func gormExprAdd(column string, delta int64) interface{} {
return gorm.Expr(fmt.Sprintf("%s + ?", column), delta)
}