Compare commits

..

11 Commits

Author SHA1 Message Date
d94cbc09b0 Merge branch 'old/master' into old 2025-12-28 17:15:11 -08:00
Tension
0e7cbf4396 fix(payment): update notification URL construction for gateway mode support 2025-12-08 16:09:21 +08:00
Tension
5d632608ab fix(purchase): update notification URL construction for gateway mode support 2025-12-08 15:47:45 +08:00
Tension
338d962618 fix(api): remove default value for size in QueryAnnouncementRequest 2025-12-01 19:01:24 +08:00
Chang lue Tsen
f1794b26b1 revert(swagger): remove basepath from Swagger file generation for improved compatibility 2025-11-30 11:07:17 -05:00
Chang lue Tsen
4cd24e7600 feat(swagger): add basepath to Swagger file generation for improved API routing 2025-11-30 11:02:02 -05:00
Chang lue Tsen
e18809f9b7 feat(report): update registration response structure and enhance error logging 2025-11-30 10:28:11 -05:00
Tension
143445a2fc feat(commission): implement commission withdrawal logic and logging 2025-11-29 14:33:05 +08:00
Chang lue Tsen
7277438b07 feat(user): add commission withdrawal and query withdrawal log functionality 2025-11-26 12:13:33 -05:00
Chang lue Tsen
5c2d0be8e2 feat(adapter): add additional protocol parameters for enhanced configuration 2025-11-24 10:43:10 -05:00
a52c7142ee feat: 添加在线设备统计功能并优化订阅相关逻辑
- 在DeviceManager中添加GetOnlineDeviceCount方法用于获取在线设备数
- 在统计接口中增加在线设备数返回
- 优化订阅查询逻辑,增加服务组关联节点数量计算
- 添加AnyTLS协议支持及相关URI生成功能
- 重构邀请佣金计算逻辑,支持首购/年付/非首购不同比例
- 修复用户基本信息更新中IsAdmin和Enable字段类型不匹配问题
- 更新数据库迁移脚本和配置文件中邀请相关配置项
2025-08-12 07:46:45 -07:00
28 changed files with 475 additions and 60 deletions

View File

@ -127,12 +127,12 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) {
HopPorts: protocol.HopPorts,
HopInterval: protocol.HopInterval,
ObfsPassword: protocol.ObfsPassword,
UpMbps: protocol.UpMbps,
DownMbps: protocol.DownMbps,
DisableSNI: protocol.DisableSNI,
ReduceRtt: protocol.ReduceRtt,
UDPRelayMode: protocol.UDPRelayMode,
CongestionController: protocol.CongestionController,
UpMbps: protocol.UpMbps,
DownMbps: protocol.DownMbps,
PaddingScheme: protocol.PaddingScheme,
Multiplex: protocol.Multiplex,
XhttpMode: protocol.XhttpMode,
@ -145,6 +145,10 @@ func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) {
EncryptionPrivateKey: protocol.EncryptionPrivateKey,
EncryptionClientPadding: protocol.EncryptionClientPadding,
EncryptionPassword: protocol.EncryptionPassword,
Ratio: protocol.Ratio,
CertMode: protocol.CertMode,
CertDNSProvider: protocol.CertDNSProvider,
CertDNSEnv: protocol.CertDNSEnv,
})
}
}

View File

@ -111,6 +111,28 @@ type (
UpdateUserRulesRequest {
Rules []string `json:"rules" validate:"required"`
}
CommissionWithdrawRequest {
Amount int64 `json:"amount"`
Content string `json:"content"`
}
WithdrawalLog {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
Amount int64 `json:"amount"`
Content string `json:"content"`
Status uint8 `json:"status"`
Reason string `json:"reason,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
QueryWithdrawalLogListRequest {
Page int `form:"page"`
Size int `form:"size"`
}
QueryWithdrawalLogListResponse {
List []WithdrawalLog `json:"list"`
Total int64 `json:"total"`
}
)
@server (
@ -222,5 +244,13 @@ service ppanel {
@doc "Update User Rules"
@handler UpdateUserRules
put /rules (UpdateUserRulesRequest)
@doc "Commission Withdraw"
@handler CommissionWithdraw
post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog)
@doc "Query Withdrawal Log"
@handler QueryWithdrawalLog
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
}

View File

@ -183,9 +183,10 @@ type (
Rules []string `json:"rules"`
}
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"`
@ -656,7 +657,7 @@ type (
// public announcement
QueryAnnouncementRequest {
Page int `form:"page"`
Size int `form:"size,default=15"`
Size int `form:"size"`
Pinned *bool `form:"pinned"`
Popup *bool `form:"popup"`
}

View File

@ -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

View File

@ -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'),

View File

@ -0,0 +1,5 @@
DROP TABLE IF EXISTS `withdrawals`;
DELETE FROM `system`
WHERE `category` = 'invite'
AND `key` = 'WithdrawalMethod';

View File

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS `withdrawals` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`user_id` BIGINT NOT NULL COMMENT 'User ID',
`amount` BIGINT NOT NULL COMMENT 'Withdrawal Amount',
`content` TEXT COMMENT 'Withdrawal Content',
`status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Withdrawal Status',
`reason` VARCHAR(500) NOT NULL DEFAULT '' COMMENT 'Rejection Reason',
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
`updated_at` DATETIME NOT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
VALUES
('invite', 'WithdrawalMethod', '', 'string', 'withdrawal method', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637');

View File

@ -200,9 +200,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 {

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Commission Withdraw
func CommissionWithdrawHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.CommissionWithdrawRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewCommissionWithdrawLogic(c.Request.Context(), svcCtx)
resp, err := l.CommissionWithdraw(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Query Withdrawal Log
func QueryWithdrawalLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.QueryWithdrawalLogListRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewQueryWithdrawalLogLogic(c.Request.Context(), svcCtx)
resp, err := l.QueryWithdrawalLog(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -807,6 +807,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Query User Commission Log
publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx))
// Commission Withdraw
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
// Get Device List
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
@ -857,6 +860,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Verify Email
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
// Query Withdrawal Log
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
}
serverGroupRouter := router.Group("/v1/server")

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"github.com/perfect-panel/server/internal/report"
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
"github.com/perfect-panel/server/internal/model/payment"
@ -43,15 +44,31 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe
Total: total,
List: make([]types.PaymentMethodDetail, len(list)),
}
// gateway mod
isGatewayMod := report.IsGatewayMode()
for i, v := range list {
config := make(map[string]interface{})
_ = json.Unmarshal([]byte(v.Config), &config)
notifyUrl := ""
if paymentPlatform.ParsePlatform(v.Platform) != paymentPlatform.Balance {
notifyUrl = v.Domain
if v.Domain != "" {
notifyUrl = v.Domain + "/v1/notify/" + v.Platform + "/" + v.Token
// if is gateway mod, use gateway domain
if isGatewayMod {
notifyUrl += "/api/"
}
notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token
} else {
notifyUrl = "https://" + l.svcCtx.Config.Host + "/v1/notify/" + v.Platform + "/" + v.Token
notifyUrl += "https://" + l.svcCtx.Config.Host
if isGatewayMod {
notifyUrl += "/api/v1/notify/" + v.Platform + "/" + v.Token
} else {
notifyUrl += "/v1/notify/" + v.Platform + "/" + v.Token
}
}
}
resp.List[i] = types.PaymentMethodDetail{

View File

@ -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()

View File

@ -50,10 +50,26 @@ func (l *GetSubscriptionLogic) GetSubscription(req *types.GetSubscriptionRequest
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

View File

@ -7,6 +7,7 @@ import (
"time"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/pkg/constant"
paymentPlatform "github.com/perfect-panel/server/pkg/payment"
@ -275,16 +276,29 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order
return "", err
}
// gateway mod
isGatewayMod := report.IsGatewayMode()
// Build notification URL for payment status callbacks
notifyUrl := ""
if config.Domain != "" {
notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token
notifyUrl = config.Domain
if isGatewayMod {
notifyUrl += "/api/"
}
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
} else {
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
if !ok {
host = l.svcCtx.Config.Host
}
notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token
notifyUrl = "https://" + host
if isGatewayMod {
notifyUrl += "/api"
}
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
}
// Create payment URL for user redirection
@ -317,18 +331,29 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
return "", err
}
// gateway mod
isGatewayMod := report.IsGatewayMode()
// Build notification URL for payment status callbacks
notifyUrl := ""
if config.Domain != "" {
notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token
notifyUrl = config.Domain
if isGatewayMod {
notifyUrl += "/api/"
}
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
} else {
host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string)
if !ok {
host = l.svcCtx.Config.Host
}
notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token
}
notifyUrl = "https://" + host
if isGatewayMod {
notifyUrl += "/api"
}
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
}
// Create payment URL for user redirection
url := client.CreatePayUrl(epay.Order{
Name: l.svcCtx.Config.Site.SiteName,

View File

@ -48,6 +48,7 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi
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
@ -56,6 +57,15 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeLi
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

View File

@ -0,0 +1,108 @@
package user
import (
"context"
"time"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type CommissionWithdrawLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Commission Withdraw
func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommissionWithdrawLogic {
return &CommissionWithdrawLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
if u.Commission < req.Amount {
logger.Errorf("User %d has insufficient commission balance: %.2f, requested: %.2f", u.Id, float64(u.Commission)/100, float64(req.Amount)/100)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", u.Id)
}
tx := l.svcCtx.DB.WithContext(l.ctx).Begin()
// update user commission balance
u.Commission -= req.Amount
if err = l.svcCtx.UserModel.Update(l.ctx, u, tx); err != nil {
tx.Rollback()
l.Errorf("Failed to update user %d commission balance: %v", u.Id, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update user %d commission balance: %v", u.Id, err)
}
// create withdrawal log
logInfo := log.Commission{
Type: log.CommissionTypeConvertBalance,
Amount: req.Amount,
Timestamp: time.Now().UnixMilli(),
}
b, err := logInfo.Marshal()
if err != nil {
tx.Rollback()
l.Errorf("Failed to marshal commission log for user %d: %v", u.Id, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to marshal commission log for user %d: %v", u.Id, err)
}
err = tx.Model(log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeCommission.Uint8(),
Date: time.Now().Format("2006-01-02"),
ObjectID: u.Id,
Content: string(b),
CreatedAt: time.Now(),
}).Error
if err != nil {
tx.Rollback()
l.Errorf("Failed to create commission log for user %d: %v", u.Id, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create commission log for user %d: %v", u.Id, err)
}
err = tx.Model(&user.Withdrawal{}).Create(&user.Withdrawal{
UserId: u.Id,
Amount: req.Amount,
Content: req.Content,
Status: 0,
Reason: "",
}).Error
if err != nil {
tx.Rollback()
l.Errorf("Failed to create withdrawal log for user %d: %v", u.Id, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal log for user %d: %v", u.Id, err)
}
if err = tx.Commit().Error; err != nil {
l.Errorf("Transaction commit failed for user %d withdrawal: %v", u.Id, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Transaction commit failed for user %d withdrawal: %v", u.Id, err)
}
return &types.WithdrawalLog{
UserId: u.Id,
Amount: req.Amount,
Content: req.Content,
Status: 0,
Reason: "",
CreatedAt: time.Now().UnixMilli(),
}, nil
}

View File

@ -0,0 +1,30 @@
package user
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type QueryWithdrawalLogLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewQueryWithdrawalLogLogic Query Withdrawal Log
func NewQueryWithdrawalLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryWithdrawalLogLogic {
return &QueryWithdrawalLogLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalLogListRequest) (resp *types.QueryWithdrawalLogListResponse, err error) {
// todo: add your logic here and delete this line
return
}

View File

@ -33,23 +33,24 @@ const (
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
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
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
GiftTypeIncrease uint16 = 341 // Increase
GiftTypeReduce uint16 = 342 // Reduce
)
// Uint8 converts Type to uint8.

View File

@ -88,6 +88,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

View File

@ -102,3 +102,18 @@ type DeviceOnlineRecord struct {
func (DeviceOnlineRecord) TableName() string {
return "user_device_online_record"
}
type Withdrawal struct {
Id int64 `gorm:"primaryKey"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
Amount int64 `gorm:"not null;comment:Withdrawal Amount"`
Content string `gorm:"type:text;comment:Withdrawal Content"`
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected"`
Reason string `gorm:"type:varchar(500);default:'';comment:Rejection Reason"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (*Withdrawal) TableName() string {
return "user_withdrawal"
}

View File

@ -6,6 +6,10 @@ const (
// RegisterResponse 模块注册响应参数
type RegisterResponse struct {
Success bool `json:"success"` // 注册是否成功
Message string `json:"message"` // 返回信息
Code int `json:"code"` // 响应代码
Message string `json:"message"` // 响应信息
Data struct {
Success bool `json:"success"` // 注册是否成功
Message string `json:"message"` // 返回信息
} `json:"data"` // 响应数据
}

View File

@ -59,6 +59,7 @@ func RegisterModule(port int) error {
// 从环境变量中读取网关模块端口
gatewayPort, err := GatewayPort()
if err != nil {
logger.Errorf("Failed to determine GATEWAY_PORT: %v", err)
return err
}
@ -82,6 +83,7 @@ func RegisterModule(port int) error {
}).SetResult(&response).Post(RegisterAPI)
if err != nil {
logger.Errorf("Failed to register service: %v", err)
return err
}
@ -89,7 +91,8 @@ func RegisterModule(port int) error {
return errors.New("failed to register module: " + result.Status())
}
if !response.Success {
if !response.Data.Success {
logger.Infof("Result: %v", result.String())
return errors.New("failed to register module: " + response.Message)
}
logger.Infof("Module registered successfully: %s", response.Message)

View File

@ -1,11 +1,5 @@
package report
// RegisterServiceResponse 模块注册请求参数
type RegisterServiceResponse struct {
Success bool `json:"success"` // 注册是否成功
Message string `json:"message"` // 返回信息
}
type RegisterServiceRequest struct {
Secret string `json:"secret"` // 通讯密钥
ProxyPath string `json:"proxy_path"` // 代理路径

View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/perfect-panel/server/internal/report"
@ -86,7 +87,7 @@ func (m *Service) Start() {
err = report.RegisterModule(port)
if err != nil {
logger.Errorf("register module error: %s", err.Error())
panic(err)
os.Exit(1)
}
logger.Infof("module registered on port %d", port)
}

View File

@ -239,6 +239,11 @@ type CommissionLog struct {
Timestamp int64 `json:"timestamp"`
}
type CommissionWithdrawRequest struct {
Amount int64 `json:"amount"`
Content string `json:"content"`
}
type Coupon struct {
Id int64 `json:"id"`
Name string `json:"name"`
@ -1723,6 +1728,16 @@ type QueryUserSubscribeNodeListResponse struct {
List []UserSubscribeInfo `json:"list"`
}
type QueryWithdrawalLogListRequest struct {
Page int `form:"page"`
Size int `form:"size"`
}
type QueryWithdrawalLogListResponse struct {
List []WithdrawalLog `json:"list"`
Total int64 `json:"total"`
}
type QuotaTask struct {
Id int64 `json:"id"`
Subscribers []int64 `json:"subscribers"`
@ -2766,3 +2781,14 @@ type VmessProtocol struct {
Network string `json:"network"`
Transport string `json:"transport"`
}
type WithdrawalLog struct {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
Amount int64 `json:"amount"`
Content string `json:"content"`
Status uint8 `json:"status"`
Reason string `json:"reason,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}

View File

@ -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()

View File

@ -18,15 +18,16 @@ const (
// User error
const (
UserExist uint32 = 20001
UserNotExist uint32 = 20002
UserPasswordError uint32 = 20003
UserDisabled uint32 = 20004
InsufficientBalance uint32 = 20005
StopRegister uint32 = 20006
TelegramNotBound uint32 = 20007
UserNotBindOauth uint32 = 20008
InviteCodeError uint32 = 20009
UserExist uint32 = 20001
UserNotExist uint32 = 20002
UserPasswordError uint32 = 20003
UserDisabled uint32 = 20004
InsufficientBalance uint32 = 20005
StopRegister uint32 = 20006
TelegramNotBound uint32 = 20007
UserNotBindOauth uint32 = 20008
InviteCodeError uint32 = 20009
UserCommissionNotEnough uint32 = 20010
)
// Node error