refactor: 更新项目引用路径从perfect-panel/ppanel-server到perfect-panel/server
Build docker and publish / build (20.15.1) (push) Failing after 6m27s

feat: 添加版本和构建时间变量
fix: 修正短信队列类型注释错误
style: 清理未使用的代码和测试文件
docs: 更新安装文档中的下载链接
chore: 迁移数据库脚本添加日志和订阅配置
This commit is contained in:
2025-10-13 01:33:03 -07:00
parent 393b42f35a
commit c582087c0f
974 changed files with 23609 additions and 23398 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
-54
View File
@@ -1,54 +0,0 @@
package application
import (
"time"
)
type Application struct {
Id int64 `gorm:"primary_key"`
Name string `gorm:"type:varchar(255);default:'';not null;comment:应用名称"`
Icon string `gorm:"type:text;not null;comment:应用图标"`
Description string `gorm:"type:text;comment:更新描述"`
SubscribeType string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"`
ApplicationVersions []ApplicationVersion
CreatedAt time.Time `gorm:"<-:create;comment:创建时间"`
UpdatedAt time.Time `gorm:"comment:更新时间"`
}
func (Application) TableName() string {
return "application"
}
type ApplicationVersion struct {
Id int64 `gorm:"primary_key"`
Url string `gorm:"type:varchar(255);default:'';not null;comment:应用地址"`
Version string `gorm:"type:varchar(255);default:'';not null;comment:应用版本"`
Platform string `gorm:"type:varchar(50);default:'';not null;comment:应用平台"`
IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:默认版本"`
Description string `gorm:"type:text;comment:更新描述"`
ApplicationId int64 `gorm:"comment:所属应用"`
CreatedAt time.Time `gorm:"<-:create;comment:创建时间"`
UpdatedAt time.Time `gorm:"comment:更新时间"`
}
func (ApplicationVersion) TableName() string {
return "application_version"
}
type ApplicationConfig struct {
Id int64 `gorm:"primary_key"`
AppId int64 `gorm:"type:int;not null;default:0;comment:App id"`
EncryptionKey string `gorm:"type:text;comment:Encryption Key"`
EncryptionMethod string `gorm:"type:varchar(255);comment:Encryption Method"`
Domains string `gorm:"type:text"`
StartupPicture string `gorm:"type:text"`
StartupPictureSkipTime int64 `gorm:"type:int;not null;default:0;comment:Startup Picture Skip Time"`
InvitationLink string `gorm:"Invitation link"`
KrWebsiteId string `gorm:"type:varchar(255);default:'';comment:Kr Website ID"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (ApplicationConfig) TableName() string {
return "application_config"
}
-245
View File
@@ -1,245 +0,0 @@
package application
import (
"context"
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
var _ Model = (*customApplicationModel)(nil)
var (
cacheApplicationIdPrefix = "cache:application:id:"
cacheApplicationConfigIdPrefix = "cache:application:config:id:"
cacheApplicationVersionIdPrefix = "cache:application:version:id:"
)
type (
Model interface {
applicationModel
customApplicationLogicModel
}
applicationModel interface {
Insert(ctx context.Context, data *Application) error
FindOne(ctx context.Context, id int64) (*Application, error)
Update(ctx context.Context, data *Application) error
Delete(ctx context.Context, id int64) error
InsertVersion(ctx context.Context, data *ApplicationVersion) error
FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error)
UpdateVersion(ctx context.Context, data *ApplicationVersion) error
InsertConfig(ctx context.Context, data *ApplicationConfig) error
FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error)
UpdateConfig(ctx context.Context, data *ApplicationConfig) error
DeleteVersion(ctx context.Context, id int64) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
customApplicationModel struct {
*defaultApplicationModel
}
defaultApplicationModel struct {
cache.CachedConn
table string
}
)
func newApplicationModel(db *gorm.DB, c *redis.Client) *defaultApplicationModel {
return &defaultApplicationModel{
CachedConn: cache.NewConn(db, c),
table: "`Application`",
}
}
func (m *defaultApplicationModel) getCacheKeys(data *Application) []string {
if data == nil {
return []string{}
}
ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, data.Id)
cacheKeys := []string{
ApplicationIdKey,
config.ApplicationKey,
}
return cacheKeys
}
func (m *defaultApplicationModel) Insert(ctx context.Context, data *Application) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Create(&data).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultApplicationModel) FindOne(ctx context.Context, id int64) (*Application, error) {
ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, id)
var resp Application
err := m.QueryCtx(ctx, &resp, ApplicationIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Application{}).Preload("ApplicationVersions").Where("`id` = ?", id).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultApplicationModel) Update(ctx context.Context, data *Application) error {
old, err := m.FindOne(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Save(data).Error
}, m.getCacheKeys(old)...)
return err
}
func (m *defaultApplicationModel) Delete(ctx context.Context, id int64) error {
data, err := m.FindOne(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
err = db.Where("application_id = ?", id).Delete(&ApplicationVersion{}).Error
if err != nil {
return err
}
return db.Delete(&Application{}, id).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultApplicationModel) getVersionCacheKeys(data *ApplicationVersion) []string {
if data == nil {
return []string{}
}
ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, data.Id)
cacheKeys := []string{
ApplicationVersionIdKey,
config.ApplicationKey,
}
return cacheKeys
}
func (m *defaultApplicationModel) getConfigCacheKeys(data *ApplicationConfig) []string {
if data == nil {
return []string{}
}
ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, data.Id)
cacheKeys := []string{
ApplicationConfigIdKey,
config.ApplicationKey,
}
return cacheKeys
}
func (m *defaultApplicationModel) InsertVersion(ctx context.Context, data *ApplicationVersion) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Transaction(func(tx *gorm.DB) error {
if data.IsDefault {
err := tx.Model(&ApplicationVersion{}).
Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault).
Updates(map[string]interface{}{"default_version": false}).Error
if err != nil {
return err
}
}
return tx.Create(&data).Error
})
}, m.getVersionCacheKeys(data)...)
return err
}
func (m *defaultApplicationModel) FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error) {
ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, id)
var resp ApplicationVersion
err := m.QueryCtx(ctx, &resp, ApplicationVersionIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&ApplicationVersion{}).Where("`id` = ?", id).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultApplicationModel) UpdateVersion(ctx context.Context, data *ApplicationVersion) error {
old, err := m.FindOneVersion(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Transaction(func(tx *gorm.DB) error {
if data.IsDefault {
err := tx.Model(&ApplicationVersion{}).
Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault).
Updates(map[string]interface{}{"default_version": false}).Error
if err != nil {
return err
}
}
return tx.Save(data).Error
})
}, m.getVersionCacheKeys(old)...)
return err
}
func (m *defaultApplicationModel) InsertConfig(ctx context.Context, data *ApplicationConfig) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Create(&data).Error
}, m.getConfigCacheKeys(data)...)
return err
}
func (m *defaultApplicationModel) FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error) {
ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, id)
var resp ApplicationConfig
err := m.QueryCtx(ctx, &resp, ApplicationConfigIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&ApplicationConfig{}).Where("`id` = ?", id).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultApplicationModel) UpdateConfig(ctx context.Context, data *ApplicationConfig) error {
old, err := m.FindOneConfig(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Save(data).Error
}, m.getConfigCacheKeys(old)...)
return err
}
func (m *defaultApplicationModel) DeleteVersion(ctx context.Context, id int64) error {
data, err := m.FindOneVersion(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Delete(&ApplicationVersion{}, id).Error
}, m.getVersionCacheKeys(data)...)
return err
}
func (m *defaultApplicationModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
return m.TransactCtx(ctx, fn)
}
-16
View File
@@ -1,16 +0,0 @@
package application
import (
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type customApplicationLogicModel interface {
}
// NewModel returns a model for the database table.
func NewModel(conn *gorm.DB, c *redis.Client) Model {
return &customApplicationModel{
defaultApplicationModel: newApplicationModel(conn, c),
}
}
+63 -6
View File
@@ -3,6 +3,8 @@ package auth
import (
"encoding/json"
"time"
"github.com/perfect-panel/server/pkg/email"
)
type Auth struct {
@@ -124,15 +126,55 @@ type EmailAuthConfig struct {
}
func (l *EmailAuthConfig) Marshal() string {
if l.ExpirationEmailTemplate == "" {
l.ExpirationEmailTemplate = email.DefaultExpirationEmailTemplate
}
if l.ExpirationEmailTemplate == "" {
l.MaintenanceEmailTemplate = email.DefaultMaintenanceEmailTemplate
}
if l.TrafficExceedEmailTemplate == "" {
l.TrafficExceedEmailTemplate = email.DefaultTrafficExceedEmailTemplate
}
if l.VerifyEmailTemplate == "" {
l.VerifyEmailTemplate = email.DefaultEmailVerifyTemplate
}
bytes, err := json.Marshal(l)
if err != nil {
bytes, _ = json.Marshal(new(EmailAuthConfig))
config := &EmailAuthConfig{
Platform: "smtp",
PlatformConfig: new(SMTPConfig),
EnableVerify: true,
EnableNotify: true,
EnableDomainSuffix: false,
DomainSuffixList: "",
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
}
bytes, _ = json.Marshal(config)
}
return string(bytes)
}
func (l *EmailAuthConfig) Unmarshal(data string) error {
return json.Unmarshal([]byte(data), &l)
func (l *EmailAuthConfig) Unmarshal(data string) {
err := json.Unmarshal([]byte(data), &l)
if err != nil {
config := &EmailAuthConfig{
Platform: "smtp",
PlatformConfig: new(SMTPConfig),
EnableVerify: true,
EnableNotify: true,
EnableDomainSuffix: false,
DomainSuffixList: "",
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
}
_ = json.Unmarshal([]byte(config.Marshal()), &l)
}
}
// SMTPConfig Email SMTP configuration
@@ -167,13 +209,28 @@ type MobileAuthConfig struct {
func (l *MobileAuthConfig) Marshal() string {
bytes, err := json.Marshal(l)
if err != nil {
bytes, _ = json.Marshal(new(MobileAuthConfig))
config := &MobileAuthConfig{
Platform: "alibaba_cloud",
PlatformConfig: new(AlibabaCloudConfig),
EnableWhitelist: false,
Whitelist: []string{},
}
bytes, _ = json.Marshal(config)
}
return string(bytes)
}
func (l *MobileAuthConfig) Unmarshal(data string) error {
return json.Unmarshal([]byte(data), &l)
func (l *MobileAuthConfig) Unmarshal(data string) {
err := json.Unmarshal([]byte(data), &l)
if err != nil {
config := &MobileAuthConfig{
Platform: "alibaba_cloud",
PlatformConfig: new(AlibabaCloudConfig),
EnableWhitelist: false,
Whitelist: []string{},
}
_ = json.Unmarshal([]byte(config.Marshal()), &l)
}
}
type AlibabaCloudConfig struct {
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
-52
View File
@@ -1,52 +0,0 @@
package cache
const (
// UserTodayUploadTrafficCacheKey 用户当日上传流量
UserTodayUploadTrafficCacheKey = "node:user_today_upload_traffic"
// UserTodayDownloadTrafficCacheKey 用户当日下载流量
UserTodayDownloadTrafficCacheKey = "node:user_today_download_traffic"
// UserTodayTotalTrafficCacheKey 用户当日总流量
UserTodayTotalTrafficCacheKey = "node:user_today_total_traffic"
// NodeTodayUploadTrafficCacheKey 节点当日上传流量
NodeTodayUploadTrafficCacheKey = "node:node_today_upload_traffic"
// NodeTodayDownloadTrafficCacheKey 节点当日下载流量
NodeTodayDownloadTrafficCacheKey = "node:node_today_download_traffic"
// NodeTodayTotalTrafficCacheKey 节点当日总流量
NodeTodayTotalTrafficCacheKey = "node:node_today_total_traffic"
// UserTodayUploadTrafficRankKey 用户当日上传流量排行榜
UserTodayUploadTrafficRankKey = "node:user_today_upload_traffic_rank"
// UserTodayDownloadTrafficRankKey 用户当日下载流量排行榜
UserTodayDownloadTrafficRankKey = "node:user_today_download_traffic_rank"
// UserTodayTotalTrafficRankKey 用户当日总流量排行榜
UserTodayTotalTrafficRankKey = "node:user_today_total_traffic_rank"
// NodeTodayUploadTrafficRankKey 节点当日上传流量排行榜
NodeTodayUploadTrafficRankKey = "node:node_today_upload_traffic_rank"
// NodeTodayDownloadTrafficRankKey 节点当日下载流量排行榜
NodeTodayDownloadTrafficRankKey = "node:node_today_download_traffic_rank"
// NodeTodayTotalTrafficRankKey 节点当日总流量排行榜
NodeTodayTotalTrafficRankKey = "node:node_today_total_traffic_rank"
// NodeOnlineUserCacheKey 节点在线用户
NodeOnlineUserCacheKey = "node:node_online_user:%d"
// UserOnlineIpCacheKey 用户在线IP
UserOnlineIpCacheKey = "node:user_online_ip:%d"
// AllNodeOnlineUserCacheKey 所有节点在线用户
AllNodeOnlineUserCacheKey = "node:all_node_online_user"
// NodeStatusCacheKey 节点状态
NodeStatusCacheKey = "node:status:%d"
// AllNodeDownloadTrafficCacheKey 所有节点下载流量
AllNodeDownloadTrafficCacheKey = "node:all_node_download_traffic"
// AllNodeUploadTrafficCacheKey 所有节点上传流量
AllNodeUploadTrafficCacheKey = "node:all_node_upload_traffic"
// YesterdayTotalTrafficRank 昨日节点总流量排行榜
YesterdayNodeTotalTrafficRank = "node:yesterday_total_traffic_rank"
// YesterdayUploadTrafficRank 昨日节点上传流量排行榜
YesterdayNodeUploadTrafficRank = "node:yesterday_upload_traffic_rank"
// YesterdayDownloadTrafficRank 昨日节点下载流量排行榜
YesterdayNodeDownloadTrafficRank = "node:yesterday_download_traffic_rank"
// YesterdayUserTotalTrafficRank 昨日用户总流量排行榜
YesterdayUserTotalTrafficRank = "node:yesterday_user_total_traffic_rank"
// YesterdayUserUploadTrafficRank 昨日用户上传流量排行榜
YesterdayUserUploadTrafficRank = "node:yesterday_user_upload_traffic_rank"
// YesterdayUserDownloadTrafficRank 昨日用户下载流量排行榜
YesterdayUserDownloadTrafficRank = "node:yesterday_user_download_traffic_rank"
)
-584
View File
@@ -1,584 +0,0 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"strconv"
"sync"
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/redis/go-redis/v9"
)
type NodeCacheClient struct {
*redis.Client
resetMutex sync.Mutex
}
func NewNodeCacheClient(rds *redis.Client) *NodeCacheClient {
return &NodeCacheClient{
Client: rds,
}
}
// AddOnlineUserIP adds user's online IP
func (c *NodeCacheClient) AddOnlineUserIP(ctx context.Context, users []NodeOnlineUser) error {
if len(users) == 0 {
// No users to add
return nil
}
// Use Pipeline to optimize Redis operations
pipe := c.Pipeline()
// Add user online IPs and clean up expired IPs for each user
for _, user := range users {
if user.SID <= 0 || user.IP == "" {
logger.Errorf("invalid user data: uid=%d, ip=%s", user.SID, user.IP)
continue
}
key := fmt.Sprintf(UserOnlineIpCacheKey, user.SID)
now := time.Now()
expireTime := now.Add(5 * time.Minute)
// Clean up expired user online IPs
pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", now.Unix()))
pipe.ZRemRangeByScore(ctx, AllNodeOnlineUserCacheKey, "0", fmt.Sprintf("%d", now.Unix()))
// Add or update user online IP
// XX: Only update elements that already exist
// NX: Only add new elements
_ = pipe.ZAdd(ctx, key, redis.Z{
Score: float64(expireTime.Unix()),
Member: user.IP,
}).Err()
_ = pipe.ZAdd(ctx, AllNodeOnlineUserCacheKey, redis.Z{
Score: float64(expireTime.Unix()),
Member: user.IP,
}).Err()
// Set key expiration to 5 minutes (slightly longer than IP expiration)
pipe.Expire(ctx, key, 5*time.Minute)
pipe.Expire(ctx, AllNodeOnlineUserCacheKey, 5*time.Minute)
}
// Execute all commands
_, err := pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to add node user online ip: %w", err)
}
return nil
}
// GetUserOnlineIp gets user's online IPs
func (c *NodeCacheClient) GetUserOnlineIp(ctx context.Context, uid int64) ([]string, error) {
if uid <= 0 {
return nil, fmt.Errorf("invalid parameters: uid=%d", uid)
}
// Get user's online IPs
ips, err := c.ZRevRangeByScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid), &redis.ZRangeBy{
Min: "0",
Max: fmt.Sprintf("%d", time.Now().Add(5*time.Minute).Unix()),
Offset: 0,
Count: 100,
}).Result()
if err != nil {
return nil, fmt.Errorf("failed to get user online ip: %w", err)
}
return ips, nil
}
// UpdateNodeOnlineUser updates node's online users and IPs
func (c *NodeCacheClient) UpdateNodeOnlineUser(ctx context.Context, nodeId int64, users []NodeOnlineUser) error {
if nodeId <= 0 || len(users) == 0 {
return fmt.Errorf("invalid parameters: nodeId=%d, users=%v", nodeId, users)
}
// Organize data
data := make(map[int64][]string)
for _, user := range users {
data[user.SID] = append(data[user.SID], user.IP)
}
value, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
c.Set(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId), value, time.Minute*5)
return nil
}
// GetNodeOnlineUser gets node's online users and IPs
func (c *NodeCacheClient) GetNodeOnlineUser(ctx context.Context, nodeId int64) (map[int64][]string, error) {
if nodeId <= 0 {
return nil, fmt.Errorf("invalid parameters: nodeId=%d", nodeId)
}
value, err := c.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Result()
if err != nil {
return nil, fmt.Errorf("failed to get node online user: %w", err)
}
var data map[int64][]string
if err := json.Unmarshal([]byte(value), &data); err != nil {
return nil, fmt.Errorf("failed to unmarshal data: %w", err)
}
return data, nil
}
// AddUserTodayTraffic Add user's today traffic
func (c *NodeCacheClient) AddUserTodayTraffic(ctx context.Context, uid int64, upload, download int64) error {
if uid <= 0 || upload <= 0 {
return fmt.Errorf("invalid parameters: uid=%d, upload=%d", uid, upload)
}
pipe := c.Pipeline()
// User's today upload traffic
pipe.HIncrBy(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid), upload)
// User's today download traffic
pipe.HIncrBy(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid), download)
// User's today total traffic
pipe.HIncrBy(ctx, UserTodayTotalTrafficCacheKey, fmt.Sprintf("%d", uid), upload+download)
// User's today traffic ranking
pipe.ZIncrBy(ctx, UserTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", uid))
pipe.ZIncrBy(ctx, UserTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", uid))
pipe.ZIncrBy(ctx, UserTodayTotalTrafficRankKey, float64(upload+download), fmt.Sprintf("%d", uid))
// All node upload traffic
pipe.IncrBy(ctx, AllNodeUploadTrafficCacheKey, upload)
// All node download traffic
pipe.IncrBy(ctx, AllNodeDownloadTrafficCacheKey, download)
// Execute commands
_, err := pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to add user today upload traffic: %w", err)
}
return nil
}
// AddNodeTodayTraffic Add node's today traffic
func (c *NodeCacheClient) AddNodeTodayTraffic(ctx context.Context, nodeId int64, userTraffic []UserTraffic) error {
if nodeId <= 0 || len(userTraffic) == 0 {
return fmt.Errorf("invalid parameters: nodeId=%d, userTraffic=%v", nodeId, userTraffic)
}
pipe := c.Pipeline()
upload, download, total := c.calculateTraffic(userTraffic)
pipe.HIncrBy(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId), upload)
pipe.HIncrBy(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId), download)
pipe.HIncrBy(ctx, NodeTodayTotalTrafficCacheKey, fmt.Sprintf("%d", nodeId), total)
pipe.ZIncrBy(ctx, NodeTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", nodeId))
pipe.ZIncrBy(ctx, NodeTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", nodeId))
pipe.ZIncrBy(ctx, NodeTodayTotalTrafficRankKey, float64(total), fmt.Sprintf("%d", nodeId))
// Execute commands
_, err := pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to add node today upload traffic: %w", err)
}
return nil
}
// Get user's traffic data
func (c *NodeCacheClient) getUserTrafficData(ctx context.Context, uid int64) (upload, download int64, err error) {
upload, err = c.HGet(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64()
if err != nil {
return 0, 0, fmt.Errorf("failed to get user today upload traffic: %w", err)
}
download, err = c.HGet(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64()
if err != nil {
return 0, 0, fmt.Errorf("failed to get user today download traffic: %w", err)
}
return upload, download, nil
}
// Get node's traffic data
func (c *NodeCacheClient) getNodeTrafficData(ctx context.Context, nodeId int64) (upload, download int64, err error) {
upload, err = c.HGet(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64()
if err != nil {
return 0, 0, fmt.Errorf("failed to get node today upload traffic: %w", err)
}
download, err = c.HGet(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64()
if err != nil {
return 0, 0, fmt.Errorf("failed to get node today download traffic: %w", err)
}
return upload, download, nil
}
// Parse ID
func (c *NodeCacheClient) parseID(member interface{}, idType string) (int64, error) {
id, err := strconv.ParseInt(member.(string), 10, 64)
if err != nil {
return 0, fmt.Errorf("failed to parse %s id %v: %w", idType, member, err)
}
return id, nil
}
// GetUserTodayTotalTrafficRank Get user's today total traffic ranking top N
func (c *NodeCacheClient) GetUserTodayTotalTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) {
if n <= 0 {
return nil, fmt.Errorf("invalid parameters: n=%d", n)
}
data, err := c.ZRevRangeWithScores(ctx, UserTodayTotalTrafficRankKey, 0, n-1).Result()
if err != nil {
return nil, fmt.Errorf("failed to get user today total traffic rank: %w", err)
}
users := make([]UserTodayTrafficRank, 0, len(data))
for _, user := range data {
uid, err := c.parseID(user.Member, "user")
if err != nil {
logger.Errorf("%v", err)
continue
}
upload, download, err := c.getUserTrafficData(ctx, uid)
if err != nil {
logger.Errorf("%v", err)
continue
}
users = append(users, UserTodayTrafficRank{
SID: uid,
Upload: upload,
Download: download,
Total: int64(user.Score),
})
}
return users, nil
}
// GetNodeTodayTotalTrafficRank Get node's today total traffic ranking top N
func (c *NodeCacheClient) GetNodeTodayTotalTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) {
if n <= 0 {
return nil, fmt.Errorf("invalid parameters: n=%d", n)
}
data, err := c.ZRevRangeWithScores(ctx, NodeTodayTotalTrafficRankKey, 0, n-1).Result()
if err != nil {
return nil, fmt.Errorf("failed to get node today total traffic rank: %w", err)
}
nodes := make([]NodeTodayTrafficRank, 0, len(data))
for _, node := range data {
nodeId, err := c.parseID(node.Member, "node")
if err != nil {
logger.Errorf("%v", err)
continue
}
upload, download, err := c.getNodeTrafficData(ctx, nodeId)
if err != nil {
logger.Errorf("%v", err)
continue
}
nodes = append(nodes, NodeTodayTrafficRank{
ID: nodeId,
Upload: upload,
Download: download,
Total: int64(node.Score),
})
}
return nodes, nil
}
// GetUserTodayUploadTrafficRank Get user's today upload traffic ranking top N
func (c *NodeCacheClient) GetUserTodayUploadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) {
if n <= 0 {
return nil, fmt.Errorf("invalid parameters: n=%d", n)
}
data, err := c.ZRevRangeWithScores(ctx, UserTodayUploadTrafficRankKey, 0, n-1).Result()
if err != nil {
return nil, fmt.Errorf("failed to get user today upload traffic rank: %w", err)
}
users := make([]UserTodayTrafficRank, 0, len(data))
for _, user := range data {
uid, err := c.parseID(user.Member, "user")
if err != nil {
logger.Errorf("%v", err)
continue
}
upload, download, err := c.getUserTrafficData(ctx, uid)
if err != nil {
logger.Errorf("%v", err)
continue
}
users = append(users, UserTodayTrafficRank{
SID: uid,
Upload: upload,
Download: download,
Total: int64(user.Score),
})
}
return users, nil
}
// GetUserTodayDownloadTrafficRank Get user's today download traffic ranking top N
func (c *NodeCacheClient) GetUserTodayDownloadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) {
if n <= 0 {
return nil, fmt.Errorf("invalid parameters: n=%d", n)
}
data, err := c.ZRevRangeWithScores(ctx, UserTodayDownloadTrafficRankKey, 0, n-1).Result()
if err != nil {
return nil, fmt.Errorf("failed to get user today download traffic rank: %w", err)
}
users := make([]UserTodayTrafficRank, 0, len(data))
for _, user := range data {
uid, err := c.parseID(user.Member, "user")
if err != nil {
logger.Errorf("%v", err)
continue
}
upload, download, err := c.getUserTrafficData(ctx, uid)
if err != nil {
logger.Errorf("%v", err)
continue
}
users = append(users, UserTodayTrafficRank{
SID: uid,
Upload: upload,
Download: download,
Total: int64(user.Score),
})
}
return users, nil
}
// GetNodeTodayUploadTrafficRank Get node's today upload traffic ranking top N
func (c *NodeCacheClient) GetNodeTodayUploadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) {
if n <= 0 {
return nil, fmt.Errorf("invalid parameters: n=%d", n)
}
data, err := c.ZRevRangeWithScores(ctx, NodeTodayUploadTrafficRankKey, 0, n-1).Result()
if err != nil {
return nil, fmt.Errorf("failed to get node today upload traffic rank: %w", err)
}
nodes := make([]NodeTodayTrafficRank, 0, len(data))
for _, node := range data {
nodeId, err := c.parseID(node.Member, "node")
if err != nil {
logger.Errorf("%v", err)
continue
}
upload, download, err := c.getNodeTrafficData(ctx, nodeId)
if err != nil {
logger.Errorf("%v", err)
continue
}
nodes = append(nodes, NodeTodayTrafficRank{
ID: nodeId,
Upload: upload,
Download: download,
Total: int64(node.Score),
})
}
return nodes, nil
}
// GetNodeTodayDownloadTrafficRank Get node's today download traffic ranking top N
func (c *NodeCacheClient) GetNodeTodayDownloadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) {
if n <= 0 {
return nil, fmt.Errorf("invalid parameters: n=%d", n)
}
data, err := c.ZRevRangeWithScores(ctx, NodeTodayDownloadTrafficRankKey, 0, n-1).Result()
if err != nil {
return nil, fmt.Errorf("failed to get node today download traffic rank: %w", err)
}
nodes := make([]NodeTodayTrafficRank, 0, len(data))
for _, node := range data {
nodeId, err := c.parseID(node.Member, "node")
if err != nil {
logger.Errorf("%v", err)
continue
}
upload, download, err := c.getNodeTrafficData(ctx, nodeId)
if err != nil {
logger.Errorf("%v", err)
continue
}
nodes = append(nodes, NodeTodayTrafficRank{
ID: nodeId,
Upload: upload,
Download: download,
Total: int64(node.Score),
})
}
return nodes, nil
}
// ResetTodayTrafficData Reset today's traffic data
func (c *NodeCacheClient) ResetTodayTrafficData(ctx context.Context) error {
c.resetMutex.Lock()
defer c.resetMutex.Unlock()
pipe := c.Pipeline()
pipe.Del(ctx, UserTodayUploadTrafficCacheKey)
pipe.Del(ctx, UserTodayDownloadTrafficCacheKey)
pipe.Del(ctx, UserTodayTotalTrafficCacheKey)
pipe.Del(ctx, NodeTodayUploadTrafficCacheKey)
pipe.Del(ctx, NodeTodayDownloadTrafficCacheKey)
pipe.Del(ctx, NodeTodayTotalTrafficCacheKey)
pipe.Del(ctx, UserTodayUploadTrafficRankKey)
pipe.Del(ctx, UserTodayDownloadTrafficRankKey)
pipe.Del(ctx, UserTodayTotalTrafficRankKey)
pipe.Del(ctx, NodeTodayUploadTrafficRankKey)
pipe.Del(ctx, NodeTodayDownloadTrafficRankKey)
pipe.Del(ctx, NodeTodayTotalTrafficRankKey)
pipe.Del(ctx, AllNodeDownloadTrafficCacheKey)
pipe.Del(ctx, AllNodeUploadTrafficCacheKey)
_, err := pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to reset today traffic data: %w", err)
}
return nil
}
// Calculate traffic
func (c *NodeCacheClient) calculateTraffic(data []UserTraffic) (upload, download, total int64) {
for _, userTraffic := range data {
upload += userTraffic.Upload
download += userTraffic.Download
total += userTraffic.Upload + userTraffic.Download
}
return upload, download, total
}
// GetAllNodeOnlineUser Get all node online user
func (c *NodeCacheClient) GetAllNodeOnlineUser(ctx context.Context) ([]string, error) {
users, err := c.ZRevRange(ctx, AllNodeOnlineUserCacheKey, 0, -1).Result()
if err != nil {
return nil, fmt.Errorf("failed to get all node online user: %w", err)
}
return users, nil
}
// UpdateNodeStatus Update node status
func (c *NodeCacheClient) UpdateNodeStatus(ctx context.Context, nodeId int64, status NodeStatus) error {
// 参数验证
if nodeId <= 0 {
return fmt.Errorf("invalid node id: %d", nodeId)
}
// 验证状态数据
if status.UpdatedAt <= 0 {
return fmt.Errorf("invalid status data: updated_at=%d", status.UpdatedAt)
}
// 序列化状态数据
value, err := json.Marshal(status)
if err != nil {
return fmt.Errorf("failed to marshal node status: %w", err)
}
// 使用 Pipeline 优化性能
pipe := c.Pipeline()
// 设置状态数据
pipe.Set(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId), value, time.Minute*5)
// 执行命令
_, err = pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to update node status: %w", err)
}
return nil
}
// GetNodeStatus Get node status
func (c *NodeCacheClient) GetNodeStatus(ctx context.Context, nodeId int64) (NodeStatus, error) {
status, err := c.Get(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId)).Result()
if err != nil {
return NodeStatus{}, fmt.Errorf("failed to get node status: %w", err)
}
var nodeStatus NodeStatus
if err := json.Unmarshal([]byte(status), &nodeStatus); err != nil {
return NodeStatus{}, fmt.Errorf("failed to unmarshal node status: %w", err)
}
return nodeStatus, nil
}
// GetOnlineNodeStatusCount Get Online Node Status Count
func (c *NodeCacheClient) GetOnlineNodeStatusCount(ctx context.Context) (int64, error) {
// 获取所有节点Key
keys, err := c.Keys(ctx, "node:status:*").Result()
if err != nil {
return 0, fmt.Errorf("failed to get all node status keys: %w", err)
}
var count int64
for _, key := range keys {
status, err := c.Get(ctx, key).Result()
if err != nil {
logger.Errorf("failed to get node status: %v", err.Error())
continue
}
if status != "" {
count++
}
}
return count, nil
}
// GetAllNodeUploadTraffic Get all node upload traffic
func (c *NodeCacheClient) GetAllNodeUploadTraffic(ctx context.Context) (int64, error) {
upload, err := c.Get(ctx, AllNodeUploadTrafficCacheKey).Int64()
if err != nil {
return 0, fmt.Errorf("failed to get all node upload traffic: %w", err)
}
return upload, nil
}
// GetAllNodeDownloadTraffic Get all node download traffic
func (c *NodeCacheClient) GetAllNodeDownloadTraffic(ctx context.Context) (int64, error) {
download, err := c.Get(ctx, AllNodeDownloadTrafficCacheKey).Int64()
if err != nil {
return 0, fmt.Errorf("failed to get all node download traffic: %w", err)
}
return download, nil
}
// UpdateYesterdayNodeTotalTrafficRank Update yesterday node total traffic rank
func (c *NodeCacheClient) UpdateYesterdayNodeTotalTrafficRank(ctx context.Context, nodes []NodeTodayTrafficRank) error {
expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24)
t := time.Until(expireAt)
pipe := c.Pipeline()
value, _ := json.Marshal(nodes)
pipe.Set(ctx, YesterdayNodeTotalTrafficRank, value, t)
_, err := pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to update yesterday node total traffic rank: %w", err)
}
return nil
}
// UpdateYesterdayUserTotalTrafficRank Update yesterday user total traffic rank
func (c *NodeCacheClient) UpdateYesterdayUserTotalTrafficRank(ctx context.Context, users []UserTodayTrafficRank) error {
expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24)
t := time.Until(expireAt)
pipe := c.Pipeline()
value, _ := json.Marshal(users)
pipe.Set(ctx, YesterdayUserTotalTrafficRank, value, t)
_, err := pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to update yesterday user total traffic rank: %w", err)
}
return nil
}
// GetYesterdayNodeTotalTrafficRank Get yesterday node total traffic rank
func (c *NodeCacheClient) GetYesterdayNodeTotalTrafficRank(ctx context.Context) ([]NodeTodayTrafficRank, error) {
value, err := c.Get(ctx, YesterdayNodeTotalTrafficRank).Result()
if err != nil {
return nil, fmt.Errorf("failed to get yesterday node total traffic rank: %w", err)
}
var nodes []NodeTodayTrafficRank
if err := json.Unmarshal([]byte(value), &nodes); err != nil {
return nil, fmt.Errorf("failed to unmarshal yesterday node total traffic rank: %w", err)
}
return nodes, nil
}
// GetYesterdayUserTotalTrafficRank Get yesterday user total traffic rank
func (c *NodeCacheClient) GetYesterdayUserTotalTrafficRank(ctx context.Context) ([]UserTodayTrafficRank, error) {
value, err := c.Get(ctx, YesterdayUserTotalTrafficRank).Result()
if err != nil {
return nil, fmt.Errorf("failed to get yesterday user total traffic rank: %w", err)
}
var users []UserTodayTrafficRank
if err := json.Unmarshal([]byte(value), &users); err != nil {
return nil, fmt.Errorf("failed to unmarshal yesterday user total traffic rank: %w", err)
}
return users, nil
}
-575
View File
@@ -1,575 +0,0 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Create a test Redis client
func newTestRedisClient(t *testing.T) *redis.Client {
mr, err := miniredis.Run()
require.NoError(t, err)
client := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
require.NoError(t, client.Ping(context.Background()).Err())
return client
}
// Clean up test data
func cleanupTestData(t *testing.T, client *redis.Client) {
ctx := context.Background()
keys := []string{
UserTodayUploadTrafficCacheKey,
UserTodayDownloadTrafficCacheKey,
UserTodayTotalTrafficCacheKey,
NodeTodayUploadTrafficCacheKey,
NodeTodayDownloadTrafficCacheKey,
NodeTodayTotalTrafficCacheKey,
UserTodayUploadTrafficRankKey,
UserTodayDownloadTrafficRankKey,
UserTodayTotalTrafficRankKey,
NodeTodayUploadTrafficRankKey,
NodeTodayDownloadTrafficRankKey,
NodeTodayTotalTrafficRankKey,
}
// Clean up all cache keys
for _, key := range keys {
require.NoError(t, client.Del(ctx, key).Err())
}
// Clean up user online IP cache
for uid := int64(1); uid <= 3; uid++ {
require.NoError(t, client.Del(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid)).Err())
}
// Clean up node online user cache
for nodeId := int64(1); nodeId <= 3; nodeId++ {
require.NoError(t, client.Del(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Err())
}
}
func TestNodeCacheClient_AddUserTodayTraffic(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
tests := []struct {
name string
uid int64
upload int64
download int64
wantErr bool
}{
{
name: "Add traffic normally",
uid: 1,
upload: 100,
download: 200,
wantErr: false,
},
{
name: "Invalid SID",
uid: 0,
upload: 100,
download: 200,
wantErr: true,
},
{
name: "Invalid upload traffic",
uid: 1,
upload: 0,
download: 200,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := cache.AddUserTodayTraffic(ctx, tt.uid, tt.upload, tt.download)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
// Verify data is added correctly
upload, err := client.HGet(ctx, UserTodayUploadTrafficCacheKey, "1").Int64()
assert.NoError(t, err)
assert.Equal(t, tt.upload, upload)
download, err := client.HGet(ctx, UserTodayDownloadTrafficCacheKey, "1").Int64()
assert.NoError(t, err)
assert.Equal(t, tt.download, download)
})
}
}
func TestNodeCacheClient_AddNodeTodayTraffic(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
tests := []struct {
name string
nodeId int64
userTraffic []UserTraffic
wantErr bool
}{
{
name: "Add node traffic normally",
nodeId: 1,
userTraffic: []UserTraffic{
{UID: 1, Upload: 100, Download: 200},
{UID: 2, Upload: 300, Download: 400},
},
wantErr: false,
},
{
name: "Invalid node ID",
nodeId: 0,
userTraffic: []UserTraffic{
{UID: 1, Upload: 100, Download: 200},
},
wantErr: true,
},
{
name: "Empty user traffic data",
nodeId: 1,
userTraffic: []UserTraffic{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := cache.AddNodeTodayTraffic(ctx, tt.nodeId, tt.userTraffic)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
// Verify data is added correctly
upload, err := client.HGet(ctx, NodeTodayUploadTrafficCacheKey, "1").Int64()
assert.NoError(t, err)
assert.Equal(t, int64(400), upload) // 100 + 300
download, err := client.HGet(ctx, NodeTodayDownloadTrafficCacheKey, "1").Int64()
assert.NoError(t, err)
assert.Equal(t, int64(600), download) // 200 + 400
})
}
}
func TestNodeCacheClient_GetUserTodayTrafficRank(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
// Prepare test data
testData := []struct {
uid int64
upload int64
download int64
}{
{1, 100, 200},
{2, 300, 400},
{3, 500, 600},
}
for _, data := range testData {
err := cache.AddUserTodayTraffic(ctx, data.uid, data.upload, data.download)
require.NoError(t, err)
}
tests := []struct {
name string
n int64
wantErr bool
}{
{
name: "Get top 2 ranks",
n: 2,
wantErr: false,
},
{
name: "Get all ranks",
n: 3,
wantErr: false,
},
{
name: "Invalid N value",
n: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ranks, err := cache.GetUserTodayTotalTrafficRank(ctx, tt.n)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Len(t, ranks, int(tt.n))
// Verify sorting is correct
for i := 1; i < len(ranks); i++ {
assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total)
}
})
}
}
func TestNodeCacheClient_ResetTodayTrafficData(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
// Prepare test data
err := cache.AddUserTodayTraffic(ctx, 1, 100, 200)
require.NoError(t, err)
err = cache.AddNodeTodayTraffic(ctx, 1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}})
require.NoError(t, err)
// Test reset functionality
err = cache.ResetTodayTrafficData(ctx)
assert.NoError(t, err)
// Verify data is cleared
keys := []string{
UserTodayUploadTrafficCacheKey,
UserTodayDownloadTrafficCacheKey,
UserTodayTotalTrafficCacheKey,
NodeTodayUploadTrafficCacheKey,
NodeTodayDownloadTrafficCacheKey,
NodeTodayTotalTrafficCacheKey,
}
for _, key := range keys {
exists, err := client.Exists(ctx, key).Result()
assert.NoError(t, err)
assert.Equal(t, int64(0), exists)
}
}
func TestNodeCacheClient_GetNodeTodayTrafficRank(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
// Prepare test data
testData := []struct {
nodeId int64
traffic []UserTraffic
}{
{1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}}},
{2, []UserTraffic{{UID: 2, Upload: 300, Download: 400}}},
{3, []UserTraffic{{UID: 3, Upload: 500, Download: 600}}},
}
for _, data := range testData {
err := cache.AddNodeTodayTraffic(ctx, data.nodeId, data.traffic)
require.NoError(t, err)
}
tests := []struct {
name string
n int64
wantErr bool
}{
{
name: "Get top 2 ranks",
n: 2,
wantErr: false,
},
{
name: "Get all ranks",
n: 3,
wantErr: false,
},
{
name: "Invalid N value",
n: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ranks, err := cache.GetNodeTodayTotalTrafficRank(ctx, tt.n)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Len(t, ranks, int(tt.n))
// Verify sorting is correct
for i := 1; i < len(ranks); i++ {
assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total)
}
})
}
}
func TestNodeCacheClient_AddNodeOnlineUser(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
tests := []struct {
name string
nodeId int64
users []NodeOnlineUser
wantErr bool
}{
{
name: "Add online users normally",
nodeId: 1,
users: []NodeOnlineUser{
{SID: 1, IP: "192.168.1.1"},
{SID: 2, IP: "192.168.1.2"},
},
wantErr: false,
},
{
name: "Invalid node ID",
nodeId: 0,
users: []NodeOnlineUser{
{SID: 1, IP: "192.168.1.1"},
},
wantErr: false,
},
{
name: "Empty user list",
nodeId: 1,
users: []NodeOnlineUser{},
wantErr: false,
},
{
name: "Add duplicate user IP",
nodeId: 1,
users: []NodeOnlineUser{
{SID: 1, IP: "192.168.1.1"},
{SID: 1, IP: "192.168.1.1"},
},
wantErr: false,
},
{
name: "Multiple IPs for same user",
nodeId: 1,
users: []NodeOnlineUser{
{SID: 1, IP: "192.168.1.1"},
{SID: 1, IP: "192.168.1.2"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := cache.AddOnlineUserIP(ctx, tt.users)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
// Verify data is added correctly
for _, user := range tt.users {
// Get user online IPs
ips, err := cache.GetUserOnlineIp(ctx, user.SID)
assert.NoError(t, err)
assert.Contains(t, ips, user.IP)
// Verify score is within valid range (current time to 5 minutes later)
score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID), user.IP).Result()
assert.NoError(t, err)
now := time.Now().Unix()
assert.GreaterOrEqual(t, score, float64(now))
assert.LessOrEqual(t, score, float64(now+300)) // 5 minutes = 300 seconds
// Verify key exists
exists, err := client.Exists(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID)).Result()
assert.NoError(t, err)
assert.Equal(t, int64(1), exists)
}
})
}
}
func TestNodeCacheClient_GetUserOnlineIp(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
// Prepare test data
testData := []struct {
nodeId int64
users []NodeOnlineUser
}{
{
nodeId: 1,
users: []NodeOnlineUser{
{SID: 1, IP: "192.168.1.1"},
{SID: 1, IP: "192.168.1.2"},
{SID: 2, IP: "192.168.1.3"},
},
},
}
// Add test data
for _, data := range testData {
err := cache.AddOnlineUserIP(ctx, data.users)
require.NoError(t, err)
}
tests := []struct {
name string
uid int64
wantErr bool
wantIPs []string
}{
{
name: "Get existing user IPs",
uid: 1,
wantErr: false,
wantIPs: []string{"192.168.1.1", "192.168.1.2"},
},
{
name: "Get another user's IPs",
uid: 2,
wantErr: false,
wantIPs: []string{"192.168.1.3"},
},
{
name: "Get non-existent user IPs",
uid: 3,
wantErr: false,
wantIPs: []string{},
},
{
name: "Invalid user ID",
uid: 0,
wantErr: true,
},
{
name: "Expired IPs should not be returned",
uid: 1,
wantErr: false,
wantIPs: []string{"192.168.1.1", "192.168.1.2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ips, err := cache.GetUserOnlineIp(ctx, tt.uid)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.ElementsMatch(t, tt.wantIPs, ips)
// Verify all returned IPs are valid
for _, ip := range ips {
score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, tt.uid), ip).Result()
assert.NoError(t, err)
now := time.Now().Unix()
assert.GreaterOrEqual(t, score, float64(now))
}
})
}
}
func TestNodeCacheClient_UpdateNodeOnlineUser(t *testing.T) {
client := newTestRedisClient(t)
defer cleanupTestData(t, client)
cache := NewNodeCacheClient(client)
ctx := context.Background()
tests := []struct {
name string
nodeId int64
users []NodeOnlineUser
wantErr bool
}{
{
name: "Update online users normally",
nodeId: 1,
users: []NodeOnlineUser{
{SID: 1, IP: "192.168.1.1"},
{SID: 2, IP: "192.168.1.2"},
},
wantErr: false,
},
{
name: "Invalid node ID",
nodeId: 0,
users: []NodeOnlineUser{
{SID: 1, IP: "192.168.1.1"},
},
wantErr: true,
},
{
name: "Empty user list",
nodeId: 1,
users: []NodeOnlineUser{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := cache.UpdateNodeOnlineUser(ctx, tt.nodeId, tt.users)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
// Verify data is updated correctly
data, err := client.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, tt.nodeId)).Result()
assert.NoError(t, err)
var result map[int64][]string
err = json.Unmarshal([]byte(data), &result)
assert.NoError(t, err)
// Verify data content
for _, user := range tt.users {
ips, exists := result[user.SID]
assert.True(t, exists)
assert.Contains(t, ips, user.IP)
}
})
}
}
-34
View File
@@ -1,34 +0,0 @@
package cache
type NodeOnlineUser struct {
SID int64
IP string
}
type NodeTodayTrafficRank struct {
ID int64
Name string
Upload int64
Download int64
Total int64
}
type UserTodayTrafficRank struct {
SID int64
Upload int64
Download int64
Total int64
}
type UserTraffic struct {
UID int64
Upload int64
Download int64
}
type NodeStatus struct {
Cpu float64
Mem float64
Disk float64
UpdatedAt int64
}
+75
View File
@@ -0,0 +1,75 @@
package client
import (
"encoding/json"
"time"
)
type SubscribeApplication struct {
Id int64 `gorm:"primaryKey"`
Name string `gorm:"type:varchar(255);default:'';not null;comment:Application Name"`
Icon string `gorm:"type:MEDIUMTEXT;default:null;comment:Application Icon"`
Description string `gorm:"type:varchar(255);default:null;comment:Application Description"`
Scheme string `gorm:"type:varchar(255);default:'';not null;comment:Scheme"`
UserAgent string `gorm:"type:varchar(255);default:'';not null;comment:User Agent"`
IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:Is Default Application"`
SubscribeTemplate string `gorm:"type:MEDIUMTEXT;default:null;comment:Subscribe Template"`
OutputFormat string `gorm:"type:varchar(50);default:'yaml';not null;comment:Output Format"`
DownloadLink string `gorm:"type:text;not null;comment:Download Link"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (SubscribeApplication) TableName() string {
return "subscribe_application"
}
type DownloadLink struct {
IOS string `json:"ios,omitempty"`
Android string `json:"android,omitempty"`
Windows string `json:"windows,omitempty"`
Mac string `json:"mac,omitempty"`
Linux string `json:"linux,omitempty"`
Harmony string `json:"harmony,omitempty"`
}
// GetDownloadLink returns the download link for the specified platform.
func (d *DownloadLink) GetDownloadLink(platform string) string {
if d == nil {
return ""
}
switch platform {
case "ios":
return d.IOS
case "android":
return d.Android
case "windows":
return d.Windows
case "mac":
return d.Mac
case "linux":
return d.Linux
case "harmony":
return d.Harmony
default:
return ""
}
}
// Marshal serializes the DownloadLink to JSON format.
func (d *DownloadLink) Marshal() ([]byte, error) {
if d == nil {
var empty DownloadLink
return json.Marshal(empty)
}
return json.Marshal(d)
}
// Unmarshal parses the JSON-encoded data and stores the result in the DownloadLink.
func (d *DownloadLink) Unmarshal(data []byte) error {
if data == nil || len(data) == 0 {
*d = DownloadLink{}
return nil
}
return json.Unmarshal(data, d)
}
+81
View File
@@ -0,0 +1,81 @@
package client
import (
"context"
"gorm.io/gorm"
)
type (
Model interface {
subscribeApplicationModel
}
subscribeApplicationModel interface {
Insert(ctx context.Context, data *SubscribeApplication) error
FindOne(ctx context.Context, id int64) (*SubscribeApplication, error)
Update(ctx context.Context, data *SubscribeApplication) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context) ([]*SubscribeApplication, error)
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
DefaultSubscribeApplicationModel struct {
*gorm.DB
}
)
func NewSubscribeApplicationModel(db *gorm.DB) Model {
return &DefaultSubscribeApplicationModel{
DB: db,
}
}
func (m *DefaultSubscribeApplicationModel) Insert(ctx context.Context, data *SubscribeApplication) error {
if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Create(data).Error; err != nil {
return err
}
return nil
}
func (m *DefaultSubscribeApplicationModel) FindOne(ctx context.Context, id int64) (*SubscribeApplication, error) {
var resp SubscribeApplication
if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("id = ?", id).First(&resp).Error; err != nil {
return nil, err
}
return &resp, nil
}
func (m *DefaultSubscribeApplicationModel) Update(ctx context.Context, data *SubscribeApplication) error {
if _, err := m.FindOne(ctx, data.Id); err != nil {
return err
}
if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("`id` = ?", data.Id).Save(data).Error; err != nil {
return err
}
return nil
}
func (m *DefaultSubscribeApplicationModel) Delete(ctx context.Context, id int64) error {
if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("`id` = ?", id).Delete(&SubscribeApplication{}).Error; err != nil {
return err
}
return nil
}
func (m *DefaultSubscribeApplicationModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
tx := m.WithContext(ctx).Begin()
if err := fn(tx); err != nil {
if rbErr := tx.Rollback().Error; rbErr != nil {
return rbErr
}
return err
}
return tx.Commit().Error
}
func (m *DefaultSubscribeApplicationModel) List(ctx context.Context) ([]*SubscribeApplication, error) {
var resp []*SubscribeApplication
if err := m.WithContext(ctx).Find(&resp).Error; err != nil {
return nil, err
}
return resp, nil
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
+25 -49
View File
@@ -6,74 +6,50 @@ import (
"gorm.io/gorm"
)
var _ Model = (*customLogModel)(nil)
var _ Model = (*customSystemLogModel)(nil)
type (
Model interface {
messageLogModel
systemLogModel
customSystemLogLogicModel
}
messageLogModel interface {
InsertMessageLog(ctx context.Context, data *MessageLog) error
FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error)
UpdateMessageLog(ctx context.Context, data *MessageLog) error
DeleteMessageLog(ctx context.Context, id int64) error
FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error)
systemLogModel interface {
Insert(ctx context.Context, data *SystemLog) error
FindOne(ctx context.Context, id int64) (*SystemLog, error)
Update(ctx context.Context, data *SystemLog) error
Delete(ctx context.Context, id int64) error
}
customLogModel struct {
customSystemLogModel struct {
*defaultLogModel
}
defaultLogModel struct {
Connection *gorm.DB
*gorm.DB
}
)
func newLogModel(db *gorm.DB) *defaultLogModel {
func newSystemLogModel(db *gorm.DB) *defaultLogModel {
return &defaultLogModel{
Connection: db,
DB: db,
}
}
func (m *defaultLogModel) InsertMessageLog(ctx context.Context, data *MessageLog) error {
return m.Connection.WithContext(ctx).Create(&data).Error
func (m *defaultLogModel) Insert(ctx context.Context, data *SystemLog) error {
return m.WithContext(ctx).Create(data).Error
}
func (m *defaultLogModel) FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error) {
var resp MessageLog
err := m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("`id` = ?", id).First(&resp).Error
return &resp, err
func (m *defaultLogModel) FindOne(ctx context.Context, id int64) (*SystemLog, error) {
var log SystemLog
err := m.WithContext(ctx).Where("id = ?", id).First(&log).Error
if err != nil {
return nil, err
}
return &log, nil
}
func (m *defaultLogModel) UpdateMessageLog(ctx context.Context, data *MessageLog) error {
return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", data.Id).Updates(data).Error
func (m *defaultLogModel) Update(ctx context.Context, data *SystemLog) error {
return m.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error
}
func (m *defaultLogModel) DeleteMessageLog(ctx context.Context, id int64) error {
return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", id).Delete(&MessageLog{}).Error
}
func (m *defaultLogModel) FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error) {
var list []*MessageLog
var total int64
conn := m.Connection.WithContext(ctx).Model(&MessageLog{})
if filter.Type != "" {
conn = conn.Where("`type` = ?", filter.Type)
}
if filter.Platform != "" {
conn = conn.Where("`platform` = ?", filter.Platform)
}
if filter.To != "" {
conn = conn.Where("`to` LIKE ?", "%"+filter.To+"%")
}
if filter.Subject != "" {
conn = conn.Where("`subject` LIKE ?", "%"+filter.Subject+"%")
}
if filter.Content != "" {
conn = conn.Where("`content` = ?", "%"+filter.Content+"%")
}
if filter.Status > 0 {
conn = conn.Where("`status` = ?", filter.Status)
}
err := conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(&list).Error
return total, list, err
func (m *defaultLogModel) Delete(ctx context.Context, id int64) error {
return m.WithContext(ctx).Where("`id` = ?", id).Delete(&SystemLog{}).Error
}
+412 -33
View File
@@ -1,45 +1,424 @@
package log
import "time"
type MessageType int
const (
Email MessageType = iota + 1
Mobile
import (
"encoding/json"
"time"
)
func (t MessageType) String() string {
switch t {
case Email:
return "email"
case Mobile:
return "mobile"
}
return "unknown"
type Type uint8
/*
Log Types:
1X Message Logs
2X Subscription Logs
3X User Logs
4X Traffic Ranking Logs
*/
const (
TypeEmailMessage Type = 10 // Message log
TypeMobileMessage Type = 11 // Mobile message log
TypeSubscribe Type = 20 // Subscription log
TypeSubscribeTraffic Type = 21 // Subscription traffic log
TypeServerTraffic Type = 22 // Server traffic log
TypeResetSubscribe Type = 23 // Reset subscription log
TypeLogin Type = 30 // Login log
TypeRegister Type = 31 // Registration log
TypeBalance Type = 32 // Balance log
TypeCommission Type = 33 // Commission log
TypeGift Type = 34 // Gift log
TypeUserTrafficRank Type = 40 // Top 10 User traffic rank log
TypeServerTrafficRank Type = 41 // Top 10 Server traffic rank log
TypeTrafficStat Type = 42 // Daily traffic statistics log
)
const (
ResetSubscribeTypeAuto uint16 = 231 // Auto reset
ResetSubscribeTypeAdvance uint16 = 232 // Advance reset
ResetSubscribeTypePaid uint16 = 233 // Paid reset
ResetSubscribeTypeQuota uint16 = 234 // Quota reset
BalanceTypeRecharge uint16 = 321 // Recharge
BalanceTypeWithdraw uint16 = 322 // Withdraw
BalanceTypePayment uint16 = 323 // Payment
BalanceTypeRefund uint16 = 324 // Refund
BalanceTypeAdjust uint16 = 326 // Admin Adjust
BalanceTypeReward uint16 = 325 // Reward
CommissionTypePurchase uint16 = 331 // Purchase
CommissionTypeRenewal uint16 = 332 // Renewal
CommissionTypeRefund uint16 = 333 // Refund
commissionTypeWithdraw uint16 = 334 // withdraw
CommissionTypeAdjust uint16 = 335 // Admin Adjust
GiftTypeIncrease uint16 = 341 // Increase
GiftTypeReduce uint16 = 342 // Reduce
)
// Uint8 converts Type to uint8.
func (t Type) Uint8() uint8 {
return uint8(t)
}
type MessageLog struct {
Id int64 `gorm:"primaryKey"`
Type string `gorm:"type:varchar(50);not null;default:'email';comment:Message Type"`
Platform string `gorm:"type:varchar(50);not null;default:'smtp';comment:Platform"`
To string `gorm:"type:text;not null;comment:To"`
Subject string `gorm:"type:varchar(255);not null;default:'';comment:Subject"`
Content string `gorm:"type:text;comment:Content"`
Status int `gorm:"type:tinyint(1);not null;default:0;comment:Status"`
// SystemLog represents a log entry in the system.
type SystemLog struct {
Id int64 `gorm:"primaryKey;AUTO_INCREMENT"`
Type uint8 `gorm:"index:idx_type;type:tinyint(1);not null;default:0;comment:Log Type: 1: Email Message 2: Mobile Message 3: Subscribe 4: Subscribe Traffic 5: Server Traffic 6: Login 7: Register 8: Balance 9: Commission 10: Reset Subscribe 11: Gift"`
Date string `gorm:"type:varchar(20);default:null;comment:Log Date"`
ObjectID int64 `gorm:"index:idx_object_id;type:bigint(20);not null;default:0;comment:Object ID"`
Content string `gorm:"type:text;not null;comment:Log Content"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (m *MessageLog) TableName() string {
return "message_log"
// TableName returns the name of the table for SystemLogs.
func (SystemLog) TableName() string {
return "system_logs"
}
type MessageLogFilterParams struct {
Type string
Platform string
To string
Subject string
Content string
Status int
// Message represents a message log entry.
type Message struct {
To string `json:"to"`
Subject string `json:"subject,omitempty"`
Content map[string]interface{} `json:"content"`
Platform string `json:"platform"`
Template string `json:"template"`
Status uint8 `json:"status"` // 1: Sent, 2: Failed
}
// Marshal implements the json.Marshaler interface for Message.
func (m *Message) Marshal() ([]byte, error) {
type Alias Message
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(m),
})
}
// Unmarshal implements the json.Unmarshaler interface for Message.
func (m *Message) Unmarshal(data []byte) error {
type Alias Message
aux := (*Alias)(m)
return json.Unmarshal(data, aux)
}
// Traffic represents a subscription traffic log entry.
type Traffic struct {
Download int64 `json:"download"`
Upload int64 `json:"upload"`
}
// Marshal implements the json.Marshaler interface for SubscribeTraffic.
func (s *Traffic) Marshal() ([]byte, error) {
type Alias Traffic
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
// Unmarshal implements the json.Unmarshaler interface for SubscribeTraffic.
func (s *Traffic) Unmarshal(data []byte) error {
type Alias Traffic
aux := (*Alias)(s)
return json.Unmarshal(data, aux)
}
// Login represents a login log entry.
type Login struct {
Method string `json:"method"`
LoginIP string `json:"login_ip"`
UserAgent string `json:"user_agent"`
Success bool `json:"success"`
Timestamp int64 `json:"timestamp"`
}
// Marshal implements the json.Marshaler interface for Login.
func (l *Login) Marshal() ([]byte, error) {
type Alias Login
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
}
// Unmarshal implements the json.Unmarshaler interface for Login.
func (l *Login) Unmarshal(data []byte) error {
type Alias Login
aux := (*Alias)(l)
return json.Unmarshal(data, aux)
}
// Register represents a registration log entry.
type Register struct {
AuthMethod string `json:"auth_method"`
Identifier string `json:"identifier"`
RegisterIP string `json:"register_ip"`
UserAgent string `json:"user_agent"`
Timestamp int64 `json:"timestamp"`
}
// Marshal implements the json.Marshaler interface for Register.
func (r *Register) Marshal() ([]byte, error) {
type Alias Register
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(r),
})
}
// Unmarshal implements the json.Unmarshaler interface for Register.
func (r *Register) Unmarshal(data []byte) error {
type Alias Register
aux := (*Alias)(r)
return json.Unmarshal(data, aux)
}
// Subscribe represents a subscription log entry.
type Subscribe struct {
Token string `json:"token"`
UserAgent string `json:"user_agent"`
ClientIP string `json:"client_ip"`
UserSubscribeId int64 `json:"user_subscribe_id"`
}
// Marshal implements the json.Marshaler interface for Subscribe.
func (s *Subscribe) Marshal() ([]byte, error) {
type Alias Subscribe
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
// Unmarshal implements the json.Unmarshaler interface for Subscribe.
func (s *Subscribe) Unmarshal(data []byte) error {
type Alias Subscribe
aux := (*Alias)(s)
return json.Unmarshal(data, aux)
}
// ResetSubscribe represents a reset subscription log entry.
type ResetSubscribe struct {
Type uint16 `json:"type"`
UserId int64 `json:"user_id"`
OrderNo string `json:"order_no,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// Marshal implements the json.Marshaler interface for ResetSubscribe.
func (r *ResetSubscribe) Marshal() ([]byte, error) {
type Alias ResetSubscribe
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(r),
})
}
// Unmarshal implements the json.Unmarshaler interface for ResetSubscribe.
func (r *ResetSubscribe) Unmarshal(data []byte) error {
type Alias ResetSubscribe
aux := (*Alias)(r)
return json.Unmarshal(data, aux)
}
// Balance represents a balance log entry.
type Balance struct {
Type uint16 `json:"type"`
Amount int64 `json:"amount"`
OrderNo string `json:"order_no,omitempty"`
Balance int64 `json:"balance"`
Timestamp int64 `json:"timestamp"`
}
// Marshal implements the json.Marshaler interface for Balance.
func (b *Balance) Marshal() ([]byte, error) {
type Alias Balance
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(b),
})
}
// Unmarshal implements the json.Unmarshaler interface for Balance.
func (b *Balance) Unmarshal(data []byte) error {
type Alias Balance
aux := (*Alias)(b)
return json.Unmarshal(data, aux)
}
// Commission represents a commission log entry.
type Commission struct {
Type uint16 `json:"type"`
Amount int64 `json:"amount"`
OrderNo string `json:"order_no"`
Timestamp int64 `json:"timestamp"`
}
// Marshal implements the json.Marshaler interface for Commission.
func (c *Commission) Marshal() ([]byte, error) {
type Alias Commission
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(c),
})
}
// Unmarshal implements the json.Unmarshaler interface for Commission.
func (c *Commission) Unmarshal(data []byte) error {
type Alias Commission
aux := (*Alias)(c)
return json.Unmarshal(data, aux)
}
// Gift represents a gift log entry.
type Gift struct {
Type uint16 `json:"type"`
OrderNo string `json:"order_no"`
SubscribeId int64 `json:"subscribe_id"`
Amount int64 `json:"amount"`
Balance int64 `json:"balance"`
Remark string `json:"remark,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// Marshal implements the json.Marshaler interface for Gift.
func (g *Gift) Marshal() ([]byte, error) {
type Alias Gift
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(g),
})
}
// Unmarshal implements the json.Unmarshaler interface for Gift.
func (g *Gift) Unmarshal(data []byte) error {
type Alias Gift
aux := (*Alias)(g)
return json.Unmarshal(data, aux)
}
// UserTraffic represents a user traffic log entry.
type UserTraffic struct {
SubscribeId int64 `json:"subscribe_id"` // Subscribe ID
UserId int64 `json:"user_id"` // User ID
Upload int64 `json:"upload"` // Upload traffic in bytes
Download int64 `json:"download"` // Download traffic in bytes
Total int64 `json:"total"` // Total traffic in bytes (Upload + Download)
}
// Marshal implements the json.Marshaler interface for UserTraffic.
func (u *UserTraffic) Marshal() ([]byte, error) {
type Alias UserTraffic
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(u),
})
}
// Unmarshal implements the json.Unmarshaler interface for UserTraffic.
func (u *UserTraffic) Unmarshal(data []byte) error {
type Alias UserTraffic
aux := (*Alias)(u)
return json.Unmarshal(data, aux)
}
// UserTrafficRank represents a user traffic rank entry.
type UserTrafficRank struct {
Rank map[uint8]UserTraffic `json:"rank"` // Key is rank ,type is UserTraffic
}
// Marshal implements the json.Marshaler interface for UserTrafficRank.
func (u *UserTrafficRank) Marshal() ([]byte, error) {
type Alias UserTrafficRank
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(u),
})
}
// Unmarshal implements the json.Unmarshaler interface for UserTrafficRank.
func (u *UserTrafficRank) Unmarshal(data []byte) error {
type Alias UserTrafficRank
aux := (*Alias)(u)
return json.Unmarshal(data, aux)
}
// ServerTraffic represents a server traffic log entry.
type ServerTraffic struct {
ServerId int64 `json:"server_id"` // Server ID
Upload int64 `json:"upload"` // Upload traffic in bytes
Download int64 `json:"download"` // Download traffic in bytes
Total int64 `json:"total"` // Total traffic in bytes (Upload + Download)
}
// Marshal implements the json.Marshaler interface for ServerTraffic.
func (s *ServerTraffic) Marshal() ([]byte, error) {
type Alias ServerTraffic
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
// Unmarshal implements the json.Unmarshaler interface for ServerTraffic.
func (s *ServerTraffic) Unmarshal(data []byte) error {
type Alias ServerTraffic
aux := (*Alias)(s)
return json.Unmarshal(data, aux)
}
// ServerTrafficRank represents a server traffic rank entry.
type ServerTrafficRank struct {
Rank map[uint8]ServerTraffic `json:"rank"` // Key is rank ,type is ServerTraffic
}
// Marshal implements the json.Marshaler interface for ServerTrafficRank.
func (s *ServerTrafficRank) Marshal() ([]byte, error) {
type Alias ServerTrafficRank
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
// Unmarshal implements the json.Unmarshaler interface for ServerTrafficRank.
func (s *ServerTrafficRank) Unmarshal(data []byte) error {
type Alias ServerTrafficRank
aux := (*Alias)(s)
return json.Unmarshal(data, aux)
}
// TrafficStat represents a daily traffic statistics log entry.
type TrafficStat struct {
Upload int64 `json:"upload"`
Download int64 `json:"download"`
Total int64 `json:"total"`
}
// Marshal implements the json.Marshaler interface for TrafficStat.
func (t *TrafficStat) Marshal() ([]byte, error) {
type Alias TrafficStat
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(t),
})
}
// Unmarshal implements the json.Unmarshaler interface for TrafficStat.
func (t *TrafficStat) Unmarshal(data []byte) error {
type Alias TrafficStat
aux := (*Alias)(t)
return json.Unmarshal(data, aux)
}
+56 -2
View File
@@ -1,9 +1,63 @@
package log
import (
"context"
"gorm.io/gorm"
)
func NewModel(conn *gorm.DB) Model {
return newLogModel(conn)
func NewModel(db *gorm.DB) Model {
return &customSystemLogModel{
defaultLogModel: newSystemLogModel(db),
}
}
type FilterParams struct {
Page int
Size int
Type uint8
Data string
Search string
ObjectID int64
}
type customSystemLogLogicModel interface {
FilterSystemLog(ctx context.Context, filter *FilterParams) ([]*SystemLog, int64, error)
}
func (m *customSystemLogModel) FilterSystemLog(ctx context.Context, filter *FilterParams) ([]*SystemLog, int64, error) {
tx := m.WithContext(ctx).Model(&SystemLog{}).Order("id DESC")
if filter == nil {
filter = &FilterParams{
Page: 1,
Size: 10,
}
}
if filter.Page < 1 {
filter.Page = 1
}
if filter.Size < 1 {
filter.Size = 10
}
if filter.Type != 0 {
tx = tx.Where("`type` = ?", filter.Type)
}
if filter.Data != "" {
tx = tx.Where("`date` = ?", filter.Data)
}
if filter.ObjectID != 0 {
tx = tx.Where("`object_id` = ?", filter.ObjectID)
}
if filter.Search != "" {
tx = tx.Where("`content` LIKE ?", "%"+filter.Search+"%")
}
var total int64
var logs []*SystemLog
err := tx.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(&logs).Error
return logs, total, err
}
+163
View File
@@ -0,0 +1,163 @@
package node
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
type (
customCacheLogicModel interface {
StatusCache(ctx context.Context, serverId int64) (Status, error)
UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error
OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error)
UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error
OnlineUserSubscribeGlobal(ctx context.Context) (int64, error)
UpdateOnlineUserSubscribeGlobal(ctx context.Context, subscribe OnlineUserSubscribe) error
}
Status struct {
Cpu float64 `json:"cpu"`
Mem float64 `json:"mem"`
Disk float64 `json:"disk"`
UpdatedAt int64 `json:"updated_at"`
}
OnlineUserSubscribe map[int64][]string
)
// Marshal to json string
func (s *Status) Marshal() string {
type Alias Status
data, _ := json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
return string(data)
}
// Unmarshal from json string
func (s *Status) Unmarshal(data string) error {
type Alias Status
aux := &struct {
*Alias
}{
Alias: (*Alias)(s),
}
return json.Unmarshal([]byte(data), &aux)
}
const (
Expiry = 300 * time.Second // Cache expiry time in seconds
StatusCacheKey = "node:status:%d" // Node status cache key format (Server ID and protocol) Example: node:status:1:shadowsocks
OnlineUserCacheKeyWithSubscribe = "node:online:subscribe:%d:%s" // Online user subscribe cache key format (Server ID and protocol) Example: node:online:subscribe:1:shadowsocks
OnlineUserSubscribeCacheKeyWithGlobal = "node:online:subscribe:global" // Online user global subscribe cache key
)
// UpdateStatusCache Update server status to cache
func (m *customServerModel) UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error {
key := fmt.Sprintf(StatusCacheKey, serverId)
return m.Cache.Set(ctx, key, status.Marshal(), Expiry).Err()
}
// DeleteStatusCache Delete server status from cache
func (m *customServerModel) DeleteStatusCache(ctx context.Context, serverId int64) error {
key := fmt.Sprintf(StatusCacheKey, serverId)
return m.Cache.Del(ctx, key).Err()
}
// StatusCache Get server status from cache
func (m *customServerModel) StatusCache(ctx context.Context, serverId int64) (Status, error) {
var status Status
key := fmt.Sprintf(StatusCacheKey, serverId)
result, err := m.Cache.Get(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return status, nil
}
return status, err
}
if result == "" {
return status, nil
}
err = status.Unmarshal(result)
return status, err
}
// OnlineUserSubscribe Get online user subscribe
func (m *customServerModel) OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error) {
key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol)
result, err := m.Cache.Get(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return OnlineUserSubscribe{}, nil
}
return nil, err
}
if result == "" {
return OnlineUserSubscribe{}, nil
}
var subscribe OnlineUserSubscribe
err = json.Unmarshal([]byte(result), &subscribe)
return subscribe, err
}
// UpdateOnlineUserSubscribe Update online user subscribe
func (m *customServerModel) UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error {
key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol)
data, err := json.Marshal(subscribe)
if err != nil {
return err
}
return m.Cache.Set(ctx, key, data, Expiry).Err()
}
// DeleteOnlineUserSubscribe Delete online user subscribe
func (m *customServerModel) DeleteOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) error {
key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol)
return m.Cache.Del(ctx, key).Err()
}
// OnlineUserSubscribeGlobal Get global online user subscribe count
func (m *customServerModel) OnlineUserSubscribeGlobal(ctx context.Context) (int64, error) {
now := time.Now().Unix()
// Clear expired data
if err := m.Cache.ZRemRangeByScore(ctx, OnlineUserSubscribeCacheKeyWithGlobal, "-inf", fmt.Sprintf("%d", now)).Err(); err != nil {
return 0, err
}
return m.Cache.ZCard(ctx, OnlineUserSubscribeCacheKeyWithGlobal).Result()
}
// UpdateOnlineUserSubscribeGlobal Update global online user subscribe count
func (m *customServerModel) UpdateOnlineUserSubscribeGlobal(ctx context.Context, subscribe OnlineUserSubscribe) error {
now := time.Now()
expireTime := now.Add(5 * time.Minute).Unix() // set expire time 5 minutes later
pipe := m.Cache.Pipeline()
// Clear expired data
pipe.ZRemRangeByScore(ctx, OnlineUserSubscribeCacheKeyWithGlobal, "-inf", fmt.Sprintf("%d", now.Unix()))
// Add or update each subscribe with new expire time
for sub := range subscribe {
// Use ZAdd to add or update the member with new score (expire time)
pipe.ZAdd(ctx, OnlineUserSubscribeCacheKeyWithGlobal, redis.Z{
Score: float64(expireTime),
Member: sub,
})
}
_, err := pipe.Exec(ctx)
return err
}
// DeleteOnlineUserSubscribeGlobal Delete global online user subscribe count
func (m *customServerModel) DeleteOnlineUserSubscribeGlobal(ctx context.Context) error {
return m.Cache.Del(ctx, OnlineUserSubscribeCacheKeyWithGlobal).Err()
}
+131
View File
@@ -0,0 +1,131 @@
package node
import (
"context"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
var _ Model = (*customServerModel)(nil)
//goland:noinspection GoNameStartsWithPackageName
type (
Model interface {
serverModel
NodeModel
customCacheLogicModel
customServerLogicModel
}
serverModel interface {
InsertServer(ctx context.Context, data *Server, tx ...*gorm.DB) error
FindOneServer(ctx context.Context, id int64) (*Server, error)
UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error
DeleteServer(ctx context.Context, id int64, tx ...*gorm.DB) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
NodeModel interface {
InsertNode(ctx context.Context, data *Node, tx ...*gorm.DB) error
FindOneNode(ctx context.Context, id int64) (*Node, error)
UpdateNode(ctx context.Context, data *Node, tx ...*gorm.DB) error
DeleteNode(ctx context.Context, id int64, tx ...*gorm.DB) error
}
customServerModel struct {
*defaultServerModel
}
defaultServerModel struct {
*gorm.DB
Cache *redis.Client
}
)
func newServerModel(db *gorm.DB, cache *redis.Client) *defaultServerModel {
return &defaultServerModel{
DB: db,
Cache: cache,
}
}
// NewModel returns a model for the database table.
func NewModel(conn *gorm.DB, cache *redis.Client) Model {
return &customServerModel{
defaultServerModel: newServerModel(conn, cache),
}
}
func (m *defaultServerModel) InsertServer(ctx context.Context, data *Server, tx ...*gorm.DB) error {
db := m.DB
if len(tx) > 0 {
db = tx[0]
}
return db.WithContext(ctx).Create(data).Error
}
func (m *defaultServerModel) FindOneServer(ctx context.Context, id int64) (*Server, error) {
var server Server
err := m.WithContext(ctx).Model(&Server{}).Where("id = ?", id).First(&server).Error
return &server, err
}
func (m *defaultServerModel) UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error {
_, err := m.FindOneServer(ctx, data.Id)
if err != nil {
return err
}
db := m.DB
if len(tx) > 0 {
db = tx[0]
}
return db.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error
}
func (m *defaultServerModel) DeleteServer(ctx context.Context, id int64, tx ...*gorm.DB) error {
db := m.DB
if len(tx) > 0 {
db = tx[0]
}
return db.WithContext(ctx).Where("`id` = ?", id).Delete(&Server{}).Error
}
func (m *defaultServerModel) InsertNode(ctx context.Context, data *Node, tx ...*gorm.DB) error {
db := m.DB
if len(tx) > 0 {
db = tx[0]
}
return db.WithContext(ctx).Create(data).Error
}
func (m *defaultServerModel) FindOneNode(ctx context.Context, id int64) (*Node, error) {
var node Node
err := m.WithContext(ctx).Model(&Node{}).Where("id = ?", id).First(&node).Error
return &node, err
}
func (m *defaultServerModel) UpdateNode(ctx context.Context, data *Node, tx ...*gorm.DB) error {
_, err := m.FindOneNode(ctx, data.Id)
if err != nil {
return err
}
db := m.DB
if len(tx) > 0 {
db = tx[0]
}
return db.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error
}
func (m *defaultServerModel) DeleteNode(ctx context.Context, id int64, tx ...*gorm.DB) error {
db := m.DB
if len(tx) > 0 {
db = tx[0]
}
return db.WithContext(ctx).Where("`id` = ?", id).Delete(&Node{}).Error
}
func (m *defaultServerModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
return m.WithContext(ctx).Transaction(fn)
}
+185
View File
@@ -0,0 +1,185 @@
package node
import (
"context"
"fmt"
"strings"
"github.com/perfect-panel/server/pkg/tool"
"gorm.io/gorm"
)
type customServerLogicModel interface {
FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error)
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
}
const (
// ServerUserListCacheKey Server User List Cache Key
ServerUserListCacheKey = "server:user:"
// ServerConfigCacheKey Server Config Cache Key
ServerConfigCacheKey = "server:config:"
)
// FilterParams Filter Server Params
type FilterParams struct {
Page int
Size int
Ids []int64 // Server IDs
Search string
}
type FilterNodeParams struct {
Page int // Page Number
Size int // Page Size
NodeId []int64 // Node IDs
ServerId []int64 // Server IDs
Tag []string // Tags
Search string // Search Address or Name
Protocol string // Protocol
Preload bool // Preload Server
Enabled *bool // Enabled
}
// FilterServerList Filter Server List
func (m *customServerModel) FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error) {
var servers []*Server
var total int64
query := m.WithContext(ctx).Model(&Server{})
if params == nil {
params = &FilterParams{
Page: 1,
Size: 10,
}
}
if params.Search != "" {
s := "%" + params.Search + "%"
query = query.Where("`name` LIKE ? OR `address` LIKE ?", s, s)
}
if len(params.Ids) > 0 {
query = query.Where("id IN ?", params.Ids)
}
err := query.Count(&total).Order("sort ASC").Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&servers).Error
return total, servers, err
}
// FilterNodeList Filter Node List
func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) {
var nodes []*Node
var total int64
query := m.WithContext(ctx).Model(&Node{})
if params == nil {
params = &FilterNodeParams{
Page: 1,
Size: 10,
}
}
if params.Search != "" {
s := "%" + params.Search + "%"
query = query.Where("`name` LIKE ? OR `address` LIKE ? OR `tags` LIKE ? OR `port` LIKE ? ", s, s, s, s)
}
if len(params.NodeId) > 0 {
query = query.Where("id IN ?", params.NodeId)
}
if len(params.ServerId) > 0 {
query = query.Where("server_id IN ?", params.ServerId)
}
if len(params.Tag) > 0 {
query = query.Scopes(InSet("tags", params.Tag))
}
if params.Protocol != "" {
query = query.Where("protocol = ?", params.Protocol)
}
if params.Enabled != nil {
query = query.Where("enabled = ?", *params.Enabled)
}
if params.Preload {
query = query.Preload("Server")
}
err := query.Count(&total).Order("sort ASC").Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&nodes).Error
return total, nodes, err
}
// ClearNodeCache Clear Node Cache
func (m *customServerModel) ClearNodeCache(ctx context.Context, params *FilterNodeParams) error {
_, nodes, err := m.FilterNodeList(ctx, params)
if err != nil {
return err
}
var cacheKeys []string
for _, node := range nodes {
cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, node.ServerId))
if node.Protocol != "" {
var cursor uint64
for {
keys, newCursor, err := m.Cache.Scan(ctx, cursor, fmt.Sprintf("%s%d*", ServerConfigCacheKey, node.ServerId), 100).Result()
if err != nil {
return err
}
if len(keys) > 0 {
cacheKeys = append(keys, keys...)
}
cursor = newCursor
if cursor == 0 {
break
}
}
}
}
if len(cacheKeys) > 0 {
cacheKeys = tool.RemoveDuplicateElements(cacheKeys...)
return m.Cache.Del(ctx, cacheKeys...).Err()
}
return nil
}
// ClearServerCache Clear Server Cache
func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64) error {
var cacheKeys []string
cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, serverId))
var cursor uint64
for {
keys, newCursor, err := m.Cache.Scan(ctx, 0, fmt.Sprintf("%s%d*", ServerConfigCacheKey, serverId), 100).Result()
if err != nil {
return err
}
if len(keys) > 0 {
cacheKeys = append(cacheKeys, keys...)
}
cursor = newCursor
if cursor == 0 {
break
}
}
if len(cacheKeys) > 0 {
cacheKeys = tool.RemoveDuplicateElements(cacheKeys...)
return m.Cache.Del(ctx, cacheKeys...).Err()
}
return nil
}
// InSet 支持多值 OR 查询
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if len(values) == 0 {
return db
}
conds := make([]string, len(values))
args := make([]interface{}, len(values))
for i, v := range values {
conds[i] = "FIND_IN_SET(?, " + field + ")"
args[i] = v
}
// 用括号包裹 OR 条件,保证外层 AND 不受影响
return db.Where("("+strings.Join(conds, " OR ")+")", args...)
}
}
+82
View File
@@ -0,0 +1,82 @@
package node
import (
"time"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/gorm"
)
type Node struct {
Id int64 `gorm:"primary_key"`
Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"`
Tags string `gorm:"type:varchar(255);not null;default:'';comment:Tags"`
Port uint16 `gorm:"not null;default:0;comment:Connect Port"`
Address string `gorm:"type:varchar(255);not null;default:'';comment:Connect Address"`
ServerId int64 `gorm:"not null;default:0;comment:Server ID"`
Server *Server `gorm:"foreignKey:ServerId;references:Id"`
Protocol string `gorm:"type:varchar(100);not null;default:'';comment:Protocol"`
Enabled *bool `gorm:"type:boolean;not null;default:true;comment:Enabled"`
Sort int `gorm:"uniqueIndex;not null;default:0;comment:Sort"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (n *Node) TableName() string {
return "nodes"
}
func (n *Node) BeforeCreate(tx *gorm.DB) error {
if n.Sort == 0 {
var maxSort int
if err := tx.Model(&Node{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil {
return err
}
n.Sort = maxSort + 1
}
return nil
}
func (n *Node) BeforeDelete(tx *gorm.DB) error {
if err := tx.Exec("UPDATE `nodes` SET sort = sort - 1 WHERE sort > ?", n.Sort).Error; err != nil {
return err
}
return nil
}
func (n *Node) BeforeUpdate(tx *gorm.DB) error {
var count int64
if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}).
Where("sort = ? AND id != ?", n.Sort, n.Id).Count(&count).Error; err != nil {
return err
}
if count > 1 {
// reorder sort
if err := reorderSortWithNode(tx); err != nil {
logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error())
return err
}
// get max sort
var maxSort int
if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil {
return err
}
n.Sort = maxSort + 1
}
return nil
}
func reorderSortWithNode(tx *gorm.DB) error {
var nodes []Node
if err := tx.Order("sort, id").Find(&nodes).Error; err != nil {
return err
}
for i, node := range nodes {
if node.Sort != i+1 {
if err := tx.Exec("UPDATE `nodes` SET sort = ? WHERE id = ?", i+1, node.Id).Error; err != nil {
return err
}
}
}
return nil
}
+188
View File
@@ -0,0 +1,188 @@
package node
import (
"encoding/json"
"time"
"github.com/perfect-panel/server/pkg/logger"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type Server struct {
Id int64 `gorm:"primary_key"`
Name string `gorm:"type:varchar(100);not null;default:'';comment:Server Name"`
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
City string `gorm:"type:varchar(128);not null;default:'';comment:City"`
//Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"`
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (*Server) TableName() string {
return "servers"
}
func (m *Server) BeforeCreate(tx *gorm.DB) error {
if m.Sort == 0 {
var maxSort int
if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil {
return err
}
m.Sort = maxSort + 1
}
return nil
}
func (m *Server) BeforeDelete(tx *gorm.DB) error {
if err := tx.Exec("UPDATE `servers` SET sort = sort - 1 WHERE sort > ?", m.Sort).Error; err != nil {
return err
}
return nil
}
func (m *Server) BeforeUpdate(tx *gorm.DB) error {
var count int64
if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}).
Where("sort = ? AND id != ?", m.Sort, m.Id).Count(&count).Error; err != nil {
return err
}
if count > 1 {
// reorder sort
if err := reorderSortWithServer(tx); err != nil {
logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error())
return err
}
// get max sort
var maxSort int
if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil {
return err
}
m.Sort = maxSort + 1
}
return nil
}
// MarshalProtocols Marshal server protocols to json
func (m *Server) MarshalProtocols(list []Protocol) error {
var validate = make(map[string]bool)
for _, protocol := range list {
if protocol.Type == "" {
return errors.New("protocol type is required")
}
if _, exists := validate[protocol.Type]; exists {
return errors.New("duplicate protocol type: " + protocol.Type)
}
validate[protocol.Type] = true
}
data, err := json.Marshal(list)
if err != nil {
return err
}
m.Protocols = string(data)
return nil
}
// UnmarshalProtocols Unmarshal server protocols from json
func (m *Server) UnmarshalProtocols() ([]Protocol, error) {
var list []Protocol
if m.Protocols == "" {
return list, nil
}
err := json.Unmarshal([]byte(m.Protocols), &list)
if err != nil {
return nil, err
}
return list, nil
}
type Protocol struct {
Type string `json:"type"`
Port uint16 `json:"port"`
Enable bool `json:"enable"`
Security string `json:"security,omitempty"`
SNI string `json:"sni,omitempty"`
AllowInsecure bool `json:"allow_insecure,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
RealityServerAddr string `json:"reality_server_addr,omitempty"`
RealityServerPort int `json:"reality_server_port,omitempty"`
RealityPrivateKey string `json:"reality_private_key,omitempty"`
RealityPublicKey string `json:"reality_public_key,omitempty"`
RealityShortId string `json:"reality_short_id,omitempty"`
Transport string `json:"transport,omitempty"`
Host string `json:"host,omitempty"`
Path string `json:"path,omitempty"`
ServiceName string `json:"service_name,omitempty"`
Cipher string `json:"cipher,omitempty"`
ServerKey string `json:"server_key,omitempty"`
Flow string `json:"flow,omitempty"`
HopPorts string `json:"hop_ports,omitempty"`
HopInterval int `json:"hop_interval,omitempty"`
ObfsPassword string `json:"obfs_password,omitempty"`
DisableSNI bool `json:"disable_sni,omitempty"`
ReduceRtt bool `json:"reduce_rtt,omitempty"`
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
CongestionController string `json:"congestion_controller,omitempty"`
Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high
PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme
UpMbps int `json:"up_mbps,omitempty"` // upload speed limit
DownMbps int `json:"down_mbps,omitempty"` // download speed limit
Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls'
ObfsHost string `json:"obfs_host,omitempty"` // obfs host
ObfsPath string `json:"obfs_path,omitempty"` // obfs path
XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode
XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path
Encryption string `json:"encryption,omitempty"` // encryption'none', 'mlkem768x25519plus'
EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode'native', 'xorpub', 'random'
EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt'0rtt', '1rtt'
EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket
EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding
EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key
EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding
EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password
Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1
CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none``http``dns``self`
CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate
CertDNSEnv string `json:"cert_dns_env"` // Environment for DNS provider
}
// Marshal protocol to json
func (m *Protocol) Marshal() ([]byte, error) {
type Alias Protocol
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(m),
})
}
// Unmarshal json to protocol
func (m *Protocol) Unmarshal(data []byte) error {
type Alias Protocol
aux := &struct {
*Alias
}{
Alias: (*Alias)(m),
}
return json.Unmarshal(data, &aux)
}
func reorderSortWithServer(tx *gorm.DB) error {
var servers []Server
if err := tx.Order("sort, id").Find(&servers).Error; err != nil {
return err
}
for i, server := range servers {
if server.Sort != i+1 {
if err := tx.Exec("UPDATE `servers` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil {
return err
}
}
}
return nil
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
+120 -36
View File
@@ -4,9 +4,9 @@ import (
"context"
"time"
"github.com/perfect-panel/ppanel-server/internal/model/payment"
"github.com/perfect-panel/server/internal/model/payment"
"github.com/perfect-panel/ppanel-server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -40,6 +40,13 @@ type Details struct {
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
type OrdersTotalWithDate struct {
Date string
AmountTotal int64
NewOrderAmount int64
RenewalOrderAmount int64
}
type customOrderLogicModel interface {
UpdateOrderStatus(ctx context.Context, orderNo string, status uint8, tx ...*gorm.DB) error
QueryOrderListByPage(ctx context.Context, page, size int, status uint8, user, subscribe int64, search string) (int64, []*Details, error)
@@ -47,11 +54,19 @@ type customOrderLogicModel interface {
FindOneDetailsByOrderNo(ctx context.Context, orderNo string) (*Details, error)
QueryMonthlyOrders(ctx context.Context, date time.Time) (OrdersTotal, error)
QueryDateOrders(ctx context.Context, date time.Time) (OrdersTotal, error)
QueryPendingOrders(ctx context.Context) ([]*Order, error)
QueryTotalOrders(ctx context.Context) (OrdersTotal, error)
QueryMonthlyUserCounts(ctx context.Context, date time.Time) (int64, int64, error)
QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error)
QueryTotalUserCounts(ctx context.Context) (int64, int64, error)
IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error)
QueryDailyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error)
QueryMonthlyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error)
}
// UserCounts User counts for new and renewal users
type UserCounts struct {
NewUsers int64 `gorm:"column:new_users"`
RenewalUsers int64 `gorm:"column:renewal_users"`
}
// NewModel returns a model for the database table.
@@ -156,51 +171,78 @@ func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time)
func (m *customOrderModel) QueryTotalOrders(ctx context.Context) (OrdersTotal, error) {
var result OrdersTotal
err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error {
err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, _ interface{}) error {
return conn.Model(&Order{}).
Select(`
SUM(amount) AS amount_total,
SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
`).
Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
Select(
"SUM(amount) as amount_total, " +
"SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " +
"SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount",
).
Scan(v).Error
Scan(&result).Error
})
return result, err
}
func (m *customOrderModel) QueryMonthlyUserCounts(ctx context.Context, date time.Time) (int64, int64, error) {
// 获取当月第一天零点
firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
lastDay := firstDay.AddDate(0, 1, -1)
// 获取下个月第一天零点(避免漏掉最后一天的订单)
nextMonth := firstDay.AddDate(0, 1, 0)
var newUsers int64
var renewalUsers int64
var counts UserCounts
// 执行查询
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
return conn.Model(&Order{}).
Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance").
Select(
"COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+
"COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users").
Row().Scan(&newUsers, &renewalUsers)
Select(`
COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
`).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, firstDay, nextMonth, "balance").
Scan(&counts).Error
})
return newUsers, renewalUsers, err
return counts.NewUsers, counts.RenewalUsers, err
}
func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) {
start := date.Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour).Add(-time.Nanosecond)
// 当天 00:00:00
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
// 下一天 00:00:00
nextDay := start.Add(24 * time.Hour)
var counts UserCounts
var newUsers int64
var renewalUsers int64
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
return conn.Model(&Order{}).
Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, start, end, "balance").
Select(
"COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+
"COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users").
Row().Scan(&newUsers, &renewalUsers)
Select(`
COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
`).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, start, nextDay, "balance").
Scan(&counts).Error
})
return newUsers, renewalUsers, err
return counts.NewUsers, counts.RenewalUsers, err
}
func (m *customOrderModel) QueryTotalUserCounts(ctx context.Context) (int64, int64, error) {
var counts UserCounts
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
return conn.Model(&Order{}).
Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
Select(`
COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
`).
Scan(&counts).Error
})
return counts.NewUsers, counts.RenewalUsers, err
}
func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error) {
@@ -213,12 +255,54 @@ func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID
return count == 0, err
}
func (m *customOrderModel) QueryPendingOrders(ctx context.Context) ([]*Order, error) {
var orderInfo []*Order
err := m.QueryNoCacheCtx(ctx, &orderInfo, func(conn *gorm.DB, v interface{}) error {
// QueryDailyOrdersList 查询当月每日订单统计
func (m *customOrderModel) QueryDailyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) {
var results []OrdersTotalWithDate
err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error {
// 当月 1 号 00:00:00
firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
// 第二天 00:00:00
nextDay := date.AddDate(0, 0, 1).Truncate(24 * time.Hour)
return conn.Model(&Order{}).
Where("status = ?", 1).
Find(v).Error
Select(`
DATE_FORMAT(created_at, '%Y-%m-%d') AS date,
SUM(amount) AS amount_total,
SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
`).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, firstDay, nextDay, "balance").
Group("DATE_FORMAT(created_at, '%Y-%m-%d')").
Order("date ASC").
Scan(v).Error
})
return orderInfo, err
return results, err
}
// QueryMonthlyOrdersList 查询过去 6 个月订单统计(包含当前月)
func (m *customOrderModel) QueryMonthlyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) {
var results []OrdersTotalWithDate
err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error {
// 六个月前(取月初)
start := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()).AddDate(0, -5, 0)
// 下个月月初
end := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()).AddDate(0, 1, 0)
return conn.Model(&Order{}).
Select(`
DATE_FORMAT(created_at, '%Y-%m') AS date,
SUM(amount) AS amount_total,
SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
`).
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
[]int64{2, 5}, start, end, "balance").
Group("DATE_FORMAT(created_at, '%Y-%m')").
Order("date ASC").
Scan(v).Error
})
return results, err
}
+18 -11
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -22,10 +22,10 @@ type (
customPaymentLogicModel
}
paymentModel interface {
Insert(ctx context.Context, data *Payment) error
Insert(ctx context.Context, data *Payment, tx ...*gorm.DB) error
FindOne(ctx context.Context, id int64) (*Payment, error)
Update(ctx context.Context, data *Payment) error
Delete(ctx context.Context, id int64) error
Update(ctx context.Context, data *Payment, tx ...*gorm.DB) error
Delete(ctx context.Context, id int64, tx ...*gorm.DB) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
@@ -67,8 +67,11 @@ func (m *defaultPaymentModel) getCacheKeys(data *Payment) []string {
return cacheKeys
}
func (m *defaultPaymentModel) Insert(ctx context.Context, data *Payment) error {
func (m *defaultPaymentModel) Insert(ctx context.Context, data *Payment, tx ...*gorm.DB) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Create(&data).Error
}, m.getCacheKeys(data)...)
return err
@@ -88,19 +91,21 @@ func (m *defaultPaymentModel) FindOne(ctx context.Context, id int64) (*Payment,
}
}
func (m *defaultPaymentModel) Update(ctx context.Context, data *Payment) error {
func (m *defaultPaymentModel) Update(ctx context.Context, data *Payment, tx ...*gorm.DB) error {
old, err := m.FindOne(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Save(data).Error
if len(tx) > 0 {
conn = tx[0]
}
return conn.Save(data).Error
}, m.getCacheKeys(old)...)
return err
}
func (m *defaultPaymentModel) Delete(ctx context.Context, id int64) error {
func (m *defaultPaymentModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error {
data, err := m.FindOne(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -109,8 +114,10 @@ func (m *defaultPaymentModel) Delete(ctx context.Context, id int64) error {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Delete(&Payment{}, id).Error
if len(tx) > 0 {
conn = tx[0]
}
return conn.Delete(&Payment{}, id).Error
}, m.getCacheKeys(data)...)
return err
}
+47 -26
View File
@@ -46,13 +46,19 @@ type StripeConfig struct {
Payment string `json:"payment"`
}
func (l *StripeConfig) Marshal() string {
b, _ := json.Marshal(l)
return string(b)
func (l *StripeConfig) Marshal() ([]byte, error) {
type Alias StripeConfig
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
}
func (l *StripeConfig) Unmarshal(s string) error {
return json.Unmarshal([]byte(s), l)
func (l *StripeConfig) Unmarshal(data []byte) error {
type Alias StripeConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
}
type AlipayF2FConfig struct {
@@ -63,13 +69,19 @@ type AlipayF2FConfig struct {
Sandbox bool `json:"sandbox"`
}
func (l *AlipayF2FConfig) Marshal() string {
b, _ := json.Marshal(l)
return string(b)
func (l *AlipayF2FConfig) Marshal() ([]byte, error) {
type Alias AlipayF2FConfig
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
}
func (l *AlipayF2FConfig) Unmarshal(s string) error {
return json.Unmarshal([]byte(s), l)
func (l *AlipayF2FConfig) Unmarshal(data []byte) error {
type Alias AlipayF2FConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
}
type EPayConfig struct {
@@ -78,29 +90,38 @@ type EPayConfig struct {
Key string `json:"key"`
}
func (l *EPayConfig) Marshal() string {
b, _ := json.Marshal(l)
return string(b)
func (l *EPayConfig) Marshal() ([]byte, error) {
type Alias EPayConfig
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
}
func (l *EPayConfig) Unmarshal(s string) error {
return json.Unmarshal([]byte(s), l)
func (l *EPayConfig) Unmarshal(data []byte) error {
type Alias EPayConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
}
type PayssionConfig struct {
PmId string `json:"pm_id"`
ApiKey string `json:"api_key"`
type CryptoSaaSConfig struct {
Endpoint string `json:"endpoint"`
AccountID string `json:"account_id"`
SecretKey string `json:"secret_key"`
Currency string `json:"currency"`
QueryUrl string `json:"query_url"`
CreateUrl string `json:"create_url"`
}
func (l *PayssionConfig) Marshal() string {
b, _ := json.Marshal(l)
return string(b)
func (l *CryptoSaaSConfig) Marshal() ([]byte, error) {
type Alias CryptoSaaSConfig
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
}
func (l *PayssionConfig) Unmarshal(s string) error {
return json.Unmarshal([]byte(s), l)
func (l *CryptoSaaSConfig) Unmarshal(data []byte) error {
type Alias CryptoSaaSConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
}
+26 -15
View File
@@ -5,9 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -23,10 +21,10 @@ type (
customServerLogicModel
}
serverModel interface {
Insert(ctx context.Context, data *Server) error
Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error
FindOne(ctx context.Context, id int64) (*Server, error)
Update(ctx context.Context, data *Server) error
Delete(ctx context.Context, id int64) error
Update(ctx context.Context, data *Server, tx ...*gorm.DB) error
Delete(ctx context.Context, id int64, tx ...*gorm.DB) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
@@ -62,23 +60,32 @@ func (m *defaultServerModel) batchGetCacheKeys(Servers ...*Server) []string {
return keys
}
func (m *defaultServerModel) getCacheKeys(data *Server) []string {
if data == nil {
return []string{}
}
detailsKey := fmt.Sprintf("%s%v", CacheServerDetailPrefix, data.Id)
ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, data.Id)
configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id)
//configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id)
//userIDKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, data.Id)
// query protocols to get config keys
cacheKeys := []string{
ServerIdKey,
detailsKey,
configIdKey,
//configIdKey,
//userIDKey,
}
return cacheKeys
}
func (m *defaultServerModel) Insert(ctx context.Context, data *Server) error {
func (m *defaultServerModel) Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Create(&data).Error
}, m.getCacheKeys(data)...)
return err
@@ -98,19 +105,21 @@ func (m *defaultServerModel) FindOne(ctx context.Context, id int64) (*Server, er
}
}
func (m *defaultServerModel) Update(ctx context.Context, data *Server) error {
func (m *defaultServerModel) Update(ctx context.Context, data *Server, tx ...*gorm.DB) error {
old, err := m.FindOne(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Save(data).Error
if len(tx) > 0 {
conn = tx[0]
}
return conn.Save(data).Error
}, m.getCacheKeys(old)...)
return err
}
func (m *defaultServerModel) Delete(ctx context.Context, id int64) error {
func (m *defaultServerModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error {
data, err := m.FindOne(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -119,8 +128,10 @@ func (m *defaultServerModel) Delete(ctx context.Context, id int64) error {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Delete(&Server{}, id).Error
if len(tx) > 0 {
conn = tx[0]
}
return conn.Delete(&Server{}, id).Error
}, m.getCacheKeys(data)...)
return err
}
+63 -12
View File
@@ -3,8 +3,8 @@ package server
import (
"context"
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/internal/config"
"gorm.io/gorm"
)
@@ -29,6 +29,10 @@ type customServerLogicModel interface {
UpdateRuleGroup(ctx context.Context, data *RuleGroup) error
DeleteRuleGroup(ctx context.Context, id int64) error
QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error)
FindServersByTag(ctx context.Context, tag string) ([]*Server, error)
FindServerTags(ctx context.Context) ([]string, error)
SetDefaultRuleGroup(ctx context.Context, id int64) error
}
var (
@@ -40,9 +44,10 @@ var (
// ClearCache Clear Cache
func (m *customServerModel) ClearCache(ctx context.Context, id int64) error {
serverIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id)
configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id)
//configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id)
//userListKey := fmt.Sprintf("%s%v", config.ServerUserListCacheKey, id)
return m.DelCacheCtx(ctx, serverIdKey, configKey)
return m.DelCacheCtx(ctx, serverIdKey)
}
// QueryServerCountByServerGroups Query Server Count By Server Groups
@@ -114,13 +119,16 @@ func (m *customServerModel) FindServerDetailByGroupIdsAndIds(ctx context.Context
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
conn = conn.
Model(&Server{}).
Where("enable = ?", true)
if len(groupId) > 0 {
conn = conn.Where("group_id IN ?", groupId)
}
if len(ids) > 0 {
conn = conn.Where("id IN ?", ids)
Where("`enable` = ?", true)
if len(groupId) > 0 && len(ids) > 0 {
// OR is used to connect group_id and id conditions
conn = conn.Where("(`group_id` IN ? OR `id` IN ?)", groupId, ids)
} else if len(groupId) > 0 {
conn = conn.Where("`group_id` IN ?", groupId)
} else if len(ids) > 0 {
conn = conn.Where("`id` IN ?", ids)
}
return conn.Order("sort ASC").Find(v).Error
})
return list, err
@@ -227,10 +235,16 @@ func (m *customServerModel) FindServerListByFilter(ctx context.Context, filter *
query = conn.Where("group_id = ?", filter.Group)
}
if filter.Search != "" {
query = query.Where("name LIKE ? OR server_addr LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%")
query = query.Where("name LIKE ? OR server_addr LIKE ? OR tags LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%", "%"+filter.Search+"%")
}
if filter.Tag != "" {
query = query.Where("tag LIKE ?", "%"+filter.Tag+"%")
if len(filter.Tags) > 0 {
for i, tag := range filter.Tags {
if i == 0 {
query = query.Where("tags LIKE ?", "%"+tag+"%")
} else {
query = query.Or("tags LIKE ?", "%"+tag+"%")
}
}
}
return query.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(v).Error
})
@@ -239,3 +253,40 @@ func (m *customServerModel) FindServerListByFilter(ctx context.Context, filter *
}
return total, data, nil
}
func (m *customServerModel) FindServerTags(ctx context.Context) ([]string, error) {
var data []string
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Distinct("tags").Pluck("tags", v).Error
})
var tags []string
for _, tag := range data {
if strings.Contains(tag, ",") {
tags = append(tags, strings.Split(tag, ",")...)
} else {
tags = append(tags, tag)
}
}
return tags, err
}
func (m *customServerModel) FindServersByTag(ctx context.Context, tag string) ([]*Server, error) {
var data []*Server
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Server{}).Where("FIND_IN_SET(?, tags)", tag).Order("sort ASC").Find(v).Error
})
return data, err
}
// SetDefaultRuleGroup sets the default rule group.
func (m *customServerModel) SetDefaultRuleGroup(ctx context.Context, id int64) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
// Reset all groups to not default
if err := conn.Model(&RuleGroup{}).Where("`id` != ?", id).Update("default", false).Error; err != nil {
return err
}
// Set the specified group as default
return conn.Model(&RuleGroup{}).Where("`id` = ?", id).Update("default", true).Error
}, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", id))
}
+35 -26
View File
@@ -3,26 +3,30 @@ package server
import (
"time"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/gorm"
)
const (
RelayModeNone = "none"
RelayModeAll = "all"
RelayModeRandom = "random"
RelayModeNone = "none"
RelayModeAll = "all"
RelayModeRandom = "random"
RuleGroupTypeReject = "reject"
RuleGroupTypeDefault = "default"
RuleGroupTypeDirect = "direct"
)
type ServerFilter struct {
Id int64
Tag string
Tags []string
Group int64
Search string
Page int
Size int
}
// Deprecated: use internal/model/node/server.go
type Server struct {
Id int64 `gorm:"primary_key"`
Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"`
@@ -52,33 +56,32 @@ func (*Server) TableName() string {
func (s *Server) BeforeDelete(tx *gorm.DB) error {
logger.Debugf("[Server] BeforeDelete")
if err := tx.Exec("UPDATE `server` SET sort = sort - 1 WHERE sort > ?", s.Sort).Error; err != nil {
return err
}
// 删除后重新排序,防止因 sort 缺口导致问题
if err := reorderSort(tx); err != nil {
return err
}
return nil
}
func (s *Server) BeforeUpdate(tx *gorm.DB) error {
logger.Debugf("[Server] BeforeUpdate")
var count int64
if err := tx.Model(&Server{}).Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil {
if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}).
Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
logger.Debugf("[Server] Duplicate sort found, reordering...")
if count > 1 {
// reorder sort
if err := reorderSort(tx); err != nil {
logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error())
return err
}
// get max sort
var maxSort int64
if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil {
return err
}
s.Sort = maxSort + 1
}
return nil
}
@@ -136,6 +139,15 @@ type Hysteria2 struct {
}
type Tuic struct {
Port int `json:"port"`
DisableSNI bool `json:"disable_sni"`
ReduceRtt bool `json:"reduce_rtt"`
UDPRelayMode string `json:"udp_relay_mode"`
CongestionController string `json:"congestion_controller"`
SecurityConfig SecurityConfig `json:"security_config"`
}
type AnyTLS struct {
Port int `json:"port"`
SecurityConfig SecurityConfig `json:"security_config"`
}
@@ -179,9 +191,11 @@ type RuleGroup struct {
Id int64 `gorm:"primary_key"`
Icon string `gorm:"type:MEDIUMTEXT;comment:Rule Group Icon"`
Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Name"`
Type string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Type"`
Tags string `gorm:"type:text;comment:Selected Node Tags"`
Rules string `gorm:"type:MEDIUMTEXT;comment:Rules"`
Enable bool `gorm:"type:tinyint(1);not null;default:1;comment:Rule Group Enable"`
Default bool `gorm:"type:tinyint(1);not null;default:0;comment:Rule Group is Default"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
@@ -189,19 +203,14 @@ type RuleGroup struct {
func (RuleGroup) TableName() string {
return "server_rule_group"
}
func reorderSort(tx *gorm.DB) error {
var servers []*Server
if err := tx.Model(&Server{}).Order("sort ASC").Find(&servers).Error; err != nil {
var servers []Server
if err := tx.Order("sort, id").Find(&servers).Error; err != nil {
return err
}
for i, server := range servers {
newSort := int64(i + 1)
if server.Sort != newSort {
if err := tx.Model(&Server{}).
Where("id = ?", server.Id).
Update("sort", newSort).Error; err != nil {
if server.Sort != int64(i)+1 {
if err := tx.Exec("UPDATE `server` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil {
return err
}
}
+31 -5
View File
@@ -4,8 +4,11 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/pkg/cache"
"github.com/perfect-panel/server/pkg/tool"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -57,11 +60,34 @@ func (m *defaultSubscribeModel) getCacheKeys(data *Subscribe) []string {
if data == nil {
return []string{}
}
SubscribeIdKey := fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id)
cacheKeys := []string{
SubscribeIdKey,
var keys []string
if data.Nodes != "" {
var nodes []*node.Node
ids := strings.Split(data.Nodes, ",")
err := m.QueryNoCacheCtx(context.Background(), &nodes, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&node.Node{}).Where("id IN (?)", tool.StringSliceToInt64Slice(ids)).Find(&nodes).Error
})
if err == nil {
for _, n := range nodes {
keys = append(keys, fmt.Sprintf("%s%d", node.ServerUserListCacheKey, n.ServerId))
}
}
}
return cacheKeys
if data.NodeTags != "" {
var nodes []*node.Node
tags := tool.RemoveDuplicateElements(strings.Split(data.NodeTags, ",")...)
err := m.QueryNoCacheCtx(context.Background(), &nodes, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&node.Node{}).Scopes(InSet("tags", tags)).Find(&nodes).Error
})
if err == nil {
for _, n := range nodes {
keys = append(keys, fmt.Sprintf("%s%d", node.ServerUserListCacheKey, n.ServerId))
}
}
}
return append(keys, fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id))
}
func (m *defaultSubscribeModel) Insert(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error {
+127 -80
View File
@@ -3,38 +3,37 @@ package subscribe
import (
"context"
"github.com/perfect-panel/server/pkg/tool"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// type Details struct {
// Id int64 `gorm:"primaryKey"`
// Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"`
// Description string `gorm:"type:text;comment:Subscribe Description"`
// UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"`
// UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"`
// Discount string `gorm:"type:text;comment:Discount"`
// Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"`
// Inventory int64 `gorm:"type:int;not null;default:0;comment:Inventory"`
// Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"`
// SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"`
// DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"`
// GroupId int64 `gorm:"type:bigint;comment:Group Id"`
// Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"`
// Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show"`
// Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"`
// DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"`
// PurchaseWithDiscount bool `gorm:"type:tinyint(1);default:0;comment:PurchaseWithDiscount"`
// ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle"`
// RenewalReset bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"`
// }
type FilterParams struct {
Page int // Page Number
Size int // Page Size
Ids []int64 // Subscribe IDs
Node []int64 // Node IDs
Tags []string // Node Tags
Show bool // Show Portal Page
Sell bool // Sell
Language string // Language
DefaultLanguage bool // Default Subscribe Language Data
Search string // Search Keywords
}
func (p *FilterParams) Normalize() {
if p.Page <= 0 {
p.Page = 1
}
if p.Size <= 0 {
p.Size = 10
}
}
type customSubscribeLogicModel interface {
QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error)
QuerySubscribeList(ctx context.Context) ([]*Subscribe, error)
QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error)
QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error)
FilterList(ctx context.Context, params *FilterParams) (int64, []*Subscribe, error)
ClearCache(ctx context.Context, id ...int64) error
QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error)
QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error)
}
// NewModel returns a model for the database table.
@@ -44,54 +43,6 @@ func NewModel(conn *gorm.DB, c *redis.Client) Model {
}
}
// QuerySubscribeListByPage Get Subscribe List
func (m *customSubscribeModel) QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) {
err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
// About to be abandoned
_ = conn.Model(&Subscribe{}).
Where("sort = ?", 0).
Update("sort", gorm.Expr("id"))
conn = conn.Model(&Subscribe{})
if group > 0 {
conn = conn.Where("group_id = ?", group)
}
if search != "" {
conn = conn.Where("`name` like ? or `description` like ?", "%"+search+"%", "%"+search+"%")
}
return conn.Count(&total).Order("sort ASC").Limit(size).Offset((page - 1) * size).Find(v).Error
})
return total, list, err
}
// QuerySubscribeList Get Subscribe List
func (m *customSubscribeModel) QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) {
var list []*Subscribe
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
conn = conn.Model(&Subscribe{})
return conn.Where("`sell` = true").Order("sort ").Find(v).Error
})
return list, err
}
func (m *customSubscribeModel) QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) {
var data []*Subscribe
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Subscribe{}).Where("FIND_IN_SET(?, server)", serverId).Or("FIND_IN_SET(?, server_group)", serverGroupId).Find(v).Error
})
return data, err
}
// QuerySubscribeListByShow Get Subscribe List By Show
func (m *customSubscribeModel) QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) {
var list []*Subscribe
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
conn = conn.Model(&Subscribe{})
return conn.Where("`show` = true").Find(v).Error
})
return list, err
}
func (m *customSubscribeModel) QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) {
var minSort int64
err := m.QueryNoCacheCtx(ctx, &minSort, func(conn *gorm.DB, v interface{}) error {
@@ -100,10 +51,106 @@ func (m *customSubscribeModel) QuerySubscribeMinSortByIds(ctx context.Context, i
return minSort, err
}
func (m *customSubscribeModel) QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) {
var list []*Subscribe
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Subscribe{}).Where("id IN ?", ids).Find(v).Error
})
return list, err
func (m *customSubscribeModel) ClearCache(ctx context.Context, ids ...int64) error {
if len(ids) <= 0 {
return nil
}
var cacheKeys []string
for _, id := range ids {
data, err := m.FindOne(ctx, id)
if err != nil {
return err
}
cacheKeys = append(cacheKeys, m.getCacheKeys(data)...)
}
return m.CachedConn.DelCacheCtx(ctx, cacheKeys...)
}
// FilterList Filter Subscribe List
func (m *customSubscribeModel) FilterList(ctx context.Context, params *FilterParams) (int64, []*Subscribe, error) {
if params == nil {
params = &FilterParams{}
}
params.Normalize()
var list []*Subscribe
var total int64
// 构建查询函数
buildQuery := func(conn *gorm.DB, lang string) *gorm.DB {
query := conn.Model(&Subscribe{})
if params.Search != "" {
s := "%" + params.Search + "%"
query = query.Where("`name` LIKE ? OR `description` LIKE ?", s, s)
}
if params.Show {
query = query.Where("`show` = true")
}
if params.Sell {
query = query.Where("`sell` = true")
}
if len(params.Ids) > 0 {
query = query.Where("id IN ?", params.Ids)
}
if len(params.Node) > 0 {
query = query.Scopes(InSet("nodes", tool.Int64SliceToStringSlice(params.Node)))
}
if len(params.Tags) > 0 {
query = query.Scopes(InSet("node_tags", params.Tags))
}
if lang != "" {
query = query.Where("language = ?", lang)
} else if params.DefaultLanguage {
query = query.Where("language = ''")
}
return query
}
// 查询数据
queryFunc := func(lang string) error {
return m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
query := buildQuery(conn, lang)
if err := query.Count(&total).Error; err != nil {
return err
}
return query.Order("sort ASC").
Limit(params.Size).
Offset((params.Page - 1) * params.Size).
Find(v).Error
})
}
err := queryFunc(params.Language)
if err != nil {
return 0, nil, err
}
// fallback 默认语言
if params.DefaultLanguage && total == 0 {
err = queryFunc("")
if err != nil {
return 0, nil, err
}
}
return total, list, nil
}
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if len(values) == 0 {
return db
}
query := db.Where("1=0")
for _, v := range values {
query = query.Or("FIND_IN_SET(?, "+field+")", v)
}
return query
}
}
+25 -3
View File
@@ -9,6 +9,7 @@ import (
type Subscribe struct {
Id int64 `gorm:"primaryKey"`
Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"`
Language string `gorm:"type:varchar(255);not null;default:'';comment:Language"`
Description string `gorm:"type:text;comment:Subscribe Description"`
UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"`
UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"`
@@ -19,9 +20,8 @@ type Subscribe struct {
SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"`
DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"`
Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"`
GroupId int64 `gorm:"type:bigint;comment:Group Id"`
ServerGroup string `gorm:"type:varchar(255);comment:Server Group"`
Server string `gorm:"type:varchar(255);comment:Server"`
Nodes string `gorm:"type:varchar(255);comment:Node Ids"`
NodeTags string `gorm:"type:varchar(255);comment:Node Tags"`
Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"`
Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"`
Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"`
@@ -48,6 +48,28 @@ func (s *Subscribe) BeforeCreate(tx *gorm.DB) error {
return nil
}
func (s *Subscribe) BeforeDelete(tx *gorm.DB) error {
if err := tx.Exec("UPDATE `subscribe` SET sort = sort - 1 WHERE sort > ?", s.Sort).Error; err != nil {
return err
}
return nil
}
func (s *Subscribe) BeforeUpdate(tx *gorm.DB) error {
var count int64
if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Subscribe{}).
Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil {
return err
}
if count > 0 {
var maxSort int64
if err := tx.Model(&Subscribe{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil {
return err
}
s.Sort = maxSort + 1
}
return nil
}
type Discount struct {
Months int64 `json:"months"`
Discount int64 `json:"discount"`
-117
View File
@@ -1,117 +0,0 @@
package subscribeType
import (
"context"
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
var _ Model = (*customSubscribeTypeModel)(nil)
var (
cacheSubscribeTypeIdPrefix = "cache:subscribeType:id:"
)
type (
Model interface {
subscribeTypeModel
customSubscribeTypeLogicModel
}
subscribeTypeModel interface {
Insert(ctx context.Context, data *SubscribeType) error
FindOne(ctx context.Context, id int64) (*SubscribeType, error)
Update(ctx context.Context, data *SubscribeType) error
Delete(ctx context.Context, id int64) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
customSubscribeTypeModel struct {
*defaultSubscribeTypeModel
}
defaultSubscribeTypeModel struct {
cache.CachedConn
table string
}
)
func newSubscribeTypeModel(db *gorm.DB, c *redis.Client) *defaultSubscribeTypeModel {
return &defaultSubscribeTypeModel{
CachedConn: cache.NewConn(db, c),
table: "`SubscribeType`",
}
}
//nolint:unused
func (m *defaultSubscribeTypeModel) batchGetCacheKeys(SubscribeTypes ...*SubscribeType) []string {
var keys []string
for _, subscribeType := range SubscribeTypes {
keys = append(keys, m.getCacheKeys(subscribeType)...)
}
return keys
}
func (m *defaultSubscribeTypeModel) getCacheKeys(data *SubscribeType) []string {
if data == nil {
return []string{}
}
SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, data.Id)
cacheKeys := []string{
SubscribeTypeIdKey,
}
return cacheKeys
}
func (m *defaultSubscribeTypeModel) Insert(ctx context.Context, data *SubscribeType) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Create(&data).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultSubscribeTypeModel) FindOne(ctx context.Context, id int64) (*SubscribeType, error) {
SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, id)
var resp SubscribeType
err := m.QueryCtx(ctx, &resp, SubscribeTypeIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&SubscribeType{}).Where("`id` = ?", id).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultSubscribeTypeModel) Update(ctx context.Context, data *SubscribeType) error {
old, err := m.FindOne(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Save(data).Error
}, m.getCacheKeys(old)...)
return err
}
func (m *defaultSubscribeTypeModel) Delete(ctx context.Context, id int64) error {
data, err := m.FindOne(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Delete(&SubscribeType{}, id).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultSubscribeTypeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
return m.TransactCtx(ctx, fn)
}
-16
View File
@@ -1,16 +0,0 @@
package subscribeType
import (
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type customSubscribeTypeLogicModel interface {
}
// NewModel returns a model for the database table.
func NewModel(conn *gorm.DB, c *redis.Client) Model {
return &customSubscribeTypeModel{
defaultSubscribeTypeModel: newSubscribeTypeModel(conn, c),
}
}
@@ -1,15 +0,0 @@
package subscribeType
import "time"
type SubscribeType struct {
Id int64 `gorm:"primary_key"`
Name string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"`
Mark string `gorm:"type:varchar(255);default:'';not null;comment:订阅标识"`
CreatedAt time.Time `gorm:"<-:create;comment:创建时间"`
UpdatedAt time.Time `gorm:"comment:更新时间"`
}
func (SubscribeType) TableName() string {
return "subscribe_type"
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
+11 -1
View File
@@ -3,7 +3,7 @@ package system
import (
"context"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/server/internal/config"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -19,6 +19,7 @@ type customSystemLogicModel interface {
GetTosConfig(ctx context.Context) ([]*System, error)
GetCurrencyConfig(ctx context.Context) ([]*System, error)
GetVerifyCodeConfig(ctx context.Context) ([]*System, error)
GetLogConfig(ctx context.Context) ([]*System, error)
UpdateNodeMultiplierConfig(ctx context.Context, config string) error
FindNodeMultiplierConfig(ctx context.Context) (*System, error)
}
@@ -152,3 +153,12 @@ func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System,
})
return configs, err
}
// GetLogConfig returns the log config.
func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) {
var configs []*System
err := m.QueryNoCacheCtx(ctx, &configs, func(conn *gorm.DB, v interface{}) error {
return conn.Where("`category` = ?", "log").Find(v).Error
})
return configs, err
}
+151
View File
@@ -0,0 +1,151 @@
package task
import (
"encoding/json"
"time"
)
type Type int8
const (
Undefined Type = -1
TypeEmail = iota
TypeQuota
)
type Task struct {
Id int64 `gorm:"primaryKey;autoIncrement;comment:ID"`
Type int8 `gorm:"not null;comment:Task Type"`
Scope string `gorm:"type:text;comment:Task Scope"`
Content string `gorm:"type:text;comment:Task Content"`
Status int8 `gorm:"not null;default:0;comment:Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed"`
Errors string `gorm:"type:text;comment:Task Errors"`
Total uint64 `gorm:"column:total;not null;default:0;comment:Total Number"`
Current uint64 `gorm:"column:current;not null;default:0;comment:Current Number"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (Task) TableName() string {
return "task"
}
type ScopeType int8
const (
ScopeAll ScopeType = iota + 1 // All users
ScopeActive // Active users
ScopeExpired // Expired users
ScopeNone // No Subscribe
ScopeSkip // Skip user filtering
)
func (t ScopeType) Int8() int8 {
return int8(t)
}
type EmailScope struct {
Type int8 `gorm:"not null;comment:Scope Type"`
RegisterStartTime int64 `json:"register_start_time"`
RegisterEndTime int64 `json:"register_end_time"`
Recipients []string `json:"recipients"` // list of email addresses
Additional []string `json:"additional"` // additional email addresses
Scheduled int64 `json:"scheduled"` // scheduled time (unix timestamp)
Interval uint8 `json:"interval"` // interval in seconds
Limit uint64 `json:"limit"` // daily send limit
}
func (s *EmailScope) Marshal() ([]byte, error) {
type Alias EmailScope
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
func (s *EmailScope) Unmarshal(data []byte) error {
type Alias EmailScope
aux := (*Alias)(s)
return json.Unmarshal(data, &aux)
}
type EmailContent struct {
Subject string `json:"subject"`
Content string `json:"content"`
}
func (c *EmailContent) Marshal() ([]byte, error) {
type Alias EmailContent
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(c),
})
}
func (c *EmailContent) Unmarshal(data []byte) error {
type Alias EmailContent
aux := (*Alias)(c)
return json.Unmarshal(data, &aux)
}
type QuotaScope struct {
Subscribers []int64 `json:"subscribers"` // Subscribe IDs
IsActive *bool `json:"is_active"` // filter by active status
StartTime int64 `json:"start_time"` // filter by subscription start time
EndTime int64 `json:"end_time"` // filter by subscription end time
Objects []int64 `json:"recipients"` // list of user subs IDs
}
func (s *QuotaScope) Marshal() ([]byte, error) {
type Alias QuotaScope
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(s),
})
}
func (s *QuotaScope) Unmarshal(data []byte) error {
type Alias QuotaScope
aux := (*Alias)(s)
return json.Unmarshal(data, &aux)
}
type QuotaContent struct {
ResetTraffic bool `json:"reset_traffic"` // whether to reset traffic
Days uint64 `json:"days,omitempty"` // days to add
GiftType uint8 `json:"gift_type,omitempty"` // 1: Fixed, 2: Ratio
GiftValue uint64 `json:"gift_value,omitempty"` // value of the gift type
}
func (c *QuotaContent) Marshal() ([]byte, error) {
type Alias QuotaContent
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(c),
})
}
func (c *QuotaContent) Unmarshal(data []byte) error {
type Alias QuotaContent
aux := (*Alias)(c)
return json.Unmarshal(data, &aux)
}
func ParseScopeType(t int8) ScopeType {
switch t {
case 1:
return ScopeAll
case 2:
return ScopeActive
case 3:
return ScopeExpired
case 4:
return ScopeNone
default:
return ScopeSkip
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
+29 -2
View File
@@ -3,6 +3,7 @@ package user
import (
"context"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/gorm"
)
@@ -31,24 +32,50 @@ func (m *defaultUserModel) FindUserAuthMethodByPlatform(ctx context.Context, use
}
func (m *defaultUserModel) InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error {
u, err := m.FindOne(ctx, data.UserId)
if err != nil {
return err
}
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&AuthMethods{}).Create(data).Error
if err = conn.Model(&AuthMethods{}).Create(data).Error; err != nil {
return err
}
return m.ClearUserCache(ctx, u)
})
}
func (m *defaultUserModel) UpdateUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error {
u, err := m.FindOne(ctx, data.UserId)
if err != nil {
return err
}
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", data.UserId, data.AuthType).Save(data).Error
err = conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", data.UserId, data.AuthType).Save(data).Error
if err != nil {
return err
}
return m.ClearUserCache(ctx, u)
})
}
func (m *defaultUserModel) DeleteUserAuthMethods(ctx context.Context, userId int64, platform string, tx ...*gorm.DB) error {
u, err := m.FindOne(ctx, userId)
if err != nil {
return err
}
defer func() {
if err = m.ClearUserCache(context.Background(), u); err != nil {
logger.Errorf("[UserModel] clear user cache failed: %v", err.Error())
}
}()
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
+285
View File
@@ -0,0 +1,285 @@
package user
import (
"context"
"fmt"
"github.com/perfect-panel/server/pkg/logger"
)
type CacheKeyGenerator interface {
GetCacheKeys() []string
}
type CacheManager interface {
ClearCache(ctx context.Context, keys ...string) error
ClearModelCache(ctx context.Context, models ...CacheKeyGenerator) error
}
type UserCacheManager struct {
model *defaultUserModel
}
func NewUserCacheManager(model *defaultUserModel) *UserCacheManager {
return &UserCacheManager{
model: model,
}
}
func (c *UserCacheManager) ClearCache(ctx context.Context, keys ...string) error {
if len(keys) == 0 {
return nil
}
return c.model.CachedConn.DelCacheCtx(ctx, keys...)
}
func (c *UserCacheManager) ClearModelCache(ctx context.Context, models ...CacheKeyGenerator) error {
var allKeys []string
for _, model := range models {
if model != nil {
allKeys = append(allKeys, model.GetCacheKeys()...)
}
}
return c.ClearCache(ctx, allKeys...)
}
func (u *User) GetCacheKeys() []string {
if u == nil {
return []string{}
}
keys := []string{
fmt.Sprintf("%s%d", cacheUserIdPrefix, u.Id),
}
for _, auth := range u.AuthMethods {
if auth.AuthType == "email" {
keys = append(keys, fmt.Sprintf("%s%s", cacheUserEmailPrefix, auth.AuthIdentifier))
break
}
}
return keys
}
func (s *Subscribe) GetCacheKeys() []string {
if s == nil {
return []string{}
}
keys := make([]string, 0)
if s.Token != "" {
keys = append(keys, fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, s.Token))
}
if s.UserId != 0 {
keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, s.UserId))
}
if s.Id != 0 {
keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, s.Id))
}
return keys
}
func (s *Subscribe) GetExtendedCacheKeys(model *defaultUserModel) []string {
keys := s.GetCacheKeys()
if s.SubscribeId != 0 && model != nil {
serverKeys := model.getServerRelatedCacheKeys(s.SubscribeId)
keys = append(keys, serverKeys...)
}
return keys
}
func (d *Device) GetCacheKeys() []string {
if d == nil {
return []string{}
}
keys := []string{}
if d.Id != 0 {
keys = append(keys, fmt.Sprintf("%s%d", cacheUserDeviceIdPrefix, d.Id))
}
if d.Identifier != "" {
keys = append(keys, fmt.Sprintf("%s%s", cacheUserDeviceNumberPrefix, d.Identifier))
}
return keys
}
func (a *AuthMethods) GetCacheKeys() []string {
if a == nil {
return []string{}
}
keys := []string{}
if a.UserId != 0 {
keys = append(keys, fmt.Sprintf("%s%d", cacheUserIdPrefix, a.UserId))
}
if a.AuthType == "email" && a.AuthIdentifier != "" {
keys = append(keys, fmt.Sprintf("%s%s", cacheUserEmailPrefix, a.AuthIdentifier))
}
return keys
}
func (m *defaultUserModel) GetCacheManager() *UserCacheManager {
return NewUserCacheManager(m)
}
func (m *defaultUserModel) getServerRelatedCacheKeys(subscribeId int64) []string {
// 这里复用了 model.go 中的逻辑,但简化了实现
keys := []string{}
if subscribeId == 0 {
return keys
}
// 这里需要从 getSubscribeCacheKey 方法中提取服务器相关的逻辑
// 为了避免重复查询,我们可以在需要时才获取
// 或者可以将这个逻辑移到一个统一的地方
return keys
}
func (m *defaultUserModel) ClearUserCache(ctx context.Context, users ...*User) error {
cacheManager := m.GetCacheManager()
models := make([]CacheKeyGenerator, len(users))
for i, user := range users {
models[i] = user
}
return cacheManager.ClearModelCache(ctx, models...)
}
func (m *defaultUserModel) ClearSubscribeCacheByModels(ctx context.Context, subscribes ...*Subscribe) error {
cacheManager := m.GetCacheManager()
models := make([]CacheKeyGenerator, len(subscribes))
for i, subscribe := range subscribes {
models[i] = subscribe
}
return cacheManager.ClearModelCache(ctx, models...)
}
func (m *defaultUserModel) ClearDeviceCache(ctx context.Context, devices ...*Device) error {
cacheManager := m.GetCacheManager()
models := make([]CacheKeyGenerator, len(devices))
for i, device := range devices {
models[i] = device
}
return cacheManager.ClearModelCache(ctx, models...)
}
func (m *defaultUserModel) ClearAuthMethodCache(ctx context.Context, authMethods ...*AuthMethods) error {
cacheManager := m.GetCacheManager()
models := make([]CacheKeyGenerator, len(authMethods))
for i, auth := range authMethods {
models[i] = auth
}
return cacheManager.ClearModelCache(ctx, models...)
}
func (m *defaultUserModel) BatchClearRelatedCache(ctx context.Context, user *User) error {
if user == nil {
return nil
}
cacheManager := m.GetCacheManager()
var allModels []CacheKeyGenerator
allModels = append(allModels, user)
for _, auth := range user.AuthMethods {
allModels = append(allModels, &auth)
}
for _, device := range user.UserDevices {
allModels = append(allModels, &device)
}
subscribes, err := m.QueryUserSubscribe(ctx, user.Id)
if err != nil {
logger.Errorf("failed to query user subscribes for cache clearing: %v", err)
} else {
for _, sub := range subscribes {
subModel := &Subscribe{
Id: sub.Id,
UserId: sub.UserId,
Token: sub.Token,
SubscribeId: sub.SubscribeId,
}
allModels = append(allModels, subModel)
}
}
return cacheManager.ClearModelCache(ctx, allModels...)
}
func (m *defaultUserModel) CacheInvalidationHandler(ctx context.Context, operation string, modelType string, model interface{}) error {
switch operation {
case "create", "update", "delete":
switch modelType {
case "user":
if user, ok := model.(*User); ok {
return m.BatchClearRelatedCache(ctx, user)
}
case "subscribe":
if subscribe, ok := model.(*Subscribe); ok {
return m.ClearSubscribeCacheByModels(ctx, subscribe)
}
case "device":
if device, ok := model.(*Device); ok {
return m.ClearDeviceCache(ctx, device)
}
case "authmethod":
if authMethod, ok := model.(*AuthMethods); ok {
return m.ClearAuthMethodCache(ctx, authMethod)
}
}
}
return nil
}
func (m *customUserModel) GetRelatedCacheKeys(ctx context.Context, modelType string, modelId int64) ([]string, error) {
var keys []string
switch modelType {
case "user":
user, err := m.FindOne(ctx, modelId)
if err != nil {
return nil, err
}
keys = append(keys, user.GetCacheKeys()...)
auths, err := m.FindUserAuthMethods(ctx, modelId)
if err == nil {
for _, auth := range auths {
keys = append(keys, auth.GetCacheKeys()...)
}
}
subscribes, err := m.QueryUserSubscribe(ctx, modelId)
if err == nil {
for _, sub := range subscribes {
subModel := &Subscribe{
Id: sub.Id,
UserId: sub.UserId,
Token: sub.Token,
SubscribeId: sub.SubscribeId,
}
keys = append(keys, subModel.GetCacheKeys()...)
}
}
case "subscribe":
subscribe, err := m.FindOneSubscribe(ctx, modelId)
if err != nil {
return nil, err
}
keys = append(keys, subscribe.GetCacheKeys()...)
case "device":
device, err := m.FindOneDevice(ctx, modelId)
if err != nil {
return nil, err
}
keys = append(keys, device.GetCacheKeys()...)
}
return keys, nil
}
+37 -61
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"github.com/perfect-panel/ppanel-server/pkg/cache"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -48,29 +48,20 @@ func newUserModel(db *gorm.DB, c *redis.Client) *defaultUserModel {
func (m *defaultUserModel) batchGetCacheKeys(users ...*User) []string {
var keys []string
for _, user := range users {
keys = append(keys, m.getCacheKeys(user)...)
keys = append(keys, user.GetCacheKeys()...)
}
return keys
}
func (m *defaultUserModel) getCacheKeys(data *User) []string {
if data == nil {
return []string{}
}
userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
cacheKeys := []string{
userIdKey,
}
// email key
if len(data.AuthMethods) > 0 {
for _, auth := range data.AuthMethods {
if auth.AuthType == "email" {
cacheKeys = append(cacheKeys, fmt.Sprintf("%s%v", cacheUserEmailPrefix, auth.AuthIdentifier))
break
}
}
}
return cacheKeys
return data.GetCacheKeys()
}
func (m *defaultUserModel) clearUserCache(ctx context.Context, data ...*User) error {
return m.ClearUserCache(ctx, data...)
}
func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*User, error) {
@@ -127,53 +118,38 @@ func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB)
}
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
// 使用批量相关缓存清理,包含所有相关数据的缓存
defer func() {
if clearErr := m.BatchClearRelatedCache(ctx, data); clearErr != nil {
// 记录清理缓存错误,但不阻断删除操作
}
return conn.Transaction(func(db *gorm.DB) error {
if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&BalanceLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&GiftAmountLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&LoginLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&SubscribeLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
}()
subs, err := m.QueryUserSubscribe(ctx, id)
if err != nil {
return err
}
for _, sub := range subs {
if err := m.DeleteSubscribeById(ctx, sub.Id, db); err != nil {
return err
}
}
return m.TransactCtx(ctx, func(db *gorm.DB) error {
if len(tx) > 0 {
db = tx[0]
}
if err := db.Model(&CommissionLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
return nil
})
}, m.getCacheKeys(data)...)
return err
// 删除用户相关的所有数据
if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil {
return err
}
if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&AuthMethods{}).Error; err != nil {
return err
}
if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&Subscribe{}).Error; err != nil {
return err
}
if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&Device{}).Error; err != nil {
return err
}
return nil
})
}
func (m *defaultUserModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
+27 -4
View File
@@ -46,18 +46,27 @@ func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subsc
return list, total, err
}
// QueryDeviceList returns a list of records that meet the conditions.
func (m *customUserModel) QueryDeviceList(ctx context.Context, userId int64) ([]*Device, int64, error) {
var list []*Device
var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Device{}).Where("`user_id` = ? and `subscribe_id` = ?", userId).Count(&total).Find(&list).Error
})
return list, total, err
}
func (m *customUserModel) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error {
old, err := m.FindOneDevice(ctx, data.Id)
if err != nil {
return err
}
deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, old.Id)
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Save(data).Error
}, deviceIdKey)
}, old.GetCacheKeys()...)
return err
}
@@ -69,12 +78,26 @@ func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gor
}
return err
}
deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, data.Id)
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Delete(&Device{}, id).Error
}, deviceIdKey)
}, data.GetCacheKeys()...)
return err
}
func (m *customUserModel) InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error {
defer func() {
if clearErr := m.ClearDeviceCache(ctx, data); clearErr != nil {
// log cache clear error
}
}()
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Create(data).Error
})
}
-81
View File
@@ -1,81 +0,0 @@
package user
import (
"context"
"github.com/pkg/errors"
"gorm.io/gorm"
)
func (m *customUserModel) InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error {
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
return conn.Create(log).Error
})
}
func (m *customUserModel) FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error) {
var list []*SubscribeLog
var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
query := conn.Model(&SubscribeLog{})
if filter != nil {
if filter.UserId != 0 {
query = query.Where("user_id = ?", filter.UserId)
}
if filter.UserSubscribeId != 0 {
query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId)
}
if filter.IP != "" {
query = query.Where("ip LIKE ?", "%"+filter.IP+"%")
}
if filter.Token != "" {
query = query.Where("token LIKE ?", "%"+filter.Token+"%")
}
if filter.UserAgent != "" {
query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%")
}
}
return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, err
}
return list, total, nil
}
func (m *customUserModel) InsertLoginLog(ctx context.Context, log *LoginLog) error {
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
return conn.Create(log).Error
})
}
func (m *customUserModel) FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error) {
var list []*LoginLog
var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
query := conn.Model(&LoginLog{})
if filter != nil {
if filter.UserId != 0 {
query = query.Where("user_id = ?", filter.UserId)
}
if filter.IP != "" {
query = query.Where("ip LIKE ?", "%"+filter.IP+"%")
}
if filter.UserAgent != "" {
query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%")
}
if filter.Success != nil {
query = query.Where("success = ?", *filter.Success)
}
}
return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error
})
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, err
}
return list, total, nil
}
+98 -172
View File
@@ -2,15 +2,12 @@ package user
import (
"context"
"errors"
"fmt"
"time"
"github.com/perfect-panel/ppanel-server/internal/config"
"github.com/perfect-panel/ppanel-server/internal/model/server"
"github.com/perfect-panel/ppanel-server/internal/model/subscribe"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
@@ -32,6 +29,7 @@ type SubscribeDetails struct {
Subscribe *subscribe.Subscribe `gorm:"foreignKey:SubscribeId;references:Id"`
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"`
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
Traffic int64 `gorm:"default:0;comment:Traffic"`
Download int64 `gorm:"default:0;comment:Download Traffic"`
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
@@ -62,6 +60,7 @@ type UserFilterParams struct {
UserId *int64
SubscribeId *int64
UserSubscribeId *int64
Order string // Order by id, e.g., "desc"
}
type customUserLogicModel interface {
@@ -78,7 +77,6 @@ type customUserLogicModel interface {
QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error)
FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error)
FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error)
InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error
FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error)
UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error
QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error)
@@ -87,7 +85,6 @@ type customUserLogicModel interface {
QueryAdminUsers(ctx context.Context) ([]*User, error)
UpdateUserCache(ctx context.Context, data *User) error
UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error
InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error
QueryActiveSubscriptions(ctx context.Context, subscribeId ...int64) (map[int64]int64, error)
FindUserAuthMethods(ctx context.Context, userId int64) ([]*AuthMethods, error)
InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error
@@ -98,23 +95,25 @@ type customUserLogicModel interface {
FindUserAuthMethodByPlatform(ctx context.Context, userId int64, platform string) (*AuthMethods, error)
FindOneByEmail(ctx context.Context, email string) (*User, error)
FindOneDevice(ctx context.Context, id int64) (*Device, error)
QueryDeviceList(ctx context.Context, userid int64) ([]*Device, int64, error)
QueryDevicePageList(ctx context.Context, userid, subscribeId int64, page, size int) ([]*Device, int64, error)
UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error
FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error)
DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error
InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error
FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error)
InsertLoginLog(ctx context.Context, log *LoginLog) error
FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error)
InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error
ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error
ClearUserCache(ctx context.Context, data ...*User) error
InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error
UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error
FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error)
DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error
FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error)
QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error)
QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error)
}
type UserStatisticsWithDate struct {
Date string
Register int64
NewOrderUsers int64
RenewalOrderUsers int64
}
// NewModel returns a model for the database table.
@@ -124,56 +123,6 @@ func NewModel(conn *gorm.DB, c *redis.Client) Model {
}
}
func (m *defaultUserModel) getSubscribeCacheKey(data *Subscribe) []string {
if data == nil {
return []string{}
}
var keys []string
if data.Token != "" {
keys = append(keys, fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, data.Token))
}
if data.UserId != 0 {
keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, data.UserId))
}
if data.Id != 0 {
keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, data.Id))
}
if data.SubscribeId != 0 {
var sub *subscribe.Subscribe
err := m.QueryNoCacheCtx(context.Background(), &sub, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&subscribe.Subscribe{}).Where("id = ?", data.SubscribeId).First(&sub).Error
})
if err != nil {
logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId))
return keys
}
if sub.Server != "" {
ids := tool.StringToInt64Slice(sub.Server)
for _, id := range ids {
keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id))
}
}
if sub.ServerGroup != "" {
ids := tool.StringToInt64Slice(sub.ServerGroup)
var servers []*server.Server
err = m.QueryNoCacheCtx(context.Background(), &servers, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&server.Server{}).Where("group_id in ?", ids).Find(v).Error
})
if err != nil {
logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId))
return keys
}
for _, s := range servers {
keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, s.Id))
}
}
}
return keys
}
// QueryPageList returns a list of records that meet the conditions.
func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, filter *UserFilterParams) ([]*User, int64, error) {
var list []*User
@@ -195,6 +144,9 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId)
}
if filter.Order != "" {
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
}
}
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error
})
@@ -218,33 +170,20 @@ func (m *customUserModel) BatchDeleteUser(ctx context.Context, ids []int64, tx .
}, m.batchGetCacheKeys(users...)...)
}
// InsertBalanceLog insert BalanceLog into the database.
func (m *customUserModel) InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error {
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Create(data).Error
})
}
// FindUserBalanceLogList returns a list of records that meet the conditions.
func (m *customUserModel) FindUserBalanceLogList(ctx context.Context, userId int64, page, size int) ([]*BalanceLog, int64, error) {
var list []*BalanceLog
var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&BalanceLog{}).Where("`user_id` = ?", userId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error
})
return list, total, err
}
func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error {
sub, err := m.FindOneSubscribe(ctx, id)
if err != nil {
return err
}
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
// 使用 defer 确保更新后清理缓存
defer func() {
if clearErr := m.ClearSubscribeCacheByModels(ctx, sub); clearErr != nil {
// 记录清理缓存错误
}
}()
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
@@ -252,7 +191,7 @@ func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id
"download": gorm.Expr("download + ?", download),
"upload": gorm.Expr("upload + ?", upload),
}).Error
}, m.getSubscribeCacheKey(sub)...)
})
}
func (m *customUserModel) QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) {
@@ -292,16 +231,7 @@ func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error)
}
func (m *customUserModel) UpdateUserCache(ctx context.Context, data *User) error {
return m.CachedConn.DelCacheCtx(ctx, m.getCacheKeys(data)...)
}
func (m *customUserModel) InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error {
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&CommissionLog{}).Create(data).Error
})
return m.ClearUserCache(ctx, data)
}
func (m *customUserModel) FindOneByReferCode(ctx context.Context, referCode string) (*User, error) {
@@ -320,81 +250,77 @@ func (m *customUserModel) FindOneSubscribeDetailsById(ctx context.Context, id in
return &data, err
}
func (m *customUserModel) InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error {
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&ResetSubscribeLog{}).Create(log).Error
})
}
// QueryDailyUserStatisticsList Query daily user statistics list for the current month (from 1st to current date)
func (m *customUserModel) QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) {
var results []UserStatisticsWithDate
func (m *customUserModel) UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error {
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&ResetSubscribeLog{}).Where("id = ?", log.Id).Updates(log).Error
})
}
err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error {
firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
func (m *customUserModel) FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error) {
var data ResetSubscribeLog
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).First(&data).Error
})
return &data, err
}
// 子查询:统计每天的新用户订单数量
newOrderSub := conn.Model(&order.Order{}).
Select("DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(DISTINCT user_id) AS new_order_users").
Where("is_new = 1 AND created_at BETWEEN ? AND ? AND status IN ?", firstDay, date, []int64{2, 5}).
Group("DATE_FORMAT(created_at, '%Y-%m-%d')")
func (m *customUserModel) DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error {
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).Delete(&ResetSubscribeLog{}).Error
})
}
// 子查询:统计每天的续费订单数量
renewalOrderSub := conn.Model(&order.Order{}).
Select("DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(DISTINCT user_id) AS renewal_order_users").
Where("is_new = 0 AND created_at BETWEEN ? AND ? AND status IN ?", firstDay, date, []int64{2, 5}).
Group("DATE_FORMAT(created_at, '%Y-%m-%d')")
func (m *customUserModel) FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error) {
if filter == nil {
return nil, 0, errors.New("filter params is nil")
}
var list []*ResetSubscribeLog
var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
query := conn.Model(&ResetSubscribeLog{})
// 应用筛选条件
if filter.UserId != 0 {
query = query.Where("user_id = ?", filter.UserId)
}
if filter.UserSubscribeId != 0 {
query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId)
}
if filter.Type != 0 {
query = query.Where("type = ?", filter.Type)
}
if filter.OrderNo != "" {
query = query.Where("order_no = ?", filter.OrderNo)
}
// 计算总数
if err := query.Count(&total).Error; err != nil {
return err
}
// 应用分页
if filter.Page > 0 && filter.Size > 0 {
query = query.Offset((filter.Page - 1) * filter.Size)
}
if filter.Size > 0 {
query = query.Limit(filter.Size)
}
return query.Find(&list).Error
return conn.Model(&User{}).
Select(`
DATE_FORMAT(user.created_at, '%Y-%m-%d') AS date,
COUNT(*) AS register,
IFNULL(MAX(n.new_order_users), 0) AS new_order_users,
IFNULL(MAX(r.renewal_order_users), 0) AS renewal_order_users
`).
Joins("LEFT JOIN (?) AS n ON DATE_FORMAT(user.created_at, '%Y-%m-%d') = n.date", newOrderSub).
Joins("LEFT JOIN (?) AS r ON DATE_FORMAT(user.created_at, '%Y-%m-%d') = r.date", renewalOrderSub).
Where("user.created_at BETWEEN ? AND ?", firstDay, date).
Group("DATE_FORMAT(user.created_at, '%Y-%m-%d')").
Order("date ASC").
Scan(v).Error
})
return list, total, err
return results, err
}
// QueryMonthlyUserStatisticsList Query monthly user statistics list for the past 6 months
func (m *customUserModel) QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) {
var results []UserStatisticsWithDate
err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error {
// 获取 6 个月前的日期
sixMonthsAgo := date.AddDate(0, -5, 0)
// 子查询:每月新订单用户数量
newOrderSub := conn.Model(&order.Order{}).
Select("DATE_FORMAT(created_at, '%Y-%m') AS date, COUNT(DISTINCT user_id) AS new_order_users").
Where("is_new = 1 AND created_at >= ? AND status IN ?", sixMonthsAgo, []int64{2, 5}).
Group("DATE_FORMAT(created_at, '%Y-%m')")
// 子查询:每月续费订单用户数量
renewalOrderSub := conn.Model(&order.Order{}).
Select("DATE_FORMAT(created_at, '%Y-%m') AS date, COUNT(DISTINCT user_id) AS renewal_order_users").
Where("is_new = 0 AND created_at >= ? AND status IN ?", sixMonthsAgo, []int64{2, 5}).
Group("DATE_FORMAT(created_at, '%Y-%m')")
return conn.Model(&User{}).
Select(`
DATE_FORMAT(user.created_at, '%Y-%m') AS date,
COUNT(*) AS register,
IFNULL(MAX(n.new_order_users), 0) AS new_order_users,
IFNULL(MAX(r.renewal_order_users), 0) AS renewal_order_users
`).
Joins("LEFT JOIN (?) AS n ON DATE_FORMAT(user.created_at, '%Y-%m') = n.date", newOrderSub).
Joins("LEFT JOIN (?) AS r ON DATE_FORMAT(user.created_at, '%Y-%m') = r.date", renewalOrderSub).
Where("user.created_at >= ?", sixMonthsAgo).
Group("DATE_FORMAT(user.created_at, '%Y-%m')").
Order("date ASC").
Scan(v).Error
})
return results, err
}
+60 -20
View File
@@ -9,7 +9,7 @@ import (
)
func (m *defaultUserModel) UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error {
return m.CachedConn.DelCacheCtx(ctx, m.getSubscribeCacheKey(data)...)
return m.ClearSubscribeCacheByModels(ctx, data)
}
// QueryActiveSubscriptions returns the number of active subscriptions.
@@ -21,7 +21,7 @@ func (m *defaultUserModel) QueryActiveSubscriptions(ctx context.Context, subscri
var result []SubscriptionCount
err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Subscribe{}).
Where("subscribe_id IN ? AND `status` IN ?", subscribeId, []int64{1, 0, 3}).
Where("subscribe_id IN ? AND `status` IN ?", subscribeId, []int64{1, 0}).
Select("subscribe_id, COUNT(id) as total").
Group("subscribe_id").
Scan(&result).
@@ -55,13 +55,18 @@ func (m *defaultUserModel) FindOneSubscribe(ctx context.Context, id int64) (*Sub
return conn.Model(&Subscribe{}).Where("id = ?", id).First(&data).Error
})
return &data, err
}
func (m *defaultUserModel) FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error) {
var data []*Subscribe
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` IN ?", subscribeId, []int64{1, 0}).Find(&data).Error
err := conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` IN ?", subscribeId, []int64{1, 0}).Find(v).Error
if err != nil {
return err
}
// update user subscribe status
return conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` = ?", subscribeId, 0).Update("status", 1).Error
})
return data, err
}
@@ -76,8 +81,12 @@ func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64,
// 获取当前时间向前推 7 天
sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour)
// 基础条件查询
conn = conn.Model(&Subscribe{}).Where("`user_id` = ? and `status` IN ?", userId, status)
return conn.Where("`expire_time` > ? OR `finished_at` >= ?", now, sevenDaysAgo).
conn = conn.Model(&Subscribe{}).Where("`user_id` = ?", userId)
if len(status) > 0 {
conn = conn.Where("`status` IN ?", status)
}
// 订阅过期时间大于当前时间或者订阅结束时间大于当前时间
return conn.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)).
Preload("Subscribe").
Find(&list).Error
})
@@ -106,12 +115,24 @@ func (m *defaultUserModel) FindOneSubscribeByToken(ctx context.Context, token st
// UpdateSubscribe updates a record.
func (m *defaultUserModel) UpdateSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
old, err := m.FindOneSubscribe(ctx, data.Id)
if err != nil {
return err
}
// 使用 defer 确保更新后清理缓存
defer func() {
if clearErr := m.ClearSubscribeCacheByModels(ctx, old, data); clearErr != nil {
// 记录清理缓存错误
}
}()
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&Subscribe{}).Where("token = ?", data.Token).Save(data).Error
}, m.getSubscribeCacheKey(data)...)
return conn.Model(&Subscribe{}).Where("id = ?", data.Id).Save(data).Error
})
}
// DeleteSubscribe deletes a record.
@@ -120,22 +141,37 @@ func (m *defaultUserModel) DeleteSubscribe(ctx context.Context, token string, tx
if err != nil {
return err
}
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
// 使用 defer 确保删除后清理缓存
defer func() {
if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil {
// 记录清理缓存错误
}
}()
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Where("token = ?", token).Delete(&Subscribe{}).Error
}, m.getSubscribeCacheKey(data)...)
})
}
// InsertSubscribe insert Subscribe into the database.
func (m *defaultUserModel) InsertSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
// 使用 defer 确保插入后清理相关缓存
defer func() {
if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil {
// 记录清理缓存错误
}
}()
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Create(data).Error
}, m.getSubscribeCacheKey(data)...)
})
}
func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx ...*gorm.DB) error {
@@ -143,18 +179,22 @@ func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx
if err != nil {
return err
}
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
// 使用 defer 确保删除后清理缓存
defer func() {
if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil {
// 记录清理缓存错误
}
}()
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Where("id = ?", id).Delete(&Subscribe{}).Error
}, m.getSubscribeCacheKey(data)...)
})
}
func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error {
var keys []string
for _, item := range data {
keys = append(keys, m.getSubscribeCacheKey(item)...)
}
return m.CachedConn.DelCacheCtx(ctx, keys...)
return m.ClearSubscribeCacheByModels(ctx, data...)
}
+25 -155
View File
@@ -2,9 +2,6 @@ package user
import (
"time"
"gorm.io/gorm"
"gorm.io/plugin/soft_delete"
)
type User struct {
@@ -14,7 +11,9 @@ type User struct {
Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount
ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"`
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
ReferralPercentage uint8 `gorm:"default:0;comment:Referral"` // Referral Percentage
OnlyFirstPurchase *bool `gorm:"default:true;not null;comment:Only First Purchase"` // Only First Purchase Referral
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
@@ -28,107 +27,33 @@ type User struct {
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (User) TableName() string {
return "user"
}
type OldUser struct {
Id int64 `gorm:"primaryKey"`
Email string `gorm:"index:idx_email;type:varchar(100);comment:Email"`
//Telephone string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:Telephone"`
//TelephoneAreaCode string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:TelephoneAreaCode"`
Password string `gorm:"type:varchar(100);not null;comment:User Password"`
Avatar string `gorm:"type:varchar(200);default:'';comment:User Avatar"`
Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount
Telegram int64 `gorm:"default:null;comment:Telegram Account"`
ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"`
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
ValidEmail *bool `gorm:"default:false;not null;comment:Is Email Verified"`
EnableEmailNotify *bool `gorm:"default:false;not null;comment:Enable Email Notifications"`
EnableTelegramNotify *bool `gorm:"default:false;not null;comment:Enable Telegram Notifications"`
EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"`
EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"`
EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"`
EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
DeletedAt gorm.DeletedAt `gorm:"default:null;comment:Deletion Time"`
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt;comment:1: Normal 0: Deleted"` // Using `1` and `0` to indicate
}
func (OldUser) TableName() string {
func (*User) TableName() string {
return "user"
}
type Subscribe struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
User User `gorm:"foreignKey:UserId;references:Id"`
OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"`
SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"`
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"`
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
FinishedAt time.Time `gorm:"default:NULL;comment:Finished Time"`
Traffic int64 `gorm:"default:0;comment:Traffic"`
Download int64 `gorm:"default:0;comment:Download Traffic"`
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"`
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
User User `gorm:"foreignKey:UserId;references:Id"`
OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"`
SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"`
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"`
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
Traffic int64 `gorm:"default:0;comment:Traffic"`
Download int64 `gorm:"default:0;comment:Download Traffic"`
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"`
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (Subscribe) TableName() string {
func (*Subscribe) TableName() string {
return "user_subscribe"
}
type BalanceLog struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
Amount int64 `gorm:"not null;comment:Amount"`
Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward"`
OrderId int64 `gorm:"default:null;comment:Order ID"`
Balance int64 `gorm:"not null;comment:Balance"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
}
func (BalanceLog) TableName() string {
return "user_balance_log"
}
type GiftAmountLog struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
UserSubscribeId int64 `gorm:"default:null;comment:Deduction User Subscribe ID"`
OrderNo string `gorm:"default:null;comment:Order No."`
Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Increase 2: Reduce"`
Amount int64 `gorm:"not null;comment:Amount"`
Balance int64 `gorm:"not null;comment:Balance"`
Remark string `gorm:"type:varchar(255);default:'';comment:Remark"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
}
func (GiftAmountLog) TableName() string {
return "user_gift_amount_log"
}
type CommissionLog struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
OrderNo string `gorm:"default:null;comment:Order No."`
Amount int64 `gorm:"not null;comment:Amount"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
}
func (CommissionLog) TableName() string {
return "user_commission_log"
}
type AuthMethods struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
@@ -139,7 +64,7 @@ type AuthMethods struct {
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (AuthMethods) TableName() string {
func (*AuthMethods) TableName() string {
return "user_auth_methods"
}
@@ -155,14 +80,14 @@ type Device struct {
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (Device) TableName() string {
func (*Device) TableName() string {
return "user_device"
}
type DeviceOnlineRecord struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"comment:User ID"`
Identifier string `gorm:"comment:Device Identifier"`
UserId int64 `gorm:"type:bigint;not null;comment:User ID"`
Identifier string `gorm:"type:varchar(255);not null;comment:Device Identifier"`
OnlineTime time.Time `gorm:"comment:Online Time"` // The time when the device goes online
OfflineTime time.Time `gorm:"comment:Offline Time"`
OnlineSeconds int64 `gorm:"comment:Offline Seconds"`
@@ -173,58 +98,3 @@ type DeviceOnlineRecord struct {
func (DeviceOnlineRecord) TableName() string {
return "user_device_online_record"
}
type LoginLog struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
LoginIP string `gorm:"type:varchar(255);not null;comment:Login IP"`
UserAgent string `gorm:"type:text;not null;comment:UserAgent"`
Success *bool `gorm:"default:false;not null;comment:Login Success"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
}
func (LoginLog) TableName() string {
return "user_login_log"
}
type SubscribeLog struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
UserSubscribeId int64 `gorm:"index:idx_user_subscribe_id;not null;comment:User Subscribe ID"`
Token string `gorm:"type:varchar(255);not null;comment:Token"`
IP string `gorm:"type:varchar(255);not null;comment:IP"`
UserAgent string `gorm:"type:text;not null;comment:UserAgent"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
}
func (SubscribeLog) TableName() string {
return "user_subscribe_log"
}
const (
ResetSubscribeTypeAuto uint8 = 1
ResetSubscribeTypeAdvance uint8 = 2
ResetSubscribeTypePaid uint8 = 3
)
type FilterResetSubscribeLogParams struct {
Page int
Size int
Type uint8
UserId int64
OrderNo string
UserSubscribeId int64
}
type ResetSubscribeLog struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"type:bigint;index:idx_user_id;not null;comment:User ID"`
Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Auto 2: Advance 3: Paid"`
OrderNo string `gorm:"type:varchar(255);default:null;comment:Order No."`
UserSubscribeId int64 `gorm:"type:bigint;index:idx_user_subscribe_id;not null;comment:User Subscribe ID"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
}
func (ResetSubscribeLog) TableName() string {
return "user_reset_subscribe_log"
}