diff --git a/apis/types.api b/apis/types.api index 780a858..8773ba5 100644 --- a/apis/types.api +++ b/apis/types.api @@ -153,9 +153,10 @@ type ( NodePushInterval int64 `json:"node_push_interval"` } InviteConfig { - ForcedInvite bool `json:"forced_invite"` - ReferralPercentage int64 `json:"referral_percentage"` - OnlyFirstPurchase bool `json:"only_first_purchase"` + ForcedInvite bool `json:"forced_invite"` + FirstPurchasePercentage int64 `json:"first_purchase_percentage"` + FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"` + NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"` } TelegramConfig { TelegramBotToken string `json:"telegram_bot_token"` diff --git a/etc/ppanel.yaml b/etc/ppanel.yaml index e69de29..2997f4b 100644 --- a/etc/ppanel.yaml +++ b/etc/ppanel.yaml @@ -0,0 +1,38 @@ +Host: 0.0.0.0 +Port: 8080 +Debug: false +JwtAuth: + AccessSecret: 1234567890 + AccessExpire: 604800 +Logger: + ServiceName: PPanel + Mode: console + Encoding: plain + TimeFormat: '2025-01-01 00:00:00.000' + Path: logs + Level: debug + MaxContentLength: 0 + Compress: false + Stat: true + KeepDays: 0 + StackCooldownMillis: 100 + MaxBackups: 0 + MaxSize: 0 + Rotation: daily + FileTimeFormat: 2025-01-01T00:00:00.000Z00:00 +MySQL: + Addr: 172.245.180.199:3306 + Dbname: ppanel + Username: ppanel + Password: ppanelpassword + Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai + MaxIdleConns: 10 + MaxOpenConns: 10 + SlowThreshold: 1000 +Redis: + Host: ppanel-cache:6379 + Pass: + DB: 0 +Administrator: + Password: password + Email: admin@ppanel.dev \ No newline at end of file diff --git a/initialize/migrate/database/00002_init_basic_data.up.sql b/initialize/migrate/database/00002_init_basic_data.up.sql index 83cdda6..dcd0619 100644 --- a/initialize/migrate/database/00002_init_basic_data.up.sql +++ b/initialize/migrate/database/00002_init_basic_data.up.sql @@ -90,11 +90,15 @@ VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-2 '2025-04-22 14:25:16.640'), (23, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), - (24, 'invite', 'ReferralPercentage', '20', 'int', 'Referral percentage', '2025-04-22 14:25:16.640', + (24, 'invite', 'FirstPurchasePercentage', '20', 'int', 'First purchase commission percentage', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), - (25, 'invite', 'OnlyFirstPurchase', 'false', 'bool', 'Only first purchase', '2025-04-22 14:25:16.640', + (25, 'invite', 'NonFirstPurchasePercentage', '10', 'int', 'Non-first purchase commission percentage', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), - (26, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640', + (26, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (42, 'invite', 'FirstYearlyPurchasePercentage', '25', 'int', 'First yearly purchase commission percentage', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (27, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), (27, 'register', 'EnableTrial', 'false', 'bool', 'is enable trial', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), diff --git a/internal/config/config.go b/internal/config/config.go index 1d51df4..d56acfa 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -119,9 +119,10 @@ type File struct { } type InviteConfig struct { - ForcedInvite bool `yaml:"ForcedInvite" default:"false"` - ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"` - OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"` + ForcedInvite bool `yaml:"ForcedInvite" default:"false"` + FirstPurchasePercentage int64 `yaml:"FirstPurchasePercentage" default:"20"` + FirstYearlyPurchasePercentage int64 `yaml:"FirstYearlyPurchasePercentage" default:"25"` + NonFirstPurchasePercentage int64 `yaml:"NonFirstPurchasePercentage" default:"10"` } type Telegram struct { diff --git a/internal/logic/admin/user/updateUserBasicInfoLogic.go b/internal/logic/admin/user/updateUserBasicInfoLogic.go index c177fd9..fb673e4 100644 --- a/internal/logic/admin/user/updateUserBasicInfoLogic.go +++ b/internal/logic/admin/user/updateUserBasicInfoLogic.go @@ -44,6 +44,9 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi userInfo.Balance = req.Balance userInfo.GiftAmount = req.GiftAmount userInfo.Commission = req.Commission + // 手动设置 IsAdmin 字段,因为类型不匹配(*bool vs bool) + userInfo.IsAdmin = &req.IsAdmin + userInfo.Enable = &req.Enable if req.Password != "" { if userInfo.Id == 2 && isDemo { diff --git a/internal/logic/common/getStatLogic.go b/internal/logic/common/getStatLogic.go index df14af0..003b317 100644 --- a/internal/logic/common/getStatLogic.go +++ b/internal/logic/common/getStatLogic.go @@ -120,10 +120,11 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) { protocol = append(protocol, p) } resp = &types.GetStatResponse{ - User: u, - Node: n, - Country: int64(len(country)), - Protocol: protocol, + User: u, + Node: n, + Country: int64(len(country)), + Protocol: protocol, + OnlineDevice: l.svcCtx.DeviceManager.GetOnlineDeviceCount(), } val, _ := json.Marshal(*resp) _ = l.svcCtx.Redis.Set(l.ctx, config.CommonStatCacheKey, string(val), time.Duration(3600)*time.Second).Err() diff --git a/internal/logic/public/portal/getSubscriptionLogic.go b/internal/logic/public/portal/getSubscriptionLogic.go index 5e85447..8796f1d 100644 --- a/internal/logic/public/portal/getSubscriptionLogic.go +++ b/internal/logic/public/portal/getSubscriptionLogic.go @@ -43,10 +43,26 @@ func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionRes tool.DeepCopy(&sub, item) if item.Discount != "" { var discount []types.SubscribeDiscount + _ = json.Unmarshal([]byte(item.Discount), &discount) sub.Discount = discount - list[i] = sub } + + // 计算节点数量(通过服务组查询关联的实际节点数量) + if item.ServerGroup != "" { + // 获取服务组ID列表 + groupIds := tool.StringToInt64Slice(item.ServerGroup) + + // 通过服务组查询关联的节点数量 + servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds) + if err != nil { + l.Errorw("[Site GetSubscription] FindServerListByGroupIds error", logger.Field("error", err.Error())) + sub.ServerCount = 0 + } else { + sub.ServerCount = int64(len(servers)) + } + } + list[i] = sub } resp.List = list diff --git a/internal/logic/public/subscribe/querySubscribeListLogic.go b/internal/logic/public/subscribe/querySubscribeListLogic.go index 09dee07..18299dd 100644 --- a/internal/logic/public/subscribe/querySubscribeListLogic.go +++ b/internal/logic/public/subscribe/querySubscribeListLogic.go @@ -40,6 +40,7 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscri list := make([]types.Subscribe, len(data)) for i, item := range data { var sub types.Subscribe + tool.DeepCopy(&sub, item) if item.Discount != "" { var discount []types.SubscribeDiscount @@ -48,6 +49,15 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscri list[i] = sub } list[i] = sub + // 通过服务组查询关联的节点数量 + servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, sub.ServerGroup) + if err != nil { + l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error())) + sub.ServerCount = 0 + } else { + sub.ServerCount = int64(len(servers)) + } + list[i] = sub } resp.List = list return diff --git a/internal/logic/public/user/queryUserSubscribeLogic.go b/internal/logic/public/user/queryUserSubscribeLogic.go index a8c7863..f570a48 100644 --- a/internal/logic/public/user/queryUserSubscribeLogic.go +++ b/internal/logic/public/user/queryUserSubscribeLogic.go @@ -60,6 +60,25 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub } } + // 计算节点数量(通过服务组关联的实际节点数量) + if item.Subscribe != nil { + // 获取服务组ID列表 + groupIds := tool.StringToInt64Slice(item.Subscribe.ServerGroup) + + // 通过服务组查询关联的节点数量 + servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds) + if err != nil { + l.Errorw("[QueryUserSubscribeLogic] FindServerListByGroupIds error", logger.Field("error", err.Error())) + sub.Subscribe.ServerCount = 0 + } else { + sub.Subscribe.ServerCount = int64(len(servers)) + } + + // 保留原始服务器ID列表用于其他用途 + serverIds := tool.StringToInt64Slice(item.Subscribe.Server) + sub.Subscribe.Server = serverIds + } + sub.ResetTime = calculateNextResetTime(&sub) resp.List = append(resp.List, sub) } diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 063e13e..6e8f763 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -118,10 +118,47 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, serverIds := tool.StringToInt64Slice(subDetails.Server) groupIds := tool.StringToInt64Slice(subDetails.ServerGroup) + // 🔍 订阅ID 2的详细调试 + if userSub.SubscribeId == 2 { + l.Infof("🔍 [DEBUG Subscribe 2] === 开始调试订阅ID 2 ===") + l.Infof("🔍 [DEBUG Subscribe 2] Subscribe详情: %+v", subDetails) + l.Infof("🔍 [DEBUG Subscribe 2] Server字段: %s", subDetails.Server) + l.Infof("🔍 [DEBUG Subscribe 2] ServerGroup字段: %s", subDetails.ServerGroup) + l.Infof("🔍 [DEBUG Subscribe 2] 解析后的serverIds: %v", serverIds) + l.Infof("🔍 [DEBUG Subscribe 2] 解析后的groupIds: %v", groupIds) + } + l.Debugf("[Generate Subscribe]serverIds: %v, groupIds: %v", serverIds, groupIds) + // 查询所有服务器用于调试 + allServers, _ := l.svc.ServerModel.FindAllServer(l.ctx.Request.Context()) + if userSub.SubscribeId == 2 { + l.Infof("🔍 [DEBUG Subscribe 2] 数据库中所有服务器:") + for _, srv := range allServers { + l.Infof("🔍 [DEBUG Subscribe 2] ID:%d Name:%s Protocol:%s Enable:%v GroupID:%d", + srv.Id, srv.Name, srv.Protocol, *srv.Enable, srv.GroupId) + } + } + servers, err := l.svc.ServerModel.FindServerDetailByGroupIdsAndIds(l.ctx.Request.Context(), groupIds, serverIds) + if userSub.SubscribeId == 2 { + l.Infof("🔍 [DEBUG Subscribe 2] 查询结果服务器数量: %d", len(servers)) + for i, srv := range servers { + l.Infof("🔍 [DEBUG Subscribe 2] 结果服务器 %d: ID=%d Name=%s Protocol=%s Enable=%v", + i+1, srv.Id, srv.Name, srv.Protocol, *srv.Enable) + } + + // 检查AnyTLS服务器 + anytlsServers := []*server.Server{} + for _, srv := range servers { + if srv.Protocol == "anytls" { + anytlsServers = append(anytlsServers, srv) + } + } + l.Infof("🔍 [DEBUG Subscribe 2] AnyTLS服务器数量: %d", len(anytlsServers)) + } + l.Debugf("[Query Subscribe]found servers: %v", len(servers)) if err != nil { diff --git a/internal/model/user/subscribe.go b/internal/model/user/subscribe.go index 5eb3c64..e0f5ca6 100644 --- a/internal/model/user/subscribe.go +++ b/internal/model/user/subscribe.go @@ -82,6 +82,7 @@ func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64, // 订阅过期时间大于当前时间或者订阅结束时间大于当前时间 return conn.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)). Preload("Subscribe"). + Order("created_at DESC"). Find(&list).Error }) return list, err diff --git a/internal/types/types.go b/internal/types/types.go index a7e0ba9..80f5fc1 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -475,6 +475,7 @@ type CreateSubscribeRequest struct { GroupId int64 `json:"group_id"` ServerGroup []int64 `json:"server_group"` Server []int64 `json:"server"` + ServerCount int64 `json:"server_count"` Show *bool `json:"show"` Sell *bool `json:"sell"` DeductionRatio int64 `json:"deduction_ratio"` @@ -861,10 +862,11 @@ type GetServerUserListResponse struct { } type GetStatResponse struct { - User int64 `json:"user"` - Node int64 `json:"node"` - Country int64 `json:"country"` - Protocol []string `json:"protocol"` + User int64 `json:"user"` + Node int64 `json:"node"` + Country int64 `json:"country"` + Protocol []string `json:"protocol"` + OnlineDevice int64 `json:"online_device"` } type GetSubscribeDetailsRequest struct { @@ -1044,9 +1046,10 @@ type Hysteria2 struct { } type InviteConfig struct { - ForcedInvite bool `json:"forced_invite"` - ReferralPercentage int64 `json:"referral_percentage"` - OnlyFirstPurchase bool `json:"only_first_purchase"` + ForcedInvite bool `json:"forced_invite"` + FirstPurchasePercentage int64 `json:"first_purchase_percentage"` + FirstYearlyPurchasePercentage int64 `json:"first_yearly_purchase_percentage"` + NonFirstPurchasePercentage int64 `json:"non_first_purchase_percentage"` } type KickOfflineRequest struct { @@ -1656,6 +1659,7 @@ type Subscribe struct { GroupId int64 `json:"group_id"` ServerGroup []int64 `json:"server_group"` Server []int64 `json:"server"` + ServerCount int64 `json:"server_count"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` diff --git a/pkg/adapter/general/uri.go b/pkg/adapter/general/uri.go index c1c925b..7a4ce72 100644 --- a/pkg/adapter/general/uri.go +++ b/pkg/adapter/general/uri.go @@ -63,6 +63,8 @@ func buildProxy(data proxy.Proxy, uuid string) string { return Hysteria2Uri(data, uuid) case "tuic": return TuicUri(data, uuid) + case "anytls": + return AnyTLSUri(data, uuid) default: return "" } @@ -271,6 +273,36 @@ func TuicUri(data proxy.Proxy, uuid string) string { return u.String() } +func AnyTLSUri(data proxy.Proxy, uuid string) string { + anytls, ok := data.Option.(proxy.AnyTLS) + if !ok { + return "" + } + + securityConfig := anytls.SecurityConfig + var query = make(url.Values) + + // 根据AnyTLS官方URI规范实现 + // 格式: anytls://[auth@]hostname[:port]/?[key=value]&[key=value]... + + // TLS配置 + setQuery(&query, "sni", securityConfig.SNI) + + // 是否允许不安全连接 + if securityConfig.AllowInsecure { + setQuery(&query, "insecure", "1") + } + + u := url.URL{ + Scheme: "anytls", + User: url.User(uuid), + Host: net.JoinHostPort(data.Server, strconv.Itoa(anytls.Port)), + RawQuery: query.Encode(), + Fragment: data.Name, + } + return u.String() +} + func setQuery(q *url.Values, k, v string) { if v != "" { q.Set(k, v) diff --git a/pkg/adapter/uilts.go b/pkg/adapter/uilts.go index 04d9cde..317da61 100644 --- a/pkg/adapter/uilts.go +++ b/pkg/adapter/uilts.go @@ -2,6 +2,7 @@ package adapter import ( "encoding/json" + "fmt" "log" "strings" @@ -83,7 +84,19 @@ func addNode(data *server.Server, host string, port int) *proxy.Proxy { node.Port = tuic.Port } option = tuic + case "anytls": + var anytls proxy.AnyTLS + if err := json.Unmarshal([]byte(data.Config), &anytls); err != nil { + logger.Errorw("解析AnyTLS配置失败", logger.Field("error", err.Error()), logger.Field("node", data.Name)) + return nil + } + if port == 0 { + node.Port = anytls.Port + } + option = anytls + logger.Infow("成功处理AnyTLS节点", logger.Field("node", data.Name), logger.Field("port", anytls.Port)) default: + fmt.Printf("[Error] 不支持的协议: %s", data.Protocol) return nil } node.Option = option diff --git a/pkg/device/device.go b/pkg/device/device.go index 9aa2239..96a1fb4 100644 --- a/pkg/device/device.go +++ b/pkg/device/device.go @@ -340,6 +340,11 @@ func (dm *DeviceManager) Broadcast(message string) { } +// GetOnlineDeviceCount returns the total number of online devices +func (dm *DeviceManager) GetOnlineDeviceCount() int64 { + return int64(atomic.LoadInt32(&dm.totalOnline)) +} + // Gracefully shut down all WebSocket connections func (dm *DeviceManager) Shutdown(ctx context.Context) { <-ctx.Done() diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index 9f49673..b4e0225 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -366,7 +366,7 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use return } - amount := l.calculateCommission(orderInfo.Price) + amount := l.calculateCommission(orderInfo.Amount, isNewPurchase && orderInfo.IsNew, orderInfo.Quantity) // Use transaction for commission updates err = l.svc.DB.Transaction(func(tx *gorm.DB) error { @@ -401,13 +401,25 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use // referrer existence, commission settings, and order type func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, orderInfo *order.Order, isNewPurchase bool) bool { return userInfo.RefererId != 0 && - l.svc.Config.Invite.ReferralPercentage != 0 && - (!l.svc.Config.Invite.OnlyFirstPurchase || (isNewPurchase && orderInfo.IsNew)) + (l.svc.Config.Invite.FirstPurchasePercentage != 0 || + l.svc.Config.Invite.FirstYearlyPurchasePercentage != 0 || + l.svc.Config.Invite.NonFirstPurchasePercentage != 0) } -// calculateCommission computes the commission amount based on order price and referral percentage -func (l *ActivateOrderLogic) calculateCommission(price int64) int64 { - return int64(float64(price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100)) +// calculateCommission computes the commission amount based on order price and purchase type +func (l *ActivateOrderLogic) calculateCommission(price int64, isFirstPurchase bool, quantity int64) int64 { + var percentage int64 + if isFirstPurchase { + // 判断是否为年付(12个月) + if quantity == 12 { + percentage = l.svc.Config.Invite.FirstYearlyPurchasePercentage + } else { + percentage = l.svc.Config.Invite.FirstPurchasePercentage + } + } else { + percentage = l.svc.Config.Invite.NonFirstPurchasePercentage + } + return int64(float64(price) * (float64(percentage) / 100)) } // clearServerCache clears user list cache for all servers associated with the subscription