同步历史版本代码

This commit is contained in:
2026-03-03 09:32:22 -08:00
parent 7d46b31866
commit 4d8516b2e1
140 changed files with 8399 additions and 489 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ import (
type Auth struct {
Id int64 `gorm:"primaryKey"`
Method string `gorm:"unique;type:varchar(255);not null;default:'';comment:platform"`
Config string `gorm:"type:text;not null;comment:Auth Configuration"`
Config string `gorm:"type:mediumtext;not null;comment:Auth Configuration"`
Enabled *bool `gorm:"type:tinyint(1);not null;default:false;comment:Is Enabled"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
@@ -129,7 +129,7 @@ func (l *EmailAuthConfig) Marshal() string {
if l.ExpirationEmailTemplate == "" {
l.ExpirationEmailTemplate = email.DefaultExpirationEmailTemplate
}
if l.ExpirationEmailTemplate == "" {
if l.MaintenanceEmailTemplate == "" {
l.MaintenanceEmailTemplate = email.DefaultMaintenanceEmailTemplate
}
if l.TrafficExceedEmailTemplate == "" {
+68
View File
@@ -0,0 +1,68 @@
package apple
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
type Model interface {
Insert(ctx context.Context, data *Transaction, tx ...*gorm.DB) error
FindByOriginalId(ctx context.Context, originalId string) (*Transaction, error)
FindByUserAndProduct(ctx context.Context, userId int64, productId string) (*Transaction, error)
}
type defaultModel struct {
cache.CachedConn
table string
}
type customModel struct {
*defaultModel
}
func NewModel(db *gorm.DB, c *redis.Client) Model {
return &customModel{
defaultModel: &defaultModel{
CachedConn: cache.NewConn(db, c),
table: "`apple_iap_transactions`",
},
}
}
func (m *defaultModel) jwsKey(jws string) string {
sum := sha256.Sum256([]byte(jws))
return fmt.Sprintf("cache:iap:jws:%s", hex.EncodeToString(sum[:]))
}
func (m *customModel) Insert(ctx context.Context, data *Transaction, tx ...*gorm.DB) error {
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&Transaction{}).Create(data).Error
}, m.jwsKey(data.JWSHash))
}
func (m *customModel) FindByOriginalId(ctx context.Context, originalId string) (*Transaction, error) {
var data Transaction
key := fmt.Sprintf("cache:iap:original:%s", originalId)
err := m.QueryCtx(ctx, &data, key, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Transaction{}).Where("original_transaction_id = ?", originalId).First(&data).Error
})
return &data, err
}
func (m *customModel) FindByUserAndProduct(ctx context.Context, userId int64, productId string) (*Transaction, error) {
var data Transaction
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Transaction{}).Where("user_id = ? AND product_id = ?", userId, productId).Order("purchase_at DESC").First(&data).Error
})
return &data, err
}
+21
View File
@@ -0,0 +1,21 @@
package apple
import "time"
type Transaction struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
OriginalTransactionId string `gorm:"type:varchar(255);uniqueIndex:uni_original;not null;comment:Original Transaction ID"`
TransactionId string `gorm:"type:varchar(255);not null;comment:Transaction ID"`
ProductId string `gorm:"type:varchar(255);not null;comment:Product ID"`
PurchaseAt time.Time `gorm:"not null;comment:Purchase Time"`
RevocationAt *time.Time `gorm:"comment:Revocation Time"`
JWSHash string `gorm:"type:varchar(255);not null;comment:JWS Hash"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (Transaction) TableName() string {
return "apple_iap_transactions"
}
+50
View File
@@ -0,0 +1,50 @@
package logmessage
import (
"context"
"gorm.io/gorm"
)
type (
Model interface {
logMessageModel
customLogMessageModel
}
logMessageModel interface {
Insert(ctx context.Context, data *LogMessage) error
FindOne(ctx context.Context, id int64) (*LogMessage, error)
Update(ctx context.Context, data *LogMessage) error
Delete(ctx context.Context, id int64) error
}
customModel struct{
*defaultModel
}
defaultModel struct{
*gorm.DB
}
)
func newDefaultModel(db *gorm.DB) *defaultModel {
return &defaultModel{DB: db}
}
func (m *defaultModel) Insert(ctx context.Context, data *LogMessage) error {
return m.WithContext(ctx).Create(data).Error
}
func (m *defaultModel) FindOne(ctx context.Context, id int64) (*LogMessage, error) {
var v LogMessage
err := m.WithContext(ctx).Where("id = ?", id).First(&v).Error
if err != nil {
return nil, err
}
return &v, nil
}
func (m *defaultModel) Update(ctx context.Context, data *LogMessage) error {
return m.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error
}
func (m *defaultModel) Delete(ctx context.Context, id int64) error {
return m.WithContext(ctx).Where("`id` = ?", id).Delete(&LogMessage{}).Error
}
+27
View File
@@ -0,0 +1,27 @@
package logmessage
import "time"
type LogMessage struct {
Id int64 `gorm:"primaryKey;AUTO_INCREMENT"`
Platform string `gorm:"type:varchar(32);not null"`
AppVersion string `gorm:"type:varchar(32);default:null"`
OsName string `gorm:"type:varchar(32);default:null"`
OsVersion string `gorm:"type:varchar(32);default:null"`
DeviceId string `gorm:"type:varchar(64);default:null"`
UserId *int64 `gorm:"type:bigint;default:null"`
SessionId string `gorm:"type:varchar(64);default:null"`
Level uint8 `gorm:"type:tinyint(1);not null;default:3"`
ErrorCode string `gorm:"type:varchar(64);default:null"`
Message string `gorm:"type:text;not null"`
Stack string `gorm:"type:mediumtext;default:null"`
Context string `gorm:"type:json;default:null"`
ClientIP string `gorm:"type:varchar(45);default:null"`
UserAgent string `gorm:"type:varchar(255);default:null"`
Locale string `gorm:"type:varchar(16);default:null"`
Digest string `gorm:"type:varchar(64);uniqueIndex:uniq_digest;default:null"`
OccurredAt *time.Time `gorm:"type:datetime;default:null"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
}
func (LogMessage) TableName() string { return "log_message" }
+52
View File
@@ -0,0 +1,52 @@
package logmessage
import (
"context"
"time"
"gorm.io/gorm"
)
func NewModel(db *gorm.DB) Model {
return &customModel{ defaultModel: newDefaultModel(db) }
}
type FilterParams struct {
Page int
Size int
Platform string
Level uint8
UserID int64
DeviceID string
ErrorCode string
Keyword string
Start time.Time
End time.Time
}
type customLogMessageModel interface {
Filter(ctx context.Context, filter *FilterParams) ([]*LogMessage, int64, error)
}
func (m *customModel) Filter(ctx context.Context, filter *FilterParams) ([]*LogMessage, int64, error) {
tx := m.WithContext(ctx).Model(&LogMessage{}).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.Platform != "" { tx = tx.Where("`platform` = ?", filter.Platform) }
if filter.Level != 0 { tx = tx.Where("`level` = ?", filter.Level) }
if filter.UserID != 0 { tx = tx.Where("`user_id` = ?", filter.UserID) }
if filter.DeviceID != "" { tx = tx.Where("`device_id` = ?", filter.DeviceID) }
if filter.ErrorCode != "" { tx = tx.Where("`error_code` = ?", filter.ErrorCode) }
if !filter.Start.IsZero() { tx = tx.Where("`created_at` >= ?", filter.Start) }
if !filter.End.IsZero() { tx = tx.Where("`created_at` <= ?", filter.End) }
if filter.Keyword != "" {
like := "%" + filter.Keyword + "%"
tx = tx.Where("`message` LIKE ? OR `stack` LIKE ?", like, like)
}
var total int64
var rows []*LogMessage
err := tx.Count(&total).Limit(filter.Size).Offset((filter.Page-1)*filter.Size).Find(&rows).Error
return rows, total, err
}
+41 -14
View File
@@ -14,6 +14,7 @@ type customServerLogicModel interface {
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
ClearServerAllCache(ctx context.Context) error
CountNodesByIdsAndTags(ctx context.Context, nodeIds []int64, tags []string) (int64, error)
}
const (
@@ -129,7 +130,7 @@ func (m *customServerModel) ClearNodeCache(ctx context.Context, params *FilterNo
return err
}
if len(keys) > 0 {
cacheKeys = append(keys, keys...)
cacheKeys = append(cacheKeys, keys...)
}
cursor = newCursor
if cursor == 0 {
@@ -152,7 +153,7 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64
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()
keys, newCursor, err := m.Cache.Scan(ctx, cursor, fmt.Sprintf("%s%d*", ServerConfigCacheKey, serverId), 100).Result()
if err != nil {
return err
}
@@ -175,18 +176,21 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64
func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
var cursor uint64
var keys []string
prefix := ServerUserListCacheKey + "*"
for {
scanKeys, newCursor, err := m.Cache.Scan(ctx, cursor, prefix, 999).Result()
if err != nil {
m.Logger.Error(ctx, fmt.Sprintf("ClearServerAllCache err:%v", err))
break
}
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache query keys:%v", scanKeys))
keys = append(keys, scanKeys...)
cursor = newCursor
if cursor == 0 {
break
prefixes := []string{ServerConfigCacheKey + "*", ServerUserListCacheKey + "*"}
for _, prefix := range prefixes {
cursor = 0
for {
scanKeys, newCursor, err := m.Cache.Scan(ctx, cursor, prefix, 999).Result()
if err != nil {
m.Logger.Error(ctx, fmt.Sprintf("ClearServerAllCache err:%v", err))
break
}
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache query keys:%v", scanKeys))
keys = append(keys, scanKeys...)
cursor = newCursor
if cursor == 0 {
break
}
}
}
if len(keys) > 0 {
@@ -196,6 +200,29 @@ func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
return nil
}
// CountNodesByIdsAndTags 根据节点ID和标签计算启用的节点数量
func (m *customServerModel) CountNodesByIdsAndTags(ctx context.Context, nodeIds []int64, tags []string) (int64, error) {
var count int64
query := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
if len(nodeIds) > 0 || len(tags) > 0 {
subQuery := m.WithContext(ctx).Model(&Node{}).Where("enabled = ?", true)
if len(nodeIds) > 0 && len(tags) > 0 {
subQuery = subQuery.Where("id IN ? OR ?", nodeIds, InSet("tags", tags))
} else if len(nodeIds) > 0 {
subQuery = subQuery.Where("id IN ?", nodeIds)
} else {
subQuery = subQuery.Scopes(InSet("tags", tags))
}
query = subQuery
}
err := query.Count(&count).Error
return count, err
}
// InSet 支持多值 OR 查询
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
+9
View File
@@ -12,6 +12,7 @@ type customPaymentLogicModel interface {
FindAll(ctx context.Context) ([]*Payment, error)
FindListByPage(ctx context.Context, page, size int, req *Filter) (int64, []*Payment, error)
FindAvailableMethods(ctx context.Context) ([]*Payment, error)
FindListByPlatform(ctx context.Context, platform string) ([]*Payment, error)
}
// NewModel returns a model for the database table.
@@ -67,3 +68,11 @@ func (m *customPaymentModel) FindListByPage(ctx context.Context, page, size int,
})
return total, resp, err
}
func (m *customPaymentModel) FindListByPlatform(ctx context.Context, platform string) ([]*Payment, error) {
var resp []*Payment
err := m.QueryNoCacheCtx(ctx, &resp, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Payment{}).Where("mark = ?", platform).Find(v).Error
})
return resp, err
}
+23
View File
@@ -127,3 +127,26 @@ func (l *CryptoSaaSConfig) Unmarshal(data []byte) error {
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
}
type AppleIAPConfig struct {
ProductIds []string `json:"product_ids"`
KeyID string `json:"key_id"`
IssuerID string `json:"issuer_id"`
PrivateKey string `json:"private_key"`
Sandbox bool `json:"sandbox"`
}
func (l *AppleIAPConfig) Marshal() ([]byte, error) {
type Alias AppleIAPConfig
return json.Marshal(&struct {
*Alias
}{
Alias: (*Alias)(l),
})
}
func (l *AppleIAPConfig) Unmarshal(data []byte) error {
type Alias AppleIAPConfig
aux := (*Alias)(l)
return json.Unmarshal(data, &aux)
}
+12
View File
@@ -56,6 +56,18 @@ func (m *customUserModel) QueryDeviceList(ctx context.Context, userId int64) ([]
return list, total, err
}
func (m *customUserModel) QueryDeviceListByUserIds(ctx context.Context, userIds []int64) ([]*Device, int64, error) {
var list []*Device
var total int64
if len(userIds) == 0 {
return list, total, nil
}
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Device{}).Where("`user_id` IN ?", userIds).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 {
+49
View File
@@ -0,0 +1,49 @@
package user
import (
"time"
"gorm.io/gorm"
)
const (
FamilyStatusActive uint8 = 1
FamilyRoleOwner uint8 = 1
FamilyRoleMember uint8 = 2
FamilyMemberActive uint8 = 1
FamilyMemberLeft uint8 = 2
FamilyMemberRemoved uint8 = 3
DefaultFamilyMaxSize int64 = 5
)
type UserFamily struct {
Id int64 `gorm:"primaryKey"`
OwnerUserId int64 `gorm:"uniqueIndex:uniq_owner_user_id;not null;comment:Owner User ID"`
MaxMembers int64 `gorm:"not null;default:5;comment:Max members in family"`
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status: 1=active, 0=disabled"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:Deletion Time"`
}
func (*UserFamily) TableName() string {
return "user_family"
}
type UserFamilyMember struct {
Id int64 `gorm:"primaryKey"`
FamilyId int64 `gorm:"index:idx_family_status,priority:1;not null;comment:Family ID"`
UserId int64 `gorm:"uniqueIndex:uniq_user_id;not null;comment:Member User ID"`
Role uint8 `gorm:"type:tinyint(1);not null;default:2;comment:Role: 1=owner, 2=member"`
Status uint8 `gorm:"index:idx_family_status,priority:2;type:tinyint(1);not null;default:1;comment:Status: 1=active, 2=left, 3=removed"`
JoinSource string `gorm:"type:varchar(32);not null;default:'';comment:Join source"`
JoinedAt time.Time `gorm:"not null;comment:Joined time"`
LeftAt *time.Time `gorm:"default:NULL;comment:Left time"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:Deletion Time"`
}
func (*UserFamilyMember) TableName() string {
return "user_family_member"
}
+22
View File
@@ -99,6 +99,7 @@ type customUserLogicModel interface {
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)
QueryDeviceListByUserIds(ctx context.Context, userIds []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)
@@ -110,6 +111,13 @@ type customUserLogicModel interface {
QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error)
QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error)
FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error)
FindActiveSubscribesByUserIds(ctx context.Context, userIds []int64) (map[int64]*UserStatusInfo, error)
}
type UserStatusInfo struct {
MemberStatus string
LastTrafficAt *time.Time
}
type UserStatisticsWithDate struct {
@@ -334,3 +342,17 @@ func (m *customUserModel) QueryMonthlyUserStatisticsList(ctx context.Context, da
return results, err
}
// FindActiveSubscribe finds the active subscription for a user
func (m *customUserModel) FindActiveSubscribe(ctx context.Context, userId int64) (*Subscribe, error) {
var subscribe Subscribe
err := m.QueryNoCacheCtx(ctx, &subscribe, func(conn *gorm.DB, v interface{}) error {
return conn.Where("user_id = ? AND status IN (0, 1) AND expire_time > ?", userId, time.Now()).
Order("expire_time DESC").
First(v).Error
})
if err != nil {
return nil, err
}
return &subscribe, nil
}
+45
View File
@@ -0,0 +1,45 @@
package user
import (
"context"
"time"
"gorm.io/gorm"
)
// FindActiveSubscribesByUserIds Find active subscriptions for multiple users
func (m *customUserModel) FindActiveSubscribesByUserIds(ctx context.Context, userIds []int64) (map[int64]*UserStatusInfo, error) {
if len(userIds) == 0 {
return map[int64]*UserStatusInfo{}, nil
}
type Result struct {
UserId int64
Name string
UpdatedAt *time.Time
}
var results []Result
// Query latest active subscription for each user
err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error {
return conn.Table("user_subscribe").
Select("user_subscribe.user_id, subscribe.name, user_subscribe.updated_at").
Joins("LEFT JOIN subscribe ON user_subscribe.subscribe_id = subscribe.id").
Where("user_subscribe.user_id IN ? AND user_subscribe.status IN (0, 1) AND user_subscribe.expire_time > ?", userIds, time.Now()).
Order("user_subscribe.created_at ASC"). // Ascending so we can overwrite in map to get the latest
Scan(v).Error
})
if err != nil {
return nil, err
}
userMap := make(map[int64]*UserStatusInfo)
for _, r := range results {
userMap[r.UserId] = &UserStatusInfo{
MemberStatus: r.Name,
LastTrafficAt: r.UpdatedAt,
}
}
return userMap, nil
}
+19 -7
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"time"
"github.com/perfect-panel/server/pkg/constant"
"gorm.io/gorm"
)
@@ -74,18 +75,29 @@ func (m *defaultUserModel) FindUsersSubscribeBySubscribeId(ctx context.Context,
// QueryUserSubscribe returns a list of records that meet the conditions.
func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) {
var list []*SubscribeDetails
key := fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, userId)
// Check if includeExpired is set in context
includeExpired, _ := ctx.Value(constant.CtxKeyIncludeExpired).(string)
cacheKeySuffix := ""
if includeExpired == "all" {
cacheKeySuffix = ":all"
}
key := fmt.Sprintf("%s%d%s", cacheUserSubscribeUserPrefix, userId, cacheKeySuffix)
err := m.QueryCtx(ctx, &list, key, func(conn *gorm.DB, v interface{}) error {
// 获取当前时间
now := time.Now()
// 获取当前时间向前推 7 天
sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour)
// 基础条件查询
conn = conn.Model(&Subscribe{}).Where("`user_id` = ?", userId)
if len(status) > 0 {
conn = conn.Where("`status` IN ?", status)
}
// 订阅过期时间大于当前时间或者订阅结束时间大于当前时间
if includeExpired == "all" {
// 查询所有订阅(包括已过期的)
return conn.Preload("Subscribe").Find(&list).Error
}
// 默认只查询有效订阅
now := time.Now()
sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour)
return conn.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)).
Preload("Subscribe").
Find(&list).Error
+3
View File
@@ -28,6 +28,9 @@ type User struct {
AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"`
UserDevices []Device `gorm:"foreignKey:UserId;references:Id"`
Rules string `gorm:"type:TEXT;comment:User Rules"`
LastLoginTime *time.Time `gorm:"default:NULL;comment:Last Login Time"`
MemberStatus string `gorm:"type:varchar(20);default:'';comment:Member Status"`
Remark string `gorm:"type:varchar(255);default:'';comment:Remark"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:Deletion Time"`