From fd522b6c71aae0cd1e1a334dfc3b3a797fe6fcb1 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Tue, 19 May 2026 19:35:44 -0700 Subject: [PATCH] path --- apis/admin/user.api | 30 ++- .../ap-east-1/configs/replica-ops.env.example | 2 +- .../migrate/database/00001_init_schema.up.sql | 2 +- .../02148_log_message_column_sizes.down.sql | 7 + .../02148_log_message_column_sizes.up.sql | 7 + ...149_user_device_user_agent_length.down.sql | 2 + ...02149_user_device_user_agent_length.up.sql | 2 + initialize/schema_compat.go | 72 +++++++ .../admin/user/approveWithdrawalHandler.go | 26 +++ .../admin/user/getWithdrawalListHandler.go | 26 +++ .../admin/user/rejectWithdrawalHandler.go | 26 +++ internal/handler/routes.go | 9 + .../admin/user/approveWithdrawalLogic.go | 27 +++ .../admin/user/getWithdrawalListLogic.go | 74 ++++++++ .../logic/admin/user/rejectWithdrawalLogic.go | 27 +++ .../admin/user/updateUserBasicInfoLogic.go | 23 +-- internal/logic/admin/user/withdrawalCommon.go | 81 ++++++++ .../logic/common/logMessageReportLogic.go | 176 ++++++++++-------- internal/logic/common/withdrawal.go | 48 +++++ internal/logic/public/file/common.go | 8 +- .../public/user/commissionWithdrawLogic.go | 38 ++-- .../public/user/queryWithdrawalLogLogic.go | 50 ++++- internal/model/log/log.go | 1 + internal/model/logmessage/entity.go | 38 ++-- internal/model/user/default.go | 17 ++ internal/model/user/device.go | 18 ++ internal/types/types.go | 21 +++ ops/aws-rds-external-replica-runbook.md | 8 +- ops/hifast-aws-standby-architecture-zh.md | 18 +- pkg/xerr/errCode.go | 1 + pkg/xerr/errMsg.go | 1 + 31 files changed, 723 insertions(+), 163 deletions(-) create mode 100644 initialize/migrate/database/02148_log_message_column_sizes.down.sql create mode 100644 initialize/migrate/database/02148_log_message_column_sizes.up.sql create mode 100644 initialize/migrate/database/02149_user_device_user_agent_length.down.sql create mode 100644 initialize/migrate/database/02149_user_device_user_agent_length.up.sql create mode 100644 internal/handler/admin/user/approveWithdrawalHandler.go create mode 100644 internal/handler/admin/user/getWithdrawalListHandler.go create mode 100644 internal/handler/admin/user/rejectWithdrawalHandler.go create mode 100644 internal/logic/admin/user/approveWithdrawalLogic.go create mode 100644 internal/logic/admin/user/getWithdrawalListLogic.go create mode 100644 internal/logic/admin/user/rejectWithdrawalLogic.go create mode 100644 internal/logic/admin/user/withdrawalCommon.go create mode 100644 internal/logic/common/withdrawal.go diff --git a/apis/admin/user.api b/apis/admin/user.api index 6965d40..aa6243f 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -230,6 +230,23 @@ type ( FamilyId int64 `json:"family_id" validate:"required,gt=0"` Reason string `json:"reason,omitempty"` } + GetWithdrawalListRequest { + Page int `form:"page"` + Size int `form:"size"` + UserId *int64 `form:"user_id,omitempty"` + Status *uint8 `form:"status,omitempty"` + } + GetWithdrawalListResponse { + List []WithdrawalLog `json:"list"` + Total int64 `json:"total"` + } + ApproveWithdrawalRequest { + WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"` + } + RejectWithdrawalRequest { + WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"` + Reason string `json:"reason" validate:"required,max=500"` + } ) @server ( @@ -370,5 +387,16 @@ service ppanel { @doc "Dissolve family" @handler DissolveFamily put /family/dissolve (DissolveFamilyRequest) -} + @doc "Get withdrawal list" + @handler GetWithdrawalList + get /withdrawal/list (GetWithdrawalListRequest) returns (GetWithdrawalListResponse) + + @doc "Approve withdrawal" + @handler ApproveWithdrawal + post /withdrawal/approve (ApproveWithdrawalRequest) + + @doc "Reject withdrawal" + @handler RejectWithdrawal + post /withdrawal/reject (RejectWithdrawalRequest) +} diff --git a/deploy/aws/ap-east-1/configs/replica-ops.env.example b/deploy/aws/ap-east-1/configs/replica-ops.env.example index 5c86942..f6d7e59 100644 --- a/deploy/aws/ap-east-1/configs/replica-ops.env.example +++ b/deploy/aws/ap-east-1/configs/replica-ops.env.example @@ -17,7 +17,7 @@ REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD=CHANGE_ME -REDIS_SOURCE_HOST=43.198.248.161 +REDIS_SOURCE_HOST=18.163.33.75 REDIS_SOURCE_PORT=6379 REDIS_SOURCE_USER= REDIS_SOURCE_PASSWORD=CHANGE_ME diff --git a/initialize/migrate/database/00001_init_schema.up.sql b/initialize/migrate/database/00001_init_schema.up.sql index ab5d0e4..490cd5e 100644 --- a/initialize/migrate/database/00001_init_schema.up.sql +++ b/initialize/migrate/database/00001_init_schema.up.sql @@ -449,7 +449,7 @@ CREATE TABLE IF NOT EXISTS `user_device` `subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID', `ip` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.', `Identifier` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.', - `user_agent` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.', + `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.', `online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online', `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', diff --git a/initialize/migrate/database/02148_log_message_column_sizes.down.sql b/initialize/migrate/database/02148_log_message_column_sizes.down.sql new file mode 100644 index 0000000..9534d8d --- /dev/null +++ b/initialize/migrate/database/02148_log_message_column_sizes.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE `log_message` + MODIFY COLUMN `app_version` VARCHAR(32) NULL, + MODIFY COLUMN `os_name` VARCHAR(32) NULL, + MODIFY COLUMN `os_version` VARCHAR(32) NULL, + MODIFY COLUMN `device_id` VARCHAR(64) NULL, + MODIFY COLUMN `session_id` VARCHAR(64) NULL, + MODIFY COLUMN `error_code` VARCHAR(64) NULL; diff --git a/initialize/migrate/database/02148_log_message_column_sizes.up.sql b/initialize/migrate/database/02148_log_message_column_sizes.up.sql new file mode 100644 index 0000000..c80b982 --- /dev/null +++ b/initialize/migrate/database/02148_log_message_column_sizes.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `log_message` + MODIFY COLUMN `app_version` VARCHAR(64) NULL, + MODIFY COLUMN `os_name` VARCHAR(64) NULL, + MODIFY COLUMN `os_version` VARCHAR(64) NULL, + MODIFY COLUMN `device_id` VARCHAR(255) NULL, + MODIFY COLUMN `session_id` VARCHAR(255) NULL, + MODIFY COLUMN `error_code` VARCHAR(128) NULL; diff --git a/initialize/migrate/database/02149_user_device_user_agent_length.down.sql b/initialize/migrate/database/02149_user_device_user_agent_length.down.sql new file mode 100644 index 0000000..0ba4edd --- /dev/null +++ b/initialize/migrate/database/02149_user_device_user_agent_length.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user_device` + MODIFY COLUMN `user_agent` VARCHAR(64) NULL COMMENT 'Device User Agent.'; diff --git a/initialize/migrate/database/02149_user_device_user_agent_length.up.sql b/initialize/migrate/database/02149_user_device_user_agent_length.up.sql new file mode 100644 index 0000000..e1057dc --- /dev/null +++ b/initialize/migrate/database/02149_user_device_user_agent_length.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user_device` + MODIFY COLUMN `user_agent` VARCHAR(255) NULL COMMENT 'Device User Agent.'; diff --git a/initialize/schema_compat.go b/initialize/schema_compat.go index ee908b2..4fca11a 100644 --- a/initialize/schema_compat.go +++ b/initialize/schema_compat.go @@ -20,6 +20,14 @@ type schemaColumnPatch struct { ddl string } +type schemaColumnDefinitionPatch struct { + table string + column string + dataType string + characterMaxLen *int64 + ddl string +} + func EnsureSchemaCompatibility(ctx *svc.ServiceContext) error { tablePatches := []schemaTablePatch{ { @@ -142,6 +150,17 @@ func EnsureSchemaCompatibility(ctx *svc.ServiceContext) error { }, } + varchar255 := int64(255) + columnDefinitionPatches := []schemaColumnDefinitionPatch{ + { + table: "user_device", + column: "user_agent", + dataType: "varchar", + characterMaxLen: &varchar255, + ddl: "ALTER TABLE `user_device` MODIFY COLUMN `user_agent` VARCHAR(255) NULL COMMENT 'Device User Agent.';", + }, + } + for _, patch := range tablePatches { exists, err := tableExists(ctx.DB, patch.table) if err != nil { @@ -199,6 +218,27 @@ func EnsureSchemaCompatibility(ctx *svc.ServiceContext) error { logger.Infof("[SchemaCompat] created missing index: %s.%s", patch.table, patch.index) } + for _, patch := range columnDefinitionPatches { + tblExists, err := tableExists(ctx.DB, patch.table) + if err != nil { + return errors.Wrapf(err, "check table %s failed", patch.table) + } + if !tblExists { + continue + } + matches, err := columnDefinitionMatches(ctx.DB, patch.table, patch.column, patch.dataType, patch.characterMaxLen) + if err != nil { + return errors.Wrapf(err, "check column definition %s.%s failed", patch.table, patch.column) + } + if matches { + continue + } + if err = ctx.DB.Exec(patch.ddl).Error; err != nil { + return errors.Wrapf(err, "modify column %s.%s failed", patch.table, patch.column) + } + logger.Infof("[SchemaCompat] repaired column definition: %s.%s", patch.table, patch.column) + } + return nil } @@ -237,6 +277,38 @@ func indexExists(db *gorm.DB, table, index string) (bool, error) { return count > 0, nil } +func columnDefinitionMatches(db *gorm.DB, table, column, dataType string, characterMaxLen *int64) (bool, error) { + type columnMeta struct { + DataType string + CharacterMaximumLen *int64 + } + + var meta columnMeta + err := db.Raw( + `SELECT DATA_TYPE AS data_type, CHARACTER_MAXIMUM_LENGTH AS character_maximum_len +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`, + table, + column, + ).Scan(&meta).Error + if err != nil { + return false, err + } + if meta.DataType == "" { + return false, nil + } + if meta.DataType != dataType { + return false, nil + } + if characterMaxLen == nil { + return true, nil + } + if meta.CharacterMaximumLen == nil { + return false, nil + } + return *meta.CharacterMaximumLen == *characterMaxLen, nil +} + func _schemaCompatDebug(table, column string) string { if column == "" { return table diff --git a/internal/handler/admin/user/approveWithdrawalHandler.go b/internal/handler/admin/user/approveWithdrawalHandler.go new file mode 100644 index 0000000..c0d76ec --- /dev/null +++ b/internal/handler/admin/user/approveWithdrawalHandler.go @@ -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) + } +} diff --git a/internal/handler/admin/user/getWithdrawalListHandler.go b/internal/handler/admin/user/getWithdrawalListHandler.go new file mode 100644 index 0000000..a440c73 --- /dev/null +++ b/internal/handler/admin/user/getWithdrawalListHandler.go @@ -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) + } +} diff --git a/internal/handler/admin/user/rejectWithdrawalHandler.go b/internal/handler/admin/user/rejectWithdrawalHandler.go new file mode 100644 index 0000000..9fac60c --- /dev/null +++ b/internal/handler/admin/user/rejectWithdrawalHandler.go @@ -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) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 0b35e68..5e387ea 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -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") diff --git a/internal/logic/admin/user/approveWithdrawalLogic.go b/internal/logic/admin/user/approveWithdrawalLogic.go new file mode 100644 index 0000000..694d150 --- /dev/null +++ b/internal/logic/admin/user/approveWithdrawalLogic.go @@ -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) +} diff --git a/internal/logic/admin/user/getWithdrawalListLogic.go b/internal/logic/admin/user/getWithdrawalListLogic.go new file mode 100644 index 0000000..ff262af --- /dev/null +++ b/internal/logic/admin/user/getWithdrawalListLogic.go @@ -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 +} diff --git a/internal/logic/admin/user/rejectWithdrawalLogic.go b/internal/logic/admin/user/rejectWithdrawalLogic.go new file mode 100644 index 0000000..6d8f02a --- /dev/null +++ b/internal/logic/admin/user/rejectWithdrawalLogic.go @@ -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) +} diff --git a/internal/logic/admin/user/updateUserBasicInfoLogic.go b/internal/logic/admin/user/updateUserBasicInfoLogic.go index 6a374c2..fcefa92 100644 --- a/internal/logic/admin/user/updateUserBasicInfoLogic.go +++ b/internal/logic/admin/user/updateUserBasicInfoLogic.go @@ -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 diff --git a/internal/logic/admin/user/withdrawalCommon.go b/internal/logic/admin/user/withdrawalCommon.go new file mode 100644 index 0000000..2cb8578 --- /dev/null +++ b/internal/logic/admin/user/withdrawalCommon.go @@ -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)) +} diff --git a/internal/logic/common/logMessageReportLogic.go b/internal/logic/common/logMessageReportLogic.go index 2427c18..c854d33 100644 --- a/internal/logic/common/logMessageReportLogic.go +++ b/internal/logic/common/logMessageReportLogic.go @@ -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 "" } diff --git a/internal/logic/common/withdrawal.go b/internal/logic/common/withdrawal.go new file mode 100644 index 0000000..6a8cbe4 --- /dev/null +++ b/internal/logic/common/withdrawal.go @@ -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 +} diff --git a/internal/logic/public/file/common.go b/internal/logic/public/file/common.go index c7e51da..cd1e909 100644 --- a/internal/logic/public/file/common.go +++ b/internal/logic/public/file/common.go @@ -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, ) } diff --git a/internal/logic/public/user/commissionWithdrawLogic.go b/internal/logic/public/user/commissionWithdrawLogic.go index d16dec0..bbe9541 100644 --- a/internal/logic/public/user/commissionWithdrawLogic.go +++ b/internal/logic/public/user/commissionWithdrawLogic.go @@ -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 } diff --git a/internal/logic/public/user/queryWithdrawalLogLogic.go b/internal/logic/public/user/queryWithdrawalLogLogic.go index 1b1a583..79e8a2c 100644 --- a/internal/logic/public/user/queryWithdrawalLogLogic.go +++ b/internal/logic/public/user/queryWithdrawalLogLogic.go @@ -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 } diff --git a/internal/model/log/log.go b/internal/model/log/log.go index 1afd13d..c970d91 100644 --- a/internal/model/log/log.go +++ b/internal/model/log/log.go @@ -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 ) diff --git a/internal/model/logmessage/entity.go b/internal/model/logmessage/entity.go index 233e3d9..1607e6c 100644 --- a/internal/model/logmessage/entity.go +++ b/internal/model/logmessage/entity.go @@ -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" } diff --git a/internal/model/user/default.go b/internal/model/user/default.go index 8eeb399..e98ffde 100644 --- a/internal/model/user/default.go +++ b/internal/model/user/default.go @@ -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 { diff --git a/internal/model/user/device.go b/internal/model/user/device.go index 3ad06aa..2d4eb11 100644 --- a/internal/model/user/device.go +++ b/internal/model/user/device.go @@ -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 diff --git a/internal/types/types.go b/internal/types/types.go index aeccc2a..385aec1 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -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"` diff --git a/ops/aws-rds-external-replica-runbook.md b/ops/aws-rds-external-replica-runbook.md index c1ca82e..d10b530 100644 --- a/ops/aws-rds-external-replica-runbook.md +++ b/ops/aws-rds-external-replica-runbook.md @@ -22,7 +22,7 @@ - Region: `ap-east-1` - AWS app EC2: - Name: `hifast-hk-app-01` - - Public IP: `43.198.248.161` + - Public IP: `18.163.33.75` - Private IP: `10.0.1.201` - AWS MySQL: - Type: `RDS MySQL` @@ -183,7 +183,7 @@ redis-cli INFO replication 重点看: - `role:slave` -- `master_host:43.198.248.161` +- `master_host:18.163.33.75` - `master_port:6379` - `master_link_status:up` @@ -416,7 +416,7 @@ SHOW REPLICA STATUS\G ```bash redis-cli CONFIG SET masterauth '0BVz9XOHf7KUfEuoFJRK-dURdKUGFiZ8QeaHpysHnKeKhLskZb55HPK121lFsKtr' -redis-cli REPLICAOF 43.198.248.161 6379 +redis-cli REPLICAOF 18.163.33.75 6379 redis-cli CONFIG SET replica-read-only yes redis-cli INFO replication ``` @@ -426,7 +426,7 @@ redis-cli INFO replication 检查 `/etc/redis/redis.conf` 至少包含: ```conf -replicaof 43.198.248.161 6379 +replicaof 18.163.33.75 6379 masterauth 0BVz9XOHf7KUfEuoFJRK-dURdKUGFiZ8QeaHpysHnKeKhLskZb55HPK121lFsKtr replica-read-only yes ``` diff --git a/ops/hifast-aws-standby-architecture-zh.md b/ops/hifast-aws-standby-architecture-zh.md index dff3b8d..2cea7ad 100644 --- a/ops/hifast-aws-standby-architecture-zh.md +++ b/ops/hifast-aws-standby-architecture-zh.md @@ -66,7 +66,7 @@ - Instance ID: `i-079cd9d3ef3748714` - 角色:当前实际生产入口 / Nginx / 业务服务 / AWS 侧 Redis 主库宿主机 - 私网 IP: `10.0.1.201` -- 公网 IP: `43.198.248.161` +- 公网 IP: `18.163.33.75` - 业务服务运行方式:`Docker Compose` - 业务容器:`ppanel-server` - 部署目录:`/opt/ppanel` @@ -103,7 +103,7 @@ - 容器名:`hifast-redis` - 版本:`redis:8.2.1` - 访问端口:`6379` -- 主库出口地址:`43.198.248.161:6379` +- 主库出口地址:`18.163.33.75:6379` - 应用当前实际连接:`127.0.0.1:6379` - 认证方式:已启用密码认证 @@ -130,10 +130,10 @@ ```mermaid flowchart TB USER["用户 / 客户端"] --> DNS["域名 / DNS / 入口层"] - DNS --> APP["AWS EC2\nhifast-hk-app-01\n43.198.248.161\n10.0.1.201"] + DNS --> APP["AWS EC2\nhifast-hk-app-01\n18.163.33.75\n10.0.1.201"] APP --> RDS["AWS RDS MySQL\nhifast-mysql-prod-v2\n主库"] - APP --> REDISM["AWS Redis 主库\nDocker redis:8.2.1\n43.198.248.161:6379"] + APP --> REDISM["AWS Redis 主库\nDocker redis:8.2.1\n18.163.33.75:6379"] RDS -. MySQL 备用 / 同步 .-> MYSQLS["104.238.220.230\nMySQL 备用库"] REDISM -. Redis 主从复制 .-> REDISS["104.238.220.230\n原生 Redis 8.6.3\n从库"] @@ -238,7 +238,7 @@ App / Nginx ```mermaid flowchart LR - SG["hifast-hk-app-core-sg"] --> REDIS["AWS Redis 主库\n43.198.248.161:6379"] + SG["hifast-hk-app-core-sg"] --> REDIS["AWS Redis 主库\n18.163.33.75:6379"] STANDBY["104.238.220.230/32"] --> SG ``` @@ -311,7 +311,7 @@ RDS 当前状态已经比之前干净很多: `104.238.220.230` 连接 AWS Redis,不是通过 PEM 证书,也不是通过 SSH 登录 AWS 机器,而是直接作为 Redis 从库去访问 AWS Redis 主库: -- 目标地址:`43.198.248.161:6379` +- 目标地址:`18.163.33.75:6379` - 连接方式:`TCP` - 认证方式:`Redis 密码` - 网络前提:AWS EC2 安全组已放行 `104.238.220.230/32 -> 6379` @@ -325,7 +325,7 @@ RDS 当前状态已经比之前干净很多: 示意命令: ```bash -redis-cli -h 43.198.248.161 -p 6379 -a '' +redis-cli -h 18.163.33.75 -p 6379 -a '' ``` ### 4.6.2 104 连接 AWS MySQL RDS 的方式 @@ -380,7 +380,7 @@ mysql -h hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com -u admin - 部署方式:Docker - 版本:`8.2.1` -- 主库地址:`43.198.248.161:6379` +- 主库地址:`18.163.33.75:6379` - 运行容器:`hifast-redis` ### 5.2 104 Redis 从库 @@ -417,7 +417,7 @@ mysql -h hifast-mysql-prod-v2.cd6aey40m6ag.ap-east-1.rds.amazonaws.com -u admin ```mermaid flowchart LR - REDISMASTER["AWS Redis 主库\n43.198.248.161:6379\nDocker redis:8.2.1"] + REDISMASTER["AWS Redis 主库\n18.163.33.75:6379\nDocker redis:8.2.1"] REDISSLAVE["104.238.220.230\n原生 Redis 8.6.3\nrole: slave"] REDISMASTER --> REDISSLAVE ``` diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index e334a71..9313241 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -37,6 +37,7 @@ const ( FamilyNotExist uint32 = 20017 FamilyStatusInvalid uint32 = 20018 FamilyOwnerOperationForbidden uint32 = 20019 + WithdrawalStatusInvalid uint32 = 20020 ) // Node error diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index 3889559..4ec5ae2 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -46,6 +46,7 @@ func init() { FamilyNotExist: "家庭组不存在", FamilyStatusInvalid: "家庭组状态无效", FamilyOwnerOperationForbidden: "家庭组所有者不允许此操作", + WithdrawalStatusInvalid: "提现状态无效", // Node error NodeExist: "Node already exists",