path
Build docker and publish / build (20.15.1) (push) Successful in 8m56s

This commit is contained in:
2026-05-19 19:35:44 -07:00
parent a1184ef5ed
commit fd522b6c71
31 changed files with 723 additions and 163 deletions
@@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Approve withdrawal
func ApproveWithdrawalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.ApproveWithdrawalRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewApproveWithdrawalLogic(c.Request.Context(), svcCtx)
err := l.ApproveWithdrawal(&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/admin/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Get withdrawal list
func GetWithdrawalListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetWithdrawalListRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewGetWithdrawalListLogic(c.Request.Context(), svcCtx)
resp, err := l.GetWithdrawalList(&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/admin/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Reject withdrawal
func RejectWithdrawalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.RejectWithdrawalRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewRejectWithdrawalLogic(c.Request.Context(), svcCtx)
err := l.RejectWithdrawal(&req)
result.HttpResult(c, nil, err)
}
}
+9
View File
@@ -707,6 +707,15 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Get admin user invite list
adminUserGroupRouter.GET("/invite/list", adminUser.GetAdminUserInviteListHandler(serverCtx))
// Get withdrawal list
adminUserGroupRouter.GET("/withdrawal/list", adminUser.GetWithdrawalListHandler(serverCtx))
// Approve withdrawal
adminUserGroupRouter.POST("/withdrawal/approve", adminUser.ApproveWithdrawalHandler(serverCtx))
// Reject withdrawal
adminUserGroupRouter.POST("/withdrawal/reject", adminUser.RejectWithdrawalHandler(serverCtx))
}
authGroupRouter := router.Group("/v1/auth")
@@ -0,0 +1,27 @@
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 ApproveWithdrawalLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewApproveWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApproveWithdrawalLogic {
return &ApproveWithdrawalLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ApproveWithdrawalLogic) ApproveWithdrawal(req *types.ApproveWithdrawalRequest) error {
return approveWithdrawal(l.ctx, l.svcCtx, req.WithdrawalId)
}
@@ -0,0 +1,74 @@
package user
import (
"context"
usermodel "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/xerr"
"github.com/pkg/errors"
)
type GetWithdrawalListLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetWithdrawalListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetWithdrawalListLogic {
return &GetWithdrawalListLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListRequest) (*types.GetWithdrawalListResponse, error) {
page := req.Page
size := req.Size
if page <= 0 {
page = 1
}
if size <= 0 {
size = 10
}
query := l.svcCtx.DB.WithContext(l.ctx).Model(&usermodel.Withdrawal{})
if req.UserId != nil {
query = query.Where("user_id = ?", *req.UserId)
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count withdrawals failed: %v", err)
}
var rows []usermodel.Withdrawal
if err := query.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&rows).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query withdrawals failed: %v", err)
}
list := make([]types.WithdrawalLog, 0, len(rows))
for _, row := range rows {
list = append(list, types.WithdrawalLog{
Id: row.Id,
UserId: row.UserId,
Amount: row.Amount,
Content: row.Content,
Status: row.Status,
Reason: row.Reason,
CreatedAt: row.CreatedAt.UnixMilli(),
UpdatedAt: row.UpdatedAt.UnixMilli(),
})
}
return &types.GetWithdrawalListResponse{
List: list,
Total: total,
}, nil
}
@@ -0,0 +1,27 @@
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 RejectWithdrawalLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRejectWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RejectWithdrawalLogic {
return &RejectWithdrawalLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RejectWithdrawalLogic) RejectWithdrawal(req *types.RejectWithdrawalRequest) error {
return rejectWithdrawal(l.ctx, l.svcCtx, req.WithdrawalId, req.Reason)
}
@@ -6,6 +6,7 @@ import (
"strings"
"time"
logicCommon "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
@@ -100,21 +101,15 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
}
if req.Commission != userInfo.Commission {
commentLog := log.Commission{
Type: log.CommissionTypeAdjust,
Amount: req.Commission - userInfo.Commission,
Timestamp: time.Now().UnixMilli(),
if isWithdrawalScene(req.Remark) {
logWithdrawalGuard(l.Logger, userInfo.Id)
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "commission overwrite is blocked in withdrawal scene")
}
content, _ := commentLog.Marshal()
err = tx.Create(&log.SystemLog{
Type: log.TypeCommission.Uint8(),
Date: time.Now().Format(time.DateOnly),
ObjectID: userInfo.Id,
Content: string(content),
}).Error
if err != nil {
change := req.Commission - userInfo.Commission
if err = l.svcCtx.UserModel.UpdateCommission(l.ctx, userInfo.Id, change, tx); err != nil {
return err
}
if err = logicCommon.WriteCommissionLog(tx, userInfo.Id, log.CommissionTypeAdjust, change, ""); err != nil {
return err
}
userInfo.Commission = req.Commission
@@ -0,0 +1,81 @@
package user
import (
"context"
"strings"
logicCommon "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
usermodel "github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
func approveWithdrawal(ctx context.Context, svcCtx *svc.ServiceContext, withdrawalID int64) error {
return svcCtx.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
withdrawal, err := logicCommon.LoadPendingWithdrawalForUpdate(ctx, tx, withdrawalID)
if err != nil {
if err.Error() == "withdrawal status invalid" {
return errors.Wrapf(xerr.NewErrCode(xerr.WithdrawalStatusInvalid), "withdrawal %d already processed", withdrawalID)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "load withdrawal failed: %v", err)
}
if err := tx.Model(&usermodel.Withdrawal{}).
Where("id = ? AND status = 0", withdrawalID).
Updates(map[string]interface{}{
"status": 1,
"reason": "",
}).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "approve withdrawal failed: %v", err)
}
if err := logicCommon.WriteCommissionLog(tx, withdrawal.UserId, log.CommissionTypeWithdraw, withdrawal.Amount, ""); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "write commission log failed: %v", err)
}
return nil
})
}
func rejectWithdrawal(ctx context.Context, svcCtx *svc.ServiceContext, withdrawalID int64, reason string) error {
reason = strings.TrimSpace(reason)
return svcCtx.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
withdrawal, err := logicCommon.LoadPendingWithdrawalForUpdate(ctx, tx, withdrawalID)
if err != nil {
if err.Error() == "withdrawal status invalid" {
return errors.Wrapf(xerr.NewErrCode(xerr.WithdrawalStatusInvalid), "withdrawal %d already processed", withdrawalID)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "load withdrawal failed: %v", err)
}
if err := tx.Model(&usermodel.Withdrawal{}).
Where("id = ? AND status = 0", withdrawalID).
Updates(map[string]interface{}{
"status": 2,
"reason": reason,
}).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "reject withdrawal failed: %v", err)
}
if err := svcCtx.UserModel.UpdateCommission(ctx, withdrawal.UserId, withdrawal.Amount, tx); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund commission failed: %v", err)
}
if err := logicCommon.WriteCommissionLog(tx, withdrawal.UserId, log.CommissionTypeWithdrawReject, withdrawal.Amount, ""); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "write commission log failed: %v", err)
}
return nil
})
}
func isWithdrawalScene(remark string) bool {
normalized := strings.ToLower(strings.TrimSpace(remark))
return strings.Contains(normalized, "withdraw") || strings.Contains(normalized, "提现")
}
func logWithdrawalGuard(l logger.Logger, userID int64) {
l.Errorw("blocked commission overwrite in withdrawal scene", logger.Field("user_id", userID))
}
+94 -82
View File
@@ -1,108 +1,120 @@
package common
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net"
"strings"
"time"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net"
"strings"
"time"
"github.com/gin-gonic/gin"
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
"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"
"github.com/gin-gonic/gin"
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
"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 ReportLogMessageLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewReportLogMessageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ReportLogMessageLogic {
return &ReportLogMessageLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx }
return &ReportLogMessageLogic{Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
func (l *ReportLogMessageLogic) ReportLogMessage(req *types.ReportLogMessageRequest, c *gin.Context) (resp *types.ReportLogMessageResponse, err error) {
ip := clientIP(c)
ua := c.GetHeader("User-Agent")
locale := c.GetHeader("Accept-Language")
ip := clientIP(c)
ua := c.GetHeader("User-Agent")
locale := c.GetHeader("Accept-Language")
// 简单限流:设备ID优先,其次IP
limitKey := "logmsg:" + strings.TrimSpace(req.DeviceId)
if limitKey == "logmsg:" { limitKey = "logmsg:" + ip }
count, _ := l.svcCtx.Redis.Incr(l.ctx, limitKey).Result()
if count == 1 { _ = l.svcCtx.Redis.Expire(l.ctx, limitKey, 60*time.Second).Err() }
if count > 120 { // 每分钟最多120条
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "too many reports")
}
// 简单限流:设备ID优先,其次IP
limitKey := "logmsg:" + strings.TrimSpace(req.DeviceId)
if limitKey == "logmsg:" {
limitKey = "logmsg:" + ip
}
count, _ := l.svcCtx.Redis.Incr(l.ctx, limitKey).Result()
if count == 1 {
_ = l.svcCtx.Redis.Expire(l.ctx, limitKey, 60*time.Second).Err()
}
if count > 120 { // 每分钟最多120条
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "too many reports")
}
// 指纹生成
h := sha256.New()
h.Write([]byte(strings.Join([]string{req.Message, req.Stack, req.ErrorCode, req.AppVersion, req.Platform}, "|")))
digest := hex.EncodeToString(h.Sum(nil))
// 指纹生成
h := sha256.New()
h.Write([]byte(strings.Join([]string{req.Message, req.Stack, req.ErrorCode, req.AppVersion, req.Platform}, "|")))
digest := hex.EncodeToString(h.Sum(nil))
var ctxStr string
if req.Context != nil {
if b, e := json.Marshal(req.Context); e == nil {
ctxStr = string(b)
}
}
var occurredAt *time.Time
if req.OccurredAt > 0 {
t := time.UnixMilli(req.OccurredAt)
occurredAt = &t
}
var ctxStr string
if req.Context != nil {
if b, e := json.Marshal(req.Context); e == nil {
ctxStr = string(b)
}
}
var occurredAt *time.Time
if req.OccurredAt > 0 {
t := time.UnixMilli(req.OccurredAt)
occurredAt = &t
}
var userIdPtr *int64
if req.UserId > 0 { userIdPtr = &req.UserId }
var userIdPtr *int64
if req.UserId > 0 {
userIdPtr = &req.UserId
}
row := &logmessage.LogMessage{
Platform: req.Platform,
AppVersion: req.AppVersion,
OsName: req.OsName,
OsVersion: req.OsVersion,
DeviceId: req.DeviceId,
UserId: userIdPtr,
SessionId: req.SessionId,
Level: req.Level,
ErrorCode: req.ErrorCode,
Message: safeTruncate(req.Message, 1024*64),
Stack: safeTruncate(req.Stack, 1024*1024),
Context: ctxStr,
ClientIP: ip,
UserAgent: safeTruncate(ua, 255),
Locale: safeTruncate(locale, 16),
Digest: digest,
OccurredAt: occurredAt,
}
row := &logmessage.LogMessage{
Platform: safeTruncate(req.Platform, 32),
AppVersion: safeTruncate(req.AppVersion, 64),
OsName: safeTruncate(req.OsName, 64),
OsVersion: safeTruncate(req.OsVersion, 64),
DeviceId: safeTruncate(req.DeviceId, 255),
UserId: userIdPtr,
SessionId: safeTruncate(req.SessionId, 255),
Level: req.Level,
ErrorCode: safeTruncate(req.ErrorCode, 128),
Message: safeTruncate(req.Message, 1024*64),
Stack: safeTruncate(req.Stack, 1024*1024),
Context: ctxStr,
ClientIP: ip,
UserAgent: safeTruncate(ua, 255),
Locale: safeTruncate(locale, 16),
Digest: digest,
OccurredAt: occurredAt,
}
if err = l.svcCtx.LogMessageModel.Insert(l.ctx, row); err != nil {
// 唯一指纹冲突时尝试查询已有记录返回ID
ex, _, findErr := l.svcCtx.LogMessageModel.Filter(l.ctx, &logmessage.FilterParams{ Keyword: req.Message, Page: 1, Size: 1 })
if findErr == nil && len(ex) > 0 {
return &types.ReportLogMessageResponse{ Id: ex[0].Id }, nil
}
l.Errorf("[ReportLogMessage] insert error: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "insert log_message failed: %v", err)
}
return &types.ReportLogMessageResponse{ Id: row.Id }, nil
if err = l.svcCtx.LogMessageModel.Insert(l.ctx, row); err != nil {
// 唯一指纹冲突时尝试查询已有记录返回ID
ex, _, findErr := l.svcCtx.LogMessageModel.Filter(l.ctx, &logmessage.FilterParams{Keyword: req.Message, Page: 1, Size: 1})
if findErr == nil && len(ex) > 0 {
return &types.ReportLogMessageResponse{Id: ex[0].Id}, nil
}
l.Errorf("[ReportLogMessage] insert error: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "insert log_message failed: %v", err)
}
return &types.ReportLogMessageResponse{Id: row.Id}, nil
}
func safeTruncate(s string, n int) string {
if len(s) <= n { return s }
return s[:n]
if len(s) <= n {
return s
}
return s[:n]
}
func clientIP(c *gin.Context) string {
ip := c.ClientIP()
if ip != "" { return ip }
host, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err == nil && host != "" { return host }
return ""
ip := c.ClientIP()
if ip != "" {
return ip
}
host, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err == nil && host != "" {
return host
}
return ""
}
+48
View File
@@ -0,0 +1,48 @@
package common
import (
"context"
"time"
"github.com/perfect-panel/server/internal/model/log"
usermodel "github.com/perfect-panel/server/internal/model/user"
"github.com/pkg/errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func WriteCommissionLog(tx *gorm.DB, objectID int64, logType uint16, amount int64, orderNo string) error {
logInfo := log.Commission{
Type: logType,
Amount: amount,
OrderNo: orderNo,
Timestamp: time.Now().UnixMilli(),
}
content, err := logInfo.Marshal()
if err != nil {
return err
}
return tx.Model(log.SystemLog{}).Create(&log.SystemLog{
Type: log.TypeCommission.Uint8(),
Date: time.Now().Format(time.DateOnly),
ObjectID: objectID,
Content: string(content),
CreatedAt: time.Now(),
}).Error
}
func LoadPendingWithdrawalForUpdate(ctx context.Context, tx *gorm.DB, withdrawalID int64) (*usermodel.Withdrawal, error) {
var withdrawal usermodel.Withdrawal
if err := tx.WithContext(ctx).
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", withdrawalID).
First(&withdrawal).Error; err != nil {
return nil, err
}
if withdrawal.Status != 0 {
return nil, errors.New("withdrawal status invalid")
}
return &withdrawal, nil
}
+4 -4
View File
@@ -109,14 +109,14 @@ func buildObjectKey(prefix string, userID int64, bizType, fileID, fileName strin
if prefix == "" {
prefix = "app-upload"
}
return fmt.Sprintf("%s/%s/%d/%04d/%02d/%s_%s",
return fmt.Sprintf("%s/%04d/%02d/%02d/%d/%s__%s",
prefix,
strings.Trim(safeFileNameRegexp.ReplaceAllString(strings.ToLower(strings.TrimSpace(bizType)), "-"), "-"),
userID,
now.Year(),
int(now.Month()),
fileID,
now.Day(),
userID,
safeFileName(fileName),
fileID,
)
}
@@ -4,6 +4,7 @@ import (
"context"
"time"
logicCommon "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
@@ -12,6 +13,7 @@ import (
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type CommissionWithdrawLogic struct {
@@ -42,38 +44,21 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
}
tx := l.svcCtx.DB.WithContext(l.ctx).Begin()
now := time.Now()
// update user commission balance
u.Commission -= req.Amount
if err = l.svcCtx.UserModel.Update(l.ctx, u, tx); err != nil {
// Atomically deduct the requested amount so concurrent commission growth is preserved.
if err = l.svcCtx.DB.WithContext(l.ctx).
Model(&user.User{}).
Where("id = ? AND commission >= ?", u.Id, req.Amount).
UpdateColumn("commission", gorm.Expr("commission - ?", req.Amount)).Error; 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)
}
_ = l.svcCtx.UserModel.ClearUserCache(l.ctx, u)
// 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 {
if err = logicCommon.WriteCommissionLog(tx, u.Id, log.CommissionTypeConvertBalance, req.Amount, ""); 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)
@@ -103,6 +88,7 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
Content: req.Content,
Status: 0,
Reason: "",
CreatedAt: time.Now().UnixMilli(),
CreatedAt: now.UnixMilli(),
UpdatedAt: now.UnixMilli(),
}, nil
}
@@ -3,9 +3,13 @@ package user
import (
"context"
"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 QueryWithdrawalLogLogic struct {
@@ -24,7 +28,49 @@ func NewQueryWithdrawalLogLogic(ctx context.Context, svcCtx *svc.ServiceContext)
}
func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalLogListRequest) (resp *types.QueryWithdrawalLogListResponse, err error) {
// todo: add your logic here and delete this line
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
l.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
return
page := req.Page
size := req.Size
if page <= 0 {
page = 1
}
if size <= 0 {
size = 10
}
query := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Withdrawal{}).Where("user_id = ?", u.Id)
var total int64
if err = query.Count(&total).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count withdrawal logs failed: %v", err)
}
var rows []user.Withdrawal
if err = query.Order("id DESC").Limit(size).Offset((page - 1) * size).Find(&rows).Error; err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query withdrawal logs failed: %v", err)
}
list := make([]types.WithdrawalLog, 0, len(rows))
for _, row := range rows {
list = append(list, types.WithdrawalLog{
Id: row.Id,
UserId: row.UserId,
Amount: row.Amount,
Content: row.Content,
Status: row.Status,
Reason: row.Reason,
CreatedAt: row.CreatedAt.UnixMilli(),
UpdatedAt: row.UpdatedAt.UnixMilli(),
})
}
return &types.QueryWithdrawalLogListResponse{
List: list,
Total: total,
}, nil
}
+1
View File
@@ -49,6 +49,7 @@ const (
CommissionTypeWithdraw uint16 = 334 // withdraw
CommissionTypeAdjust uint16 = 335 // Admin Adjust
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
CommissionTypeWithdrawReject uint16 = 337 // Withdraw rejected refund
GiftTypeIncrease uint16 = 341 // Increase
GiftTypeReduce uint16 = 342 // Reduce
)
+19 -19
View File
@@ -3,25 +3,25 @@ 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"`
Id int64 `gorm:"primaryKey;AUTO_INCREMENT"`
Platform string `gorm:"type:varchar(32);not null"`
AppVersion string `gorm:"type:varchar(64);default:null"`
OsName string `gorm:"type:varchar(64);default:null"`
OsVersion string `gorm:"type:varchar(64);default:null"`
DeviceId string `gorm:"type:varchar(255);default:null"`
UserId *int64 `gorm:"type:bigint;default:null"`
SessionId string `gorm:"type:varchar(255);default:null"`
Level uint8 `gorm:"type:tinyint(1);not null;default:3"`
ErrorCode string `gorm:"type:varchar(128);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" }
+17
View File
@@ -26,6 +26,7 @@ type (
Insert(ctx context.Context, data *User, tx ...*gorm.DB) error
FindOne(ctx context.Context, id int64) (*User, error)
Update(ctx context.Context, data *User, tx ...*gorm.DB) error
UpdateCommission(ctx context.Context, userId int64, delta int64, tx ...*gorm.DB) error
Delete(ctx context.Context, id int64, tx ...*gorm.DB) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
}
@@ -111,6 +112,22 @@ func (m *defaultUserModel) Update(ctx context.Context, data *User, tx ...*gorm.D
return err
}
func (m *defaultUserModel) UpdateCommission(ctx context.Context, userId int64, delta int64, tx ...*gorm.DB) error {
old, err := m.FindOne(ctx, userId)
if err != nil {
return err
}
return m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Model(&User{}).
Where("id = ?", userId).
UpdateColumn("commission", gorm.Expr("commission + ?", delta)).Error
}, m.getCacheKeys(old)...)
}
func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error {
data, err := m.FindOne(ctx, id)
if err != nil {
+18
View File
@@ -8,6 +8,22 @@ import (
"gorm.io/gorm"
)
const userDeviceUserAgentMaxLength = 255
func normalizeDeviceForStorage(data *Device) {
if data == nil {
return
}
data.UserAgent = truncateForColumn(data.UserAgent, userDeviceUserAgentMaxLength)
}
func truncateForColumn(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max]
}
func (m *customUserModel) FindOneDevice(ctx context.Context, id int64) (*Device, error) {
deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, id)
var resp Device
@@ -69,6 +85,7 @@ func (m *customUserModel) QueryDeviceListByUserIds(ctx context.Context, userIds
}
func (m *customUserModel) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error {
normalizeDeviceForStorage(data)
old, err := m.FindOneDevice(ctx, data.Id)
if err != nil {
return err
@@ -100,6 +117,7 @@ func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gor
}
func (m *customUserModel) InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error {
normalizeDeviceForStorage(data)
defer func() {
if clearErr := m.ClearDeviceCache(ctx, data); clearErr != nil {
// log cache clear error
+21
View File
@@ -2284,6 +2284,27 @@ type QueryWithdrawalLogListResponse struct {
Total int64 `json:"total"`
}
type GetWithdrawalListRequest struct {
Page int `form:"page"`
Size int `form:"size"`
UserId *int64 `form:"user_id,omitempty"`
Status *uint8 `form:"status,omitempty"`
}
type GetWithdrawalListResponse struct {
List []WithdrawalLog `json:"list"`
Total int64 `json:"total"`
}
type ApproveWithdrawalRequest struct {
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
}
type RejectWithdrawalRequest struct {
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
Reason string `json:"reason" validate:"required,max=500"`
}
type QuotaTask struct {
Id int64 `json:"id"`
Subscribers []int64 `json:"subscribers"`