Merge branch perfect-panel/master/server into develop

This commit is contained in:
EUForest
2025-12-11 23:53:32 +08:00
parent 5e46357104
commit 76ff9a658d
60 changed files with 1897 additions and 84 deletions
@@ -0,0 +1,18 @@
package subscribe
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/subscribe"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// Reset all subscribe tokens
func ResetAllSubscribeTokenHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := subscribe.NewResetAllSubscribeTokenLogic(c.Request.Context(), svcCtx)
resp, err := l.ResetAllSubscribeToken()
result.HttpResult(c, resp, err)
}
}
@@ -0,0 +1,18 @@
package system
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/system"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// GetModuleConfigHandler Get Module Config
func GetModuleConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := system.NewGetModuleConfigLogic(c.Request.Context(), svcCtx)
resp, err := l.GetModuleConfig()
result.HttpResult(c, resp, err)
}
}
@@ -0,0 +1,26 @@
package tool
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/tool"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// QueryIPLocationHandler Query IP Location
func QueryIPLocationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.QueryIPLocationRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := tool.NewQueryIPLocationLogic(c.Request.Context(), svcCtx)
resp, err := l.QueryIPLocation(&req)
result.HttpResult(c, resp, err)
}
}
@@ -10,7 +10,6 @@ import (
// Get Client
func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := common.NewGetClientLogic(c.Request.Context(), svcCtx)
resp, err := l.GetClient()
result.HttpResult(c, resp, err)
@@ -0,0 +1,18 @@
package common
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// Heartbeat
func HeartbeatHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := common.NewHeartbeatLogic(c.Request.Context(), svcCtx)
resp, err := l.Heartbeat()
result.HttpResult(c, resp, err)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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"
)
// Update User Rules
func UpdateUserRulesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.UpdateUserRulesRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewUpdateUserRulesLogic(c.Request.Context(), svcCtx)
err := l.UpdateUserRules(&req)
result.HttpResult(c, nil, err)
}
}
@@ -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"
)
// Update User Subscribe Note
func UpdateUserSubscribeNoteHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.UpdateUserSubscribeNoteRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewUpdateUserSubscribeNoteLogic(c.Request.Context(), svcCtx)
err := l.UpdateUserSubscribeNote(&req)
result.HttpResult(c, nil, err)
}
}
+24
View File
@@ -386,6 +386,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get subscribe list
adminSubscribeGroupRouter.GET("/list", adminSubscribe.GetSubscribeListHandler(serverCtx))
// Reset all subscribe tokens
adminSubscribeGroupRouter.POST("/reset_all_token", adminSubscribe.ResetAllSubscribeTokenHandler(serverCtx))
// Subscribe sort
adminSubscribeGroupRouter.POST("/sort", adminSubscribe.SubscribeSortHandler(serverCtx))
}
@@ -409,6 +412,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Update invite config
adminSystemGroupRouter.PUT("/invite_config", adminSystem.UpdateInviteConfigHandler(serverCtx))
// Get Module Config
adminSystemGroupRouter.GET("/module", adminSystem.GetModuleConfigHandler(serverCtx))
// Get node config
adminSystemGroupRouter.GET("/node_config", adminSystem.GetNodeConfigHandler(serverCtx))
@@ -488,6 +494,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
adminToolGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
{
// Query IP Location
adminToolGroupRouter.GET("/ip/location", adminTool.QueryIPLocationHandler(serverCtx))
// Get System Log
adminToolGroupRouter.GET("/log", adminTool.GetSystemLogHandler(serverCtx))
@@ -636,6 +645,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get Client
commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx))
// Heartbeat
commonGroupRouter.GET("/heartbeat", common.HeartbeatHandler(serverCtx))
// Get verification code
commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx))
@@ -796,6 +808,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))
// Delete Current User Account
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
@@ -820,12 +835,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Update User Password
publicUserGroupRouter.PUT("/password", publicUser.UpdateUserPasswordHandler(serverCtx))
// Update User Rules
publicUserGroupRouter.PUT("/rules", publicUser.UpdateUserRulesHandler(serverCtx))
// Query User Subscribe
publicUserGroupRouter.GET("/subscribe", publicUser.QueryUserSubscribeHandler(serverCtx))
// Get Subscribe Log
publicUserGroupRouter.GET("/subscribe_log", publicUser.GetSubscribeLogHandler(serverCtx))
// Update User Subscribe Note
publicUserGroupRouter.PUT("/subscribe_note", publicUser.UpdateUserSubscribeNoteHandler(serverCtx))
// Reset User Subscribe Token
publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx))
@@ -846,6 +867,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))
}
publicUserWsGroupRouter := router.Group("/v1/public/user")
+16
View File
@@ -23,6 +23,22 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
ua := c.GetHeader("User-Agent")
req.UA = c.Request.Header.Get("User-Agent")
req.Flag = c.Query("flag")
if svcCtx.Config.Subscribe.PanDomain {
domain := c.Request.Host
domainArr := strings.Split(domain, ".")
short, err := tool.FixedUniqueString(req.Token, 8, "")
if err != nil {
logger.Errorf("[SubscribeHandler] Generate short token failed: %v", err)
c.String(http.StatusInternalServerError, "Internal Server")
c.Abort()
return
}
if short != domainArr[0] {
c.String(http.StatusForbidden, "Access denied")
c.Abort()
return
}
}
if svcCtx.Config.Subscribe.UserAgentLimit {
if ua == "" {
@@ -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{
@@ -0,0 +1,61 @@
package subscribe
import (
"context"
"strconv"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/pkg/uuidx"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type ResetAllSubscribeTokenLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Reset all subscribe tokens
func NewResetAllSubscribeTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetAllSubscribeTokenLogic {
return &ResetAllSubscribeTokenLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ResetAllSubscribeTokenLogic) ResetAllSubscribeToken() (resp *types.ResetAllSubscribeTokenResponse, err error) {
var list []*user.Subscribe
tx := l.svcCtx.DB.WithContext(l.ctx).Begin()
// select all active and Finished subscriptions
if err = tx.Model(&user.Subscribe{}).Where("`status` IN ?", []int64{1, 2}).Find(&list).Error; err != nil {
logger.Errorf("[ResetAllSubscribeToken] Failed to fetch subscribe list: %v", err.Error())
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to fetch subscribe list: %v", err.Error())
}
for _, sub := range list {
sub.Token = uuidx.SubscribeToken(strconv.FormatInt(time.Now().UnixMilli(), 10) + strconv.FormatInt(sub.Id, 10))
sub.UUID = uuidx.NewUUID().String()
if err = tx.Model(&user.Subscribe{}).Where("id = ?", sub.Id).Save(sub).Error; err != nil {
tx.Rollback()
logger.Errorf("[ResetAllSubscribeToken] Failed to update subscribe token for ID %d: %v", sub.Id, err.Error())
return &types.ResetAllSubscribeTokenResponse{
Success: false,
}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update subscribe token for ID %d: %v", sub.Id, err.Error())
}
}
if err = tx.Commit().Error; err != nil {
logger.Errorf("[ResetAllSubscribeToken] Failed to commit transaction: %v", err.Error())
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to commit transaction: %v", err.Error())
}
return &types.ResetAllSubscribeTokenResponse{
Success: true,
}, nil
}
@@ -0,0 +1,41 @@
package system
import (
"context"
"os"
"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 GetModuleConfigLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get Module Config
func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetModuleConfigLogic {
return &GetModuleConfigLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
value, exists := os.LookupEnv("SECRET_KEY")
if !exists {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
}
return &types.ModuleConfig{
Secret: value,
ServiceName: constant.ServiceName,
ServiceVersion: constant.Version,
}, nil
}
@@ -0,0 +1,57 @@
package tool
import (
"context"
"net"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type QueryIPLocationLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewQueryIPLocationLogic Query IP Location
func NewQueryIPLocationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryIPLocationLogic {
return &QueryIPLocationLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *QueryIPLocationLogic) QueryIPLocation(req *types.QueryIPLocationRequest) (resp *types.QueryIPLocationResponse, err error) {
if l.svcCtx.GeoIP == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " GeoIP database not configured")
}
ip := net.ParseIP(req.IP)
record, err := l.svcCtx.GeoIP.DB.City(ip)
if err != nil {
l.Errorf("Failed to query IP location: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to query IP location")
}
var country, region, city string
if record.Country.Names != nil {
country = record.Country.Names["en"]
}
if len(record.Subdivisions) > 0 && record.Subdivisions[0].Names != nil {
region = record.Subdivisions[0].Names["en"]
}
if record.City.Names != nil {
city = record.City.Names["en"]
}
return &types.QueryIPLocationResponse{
Country: country,
Region: region,
City: city,
}, nil
}
+33
View File
@@ -0,0 +1,33 @@
package common
import (
"context"
"time"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type HeartbeatLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewHeartbeatLogic Heartbeat
func NewHeartbeatLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HeartbeatLogic {
return &HeartbeatLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *HeartbeatLogic) Heartbeat() (resp *types.HeartbeatResponse, err error) {
return &types.HeartbeatResponse{
Status: true,
Message: "service is alive",
Timestamp: time.Now().Unix(),
}, nil
}
@@ -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,
@@ -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
}
@@ -60,6 +60,8 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub
}
}
short, _ := tool.FixedUniqueString(item.Token, 8, "")
sub.Short = short
sub.ResetTime = calculateNextResetTime(&sub)
resp.List = append(resp.List, sub)
}
@@ -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
}
@@ -0,0 +1,51 @@
package user
import (
"context"
"encoding/json"
"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 UpdateUserRulesLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewUpdateUserRulesLogic Update User Rules
func NewUpdateUserRulesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserRulesLogic {
return &UpdateUserRulesLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateUserRulesLogic) UpdateUserRules(req *types.UpdateUserRulesRequest) error {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
if len(req.Rules) > 0 {
bytes, err := json.Marshal(req.Rules)
if err != nil {
l.Logger.Errorf("UpdateUserRulesLogic json marshal rules error: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json marshal rules failed: %v", err.Error())
}
u.Rules = string(bytes)
err = l.svcCtx.UserModel.Update(l.ctx, u)
if err != nil {
l.Logger.Errorf("UpdateUserRulesLogic UpdateUserRules error: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user rules failed: %v", err.Error())
}
}
return nil
}
@@ -0,0 +1,73 @@
package user
import (
"context"
"github.com/perfect-panel/server/pkg/constant"
"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/logger"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type UpdateUserSubscribeNoteLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewUpdateUserSubscribeNoteLogic Update User Subscribe Note
func NewUpdateUserSubscribeNoteLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserSubscribeNoteLogic {
return &UpdateUserSubscribeNoteLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateUserSubscribeNoteLogic) UpdateUserSubscribeNote(req *types.UpdateUserSubscribeNoteRequest) error {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId)
if err != nil {
l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error())
}
if userSub.UserId != u.Id {
l.Errorw("UserSubscribeId does not belong to the current user")
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "UserSubscribeId does not belong to the current user")
}
userSub.Note = req.Note
var newSub user.Subscribe
tool.DeepCopy(&newSub, userSub)
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &newSub)
if err != nil {
l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error())
}
// Clear user subscription cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &newSub); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
}
// Clear subscription cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
}
return nil
}
+18 -17
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.
+1
View File
@@ -36,6 +36,7 @@ type SubscribeDetails struct {
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: Cancelled"`
Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
+17
View File
@@ -25,6 +25,7 @@ type User struct {
EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"`
AuthMethods []AuthMethods `gorm:"foreignKey:UserId;references:Id"`
UserDevices []Device `gorm:"foreignKey:UserId;references:Id"`
Rules string `gorm:"type:TEXT;comment:User Rules"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
@@ -48,6 +49,7 @@ type Subscribe struct {
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"`
Note string `gorm:"type:varchar(500);default:'';comment:User note for subscription"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
@@ -100,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"
}
+15
View File
@@ -0,0 +1,15 @@
package report
const (
RegisterAPI = "/basic/register" // 模块注册接口
)
// RegisterResponse 模块注册响应参数
type RegisterResponse struct {
Code int `json:"code"` // 响应代码
Message string `json:"message"` // 响应信息
Data struct {
Success bool `json:"success"` // 注册是否成功
Message string `json:"message"` // 返回信息
} `json:"data"` // 响应数据
}
+113
View File
@@ -0,0 +1,113 @@
package report
import (
"fmt"
"net"
"os"
"github.com/go-resty/resty/v2"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/pkg/errors"
)
// FreePort returns a free TCP port by opening a listener on port 0.
func FreePort() (int, error) {
l, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}
defer l.Close()
// Get the assigned port
addr := l.Addr().(*net.TCPAddr)
return addr.Port, nil
}
// ModulePort returns the module port from the environment variable or a free port.
func ModulePort() (int, error) {
// 从环境变量获取端口号
value, exists := os.LookupEnv("PPANEL_PORT")
if exists {
var port int
_, err := fmt.Sscanf(value, "%d", &port)
if err != nil {
return FreePort()
}
return port, nil
}
return FreePort()
}
// GatewayPort returns the gateway port from the environment variable or a free port.
func GatewayPort() (int, error) {
// 从环境变量获取端口号
value, exists := os.LookupEnv("GATEWAY_PORT")
if exists {
var port int
_, err := fmt.Sscanf(value, "%d", &port)
if err != nil {
logger.Errorf("Failed to parse GATEWAY_PORT: %v Value %s", err.Error(), value)
panic(err)
}
return port, nil
}
return 0, errors.New("could not determine gateway port")
}
// RegisterModule registers a module with the gateway.
func RegisterModule(port int) error {
// 从环境变量中读取网关模块端口
gatewayPort, err := GatewayPort()
if err != nil {
logger.Errorf("Failed to determine GATEWAY_PORT: %v", err)
return err
}
// 从环境变量中获取通讯密钥
value, exists := os.LookupEnv("SECRET_KEY")
if !exists {
panic("could not determine secret key")
}
var response RegisterResponse
client := resty.New().SetBaseURL(fmt.Sprintf("http://127.0.0.1:%d", gatewayPort))
result, err := client.R().SetHeader("Content-Type", "application/json").SetBody(RegisterServiceRequest{
Secret: value,
ProxyPath: "/api",
ServiceURL: fmt.Sprintf("http://127.0.0.1:%d", port),
Repository: constant.Repository,
HeartbeatURL: fmt.Sprintf("http://127.0.0.1:%d/v1/common/heartbeat", port),
ServiceName: constant.ServiceName,
ServiceVersion: constant.Version,
}).SetResult(&response).Post(RegisterAPI)
if err != nil {
logger.Errorf("Failed to register service: %v", err)
return err
}
if result.IsError() {
return errors.New("failed to register module: " + result.Status())
}
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)
return nil
}
// IsGatewayMode checks if the application is running in gateway mode.
// It returns true if GATEWAY_MODE is set to "true" and GATEWAY_PORT is valid.
func IsGatewayMode() bool {
value, exists := os.LookupEnv("GATEWAY_MODE")
if exists && value == "true" {
if _, err := GatewayPort(); err == nil {
return true
}
}
return false
}
+21
View File
@@ -0,0 +1,21 @@
package report
import (
"testing"
)
func TestFreePort(t *testing.T) {
port, err := FreePort()
if err != nil {
t.Fatalf("FreePort() error: %v", err)
}
t.Logf("FreePort: %v", port)
}
func TestModulePort(t *testing.T) {
port, err := ModulePort()
if err != nil {
t.Fatalf("ModulePort() error: %v", err)
}
t.Logf("ModulePort: %v", port)
}
+11
View File
@@ -0,0 +1,11 @@
package report
type RegisterServiceRequest struct {
Secret string `json:"secret"` // 通讯密钥
ProxyPath string `json:"proxy_path"` // 代理路径
ServiceURL string `json:"service_url"` // 服务地址
Repository string `json:"repository"` // 服务代码仓库
ServiceName string `json:"service_name"` // 服务名称
HeartbeatURL string `json:"heartbeat_url"` // 心跳检测地址
ServiceVersion string `json:"service_version"` // 服务版本
}
+27 -2
View File
@@ -6,8 +6,10 @@ import (
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/perfect-panel/server/internal/report"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/proc"
@@ -48,7 +50,7 @@ func initServer(svc *svc.ServiceContext) *gin.Engine {
}
r.Use(sessions.Sessions("ppanel", sessionStore))
// use cors middleware
r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, middleware.PanDomainMiddleware(svc), gin.Recovery())
r.Use(middleware.TraceMiddleware(svc), middleware.LoggerMiddleware(svc), middleware.CorsMiddleware, gin.Recovery())
// register handlers
handler.RegisterHandlers(r, svc)
@@ -65,9 +67,32 @@ func (m *Service) Start() {
if m.svc == nil {
panic("config file path is nil")
}
// init service
r := initServer(m.svc)
serverAddr := fmt.Sprintf("%v:%d", m.svc.Config.Host, m.svc.Config.Port)
// get server port
port := m.svc.Config.Port
host := m.svc.Config.Host
// check gateway mode
if report.IsGatewayMode() {
// get free port
freePort, err := report.ModulePort()
if err != nil {
logger.Errorf("get module port error: %s", err.Error())
panic(err)
}
port = freePort
host = "127.0.0.1"
// register module
err = report.RegisterModule(port)
if err != nil {
logger.Errorf("register module error: %s", err.Error())
os.Exit(1)
}
logger.Infof("module registered on port %d", port)
}
serverAddr := fmt.Sprintf("%v:%d", host, port)
m.server = &http.Server{
Addr: serverAddr,
Handler: r,
+74
View File
@@ -0,0 +1,74 @@
package svc
import (
"io"
"net/http"
"os"
"path/filepath"
"github.com/oschwald/geoip2-golang"
"github.com/perfect-panel/server/pkg/logger"
)
const GeoIPDBURL = "https://raw.githubusercontent.com/adysec/IP_database/main/geolite/GeoLite2-City.mmdb"
type IPLocation struct {
Path string
DB *geoip2.Reader
}
func NewIPLocation(path string) (*IPLocation, error) {
// 检查文件是否存在
if _, err := os.Stat(path); os.IsNotExist(err) {
logger.Infof("[GeoIP] Database not found, downloading from %s", GeoIPDBURL)
// 文件不存在,下载数据库
err := DownloadGeoIPDatabase(GeoIPDBURL, path)
if err != nil {
logger.Errorf("[GeoIP] Failed to download database: %v", err.Error())
return nil, err
}
logger.Infof("[GeoIP] Database downloaded successfully")
}
db, err := geoip2.Open(path)
if err != nil {
return nil, err
}
return &IPLocation{
Path: path,
DB: db,
}, nil
}
func (ipLoc *IPLocation) Close() error {
return ipLoc.DB.Close()
}
func DownloadGeoIPDatabase(url, path string) error {
// 创建路径, 确保目录存在
err := os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
logger.Errorf("[GeoIP] Failed to create directory: %v", err.Error())
return err
}
// 创建文件
out, err := os.Create(path)
if err != nil {
return err
}
defer out.Close()
// 请求远程文件
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// 保存文件
_, err = io.Copy(out, resp.Body)
return err
}
+10
View File
@@ -37,6 +37,7 @@ type ServiceContext struct {
Config config.Config
Queue *asynq.Client
ExchangeRate float64
GeoIP *IPLocation
//NodeCache *cache.NodeCacheClient
AuthModel auth.Model
@@ -68,9 +69,17 @@ func NewServiceContext(c config.Config) *ServiceContext {
db, err := orm.ConnectMysql(orm.Mysql{
Config: c.MySQL,
})
if err != nil {
panic(err.Error())
}
// IP location initialize
geoIP, err := NewIPLocation("./cache/GeoLite2-City.mmdb")
if err != nil {
panic(err.Error())
}
rds := redis.NewClient(&redis.Options{
Addr: c.Redis.Host,
Password: c.Redis.Pass,
@@ -89,6 +98,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
Config: c,
Queue: NewAsynqClient(c),
ExchangeRate: 1.0,
GeoIP: geoIP,
//NodeCache: cache.NewNodeCacheClient(rds),
AuthLimiter: authLimiter,
AdsModel: ads.NewModel(db, rds),
+64 -1
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 ConnectionRecords struct {
CurrentContinuousDays int64 `json:"current_continuous_days"`
HistoryContinuousDays int64 `json:"history_continuous_days"`
@@ -1169,6 +1174,12 @@ type HasMigrateSeverNodeResponse struct {
HasMigrate bool `json:"has_migrate"`
}
type HeartbeatResponse struct {
Status bool `json:"status"`
Message string `json:"message,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
type Hysteria2 struct {
Port int `json:"port" validate:"required"`
HopPorts string `json:"hop_ports" validate:"required"`
@@ -1232,6 +1243,12 @@ type MobileAuthenticateConfig struct {
Whitelist []string `json:"whitelist"`
}
type ModuleConfig struct {
Secret string `json:"secret"` // 通讯密钥
ServiceName string `json:"service_name"` // 服务名称
ServiceVersion string `json:"service_version"` // 服务版本
}
type Node struct {
Id int64 `json:"id"`
Name string `json:"name"`
@@ -1551,7 +1568,7 @@ type PurchaseOrderResponse struct {
type QueryAnnouncementRequest struct {
Page int `form:"page"`
Size int `form:"size,default=15"`
Size int `form:"size"`
Pinned *bool `form:"pinned"`
Popup *bool `form:"popup"`
}
@@ -1570,6 +1587,16 @@ type QueryDocumentListResponse struct {
List []Document `json:"list"`
}
type QueryIPLocationRequest struct {
IP string `form:"ip" validate:"required"`
}
type QueryIPLocationResponse struct {
Country string `json:"country"`
Region string `json:"region,omitempty"`
City string `json:"city"`
}
type QueryNodeTagResponse struct {
Tags []string `json:"tags"`
}
@@ -1712,6 +1739,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"`
@@ -1771,6 +1808,10 @@ type RenewalOrderResponse struct {
OrderNo string `json:"order_no"`
}
type ResetAllSubscribeTokenResponse struct {
Success bool `json:"success"`
}
type ResetPasswordRequest struct {
Identifier string `json:"identifier"`
Email string `json:"email" validate:"required"`
@@ -2464,6 +2505,15 @@ type UpdateUserPasswordRequest struct {
Password string `json:"password" validate:"required"`
}
type UpdateUserRulesRequest struct {
Rules []string `json:"rules" validate:"required"`
}
type UpdateUserSubscribeNoteRequest struct {
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
Note string `json:"note" validate:"max=500"`
}
type UpdateUserSubscribeRequest struct {
UserSubscribeId int64 `json:"user_subscribe_id"`
SubscribeId int64 `json:"subscribe_id"`
@@ -2497,6 +2547,7 @@ type User struct {
EnableTradeNotify bool `json:"enable_trade_notify"`
AuthMethods []UserAuthMethod `json:"auth_methods"`
UserDevices []UserDevice `json:"user_devices"`
Rules []string `json:"rules"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at,omitempty"`
@@ -2587,6 +2638,7 @@ type UserSubscribe struct {
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
Short string `json:"short"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
@@ -2751,3 +2803,14 @@ type WeeklyStat struct {
DayName string `json:"day_name"`
Hours float64 `json:"hours"`
}
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"`
}