From f5ad26b943132708c62c09e31c0c068285aa6d36 Mon Sep 17 00:00:00 2001 From: shanshanzhong147 Date: Wed, 10 Jun 2026 00:19:12 -0700 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD(#18):=20=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8F=90=E7=8E=B0=E8=AE=B0=E5=BD=95=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E6=8F=90=E7=8E=B0=E5=92=8C=E9=80=80=E4=BD=A3?= =?UTF-8?q?=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- apis/public/user.api | 6 +- .../public/user/queryWithdrawalLogLogic.go | 74 +++++++- .../user/queryWithdrawalLogLogic_test.go | 179 ++++++++++++++++++ internal/types/types.go | 6 +- user.json | 21 ++ 5 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 internal/logic/public/user/queryWithdrawalLogLogic_test.go diff --git a/apis/public/user.api b/apis/public/user.api index 9ac6501..9ea237f 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -117,6 +117,7 @@ type ( } WithdrawalLog { Id int64 `json:"id"` + BizType string `json:"biz_type"` UserId int64 `json:"user_id"` Amount int64 `json:"amount"` Content string `json:"content"` @@ -132,8 +133,9 @@ type ( WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"` } QueryWithdrawalLogListRequest { - Page int `form:"page"` - Size int `form:"size"` + Page int `form:"page"` + Size int `form:"size"` + BizType string `form:"biz_type" validate:"omitempty,oneof=withdrawal commission_refund"` } QueryWithdrawalLogListResponse { List []WithdrawalLog `json:"list"` diff --git a/internal/logic/public/user/queryWithdrawalLogLogic.go b/internal/logic/public/user/queryWithdrawalLogLogic.go index 9219f0f..8e7dc17 100644 --- a/internal/logic/public/user/queryWithdrawalLogLogic.go +++ b/internal/logic/public/user/queryWithdrawalLogLogic.go @@ -3,6 +3,7 @@ package user import ( "context" + "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" @@ -12,6 +13,11 @@ import ( "github.com/pkg/errors" ) +const ( + withdrawalLogBizTypeWithdrawal = "withdrawal" + withdrawalLogBizTypeCommissionRefund = "commission_refund" +) + type QueryWithdrawalLogLogic struct { logger.Logger ctx context.Context @@ -43,15 +49,26 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL size = 10 } - query := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Withdrawal{}).Where("user_id = ?", u.Id) + switch req.BizType { + case "", withdrawalLogBizTypeWithdrawal: + return l.queryWithdrawalLogs(u.Id, page, size) + case withdrawalLogBizTypeCommissionRefund: + return l.queryCommissionRefundLogs(u.Id, page, size) + default: + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "invalid biz_type: %s", req.BizType) + } +} + +func (l *QueryWithdrawalLogLogic) queryWithdrawalLogs(userID int64, page, size int) (*types.QueryWithdrawalLogListResponse, error) { + query := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Withdrawal{}).Where("user_id = ?", userID) var total int64 - if err = query.Count(&total).Error; err != nil { + 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 { + 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) } @@ -59,6 +76,7 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL for _, row := range rows { list = append(list, types.WithdrawalLog{ Id: row.Id, + BizType: withdrawalLogBizTypeWithdrawal, UserId: row.UserId, Amount: row.Amount, Content: row.Content, @@ -77,3 +95,53 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL Total: total, }, nil } + +func (l *QueryWithdrawalLogLogic) queryCommissionRefundLogs(userID int64, page, size int) (*types.QueryWithdrawalLogListResponse, error) { + query := l.svcCtx.DB.WithContext(l.ctx). + Model(&log.SystemLog{}). + Where("`type` = ? AND object_id = ? AND `content` LIKE ?", log.TypeCommission.Uint8(), userID, "%\"type\":333%") + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count commission refund logs failed: %v", err) + } + + var rows []log.SystemLog + 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 commission refund logs failed: %v", err) + } + + list := make([]types.WithdrawalLog, 0, len(rows)) + for _, row := range rows { + var content log.Commission + if err := content.Unmarshal([]byte(row.Content)); err != nil { + l.Errorw("unmarshal commission refund log content failed", + logger.Field("log_id", row.Id), + logger.Field("error", err.Error()), + ) + continue + } + if content.Type != log.CommissionTypeRefund { + continue + } + + timestamp := content.Timestamp + if timestamp == 0 { + timestamp = row.CreatedAt.UnixMilli() + } + list = append(list, types.WithdrawalLog{ + Id: row.Id, + BizType: withdrawalLogBizTypeCommissionRefund, + UserId: row.ObjectID, + Amount: content.Amount, + Content: row.Content, + CreatedAt: timestamp, + UpdatedAt: timestamp, + }) + } + + return &types.QueryWithdrawalLogListResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/public/user/queryWithdrawalLogLogic_test.go b/internal/logic/public/user/queryWithdrawalLogLogic_test.go new file mode 100644 index 0000000..9cc13a2 --- /dev/null +++ b/internal/logic/public/user/queryWithdrawalLogLogic_test.go @@ -0,0 +1,179 @@ +package user + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + logmodel "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/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" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func TestQueryWithdrawalLog_WithWithdrawalBizType(t *testing.T) { + const userID = int64(42) + createdAt := time.Unix(1700000000, 0) + updatedAt := createdAt.Add(time.Minute) + + db, mock, cleanup := newQueryWithdrawalLogTestDB(t) + defer cleanup() + + mock.ExpectQuery("SELECT count(*) FROM `withdrawals` WHERE user_id = ?"). + WithArgs(userID). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery("SELECT * FROM `withdrawals` WHERE user_id = ? ORDER BY id DESC LIMIT ?"). + WithArgs(userID, 10). + WillReturnRows(sqlmock.NewRows([]string{ + "id", "user_id", "amount", "content", "status", "reason", "method", "account", "qr_code_url", "created_at", "updated_at", + }).AddRow( + int64(1001), userID, int64(3000), "bank withdrawal", usermodel.WithdrawalStatusPending, "", uint8(3), "acct-001", "", createdAt, updatedAt, + )) + + logic := newTestQueryWithdrawalLogLogic(t, db, userID) + resp, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{ + BizType: withdrawalLogBizTypeWithdrawal, + Page: 1, + Size: 10, + }) + if err != nil { + t.Fatalf("QueryWithdrawalLog unexpected error: %v", err) + } + if resp.Total != 1 || len(resp.List) != 1 { + t.Fatalf("QueryWithdrawalLog response = %+v, want one withdrawal", resp) + } + got := resp.List[0] + if got.BizType != withdrawalLogBizTypeWithdrawal { + t.Fatalf("BizType = %q, want %q", got.BizType, withdrawalLogBizTypeWithdrawal) + } + if got.Id != 1001 || got.UserId != userID || got.Amount != 3000 || got.CreatedAt != createdAt.UnixMilli() || got.UpdatedAt != updatedAt.UnixMilli() { + t.Fatalf("withdrawal item = %+v", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestQueryWithdrawalLog_WithCommissionRefundBizType(t *testing.T) { + const userID = int64(42) + content := `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000000123}` + createdAt := time.Unix(1700000000, 0) + + db, mock, cleanup := newQueryWithdrawalLogTestDB(t) + defer cleanup() + + mock.ExpectQuery("SELECT count(*) FROM `system_logs` WHERE `type` = ? AND object_id = ? AND `content` LIKE ?"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND `content` LIKE ? ORDER BY id DESC LIMIT ?"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}). + AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, content, createdAt)) + + logic := newTestQueryWithdrawalLogLogic(t, db, userID) + resp, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{ + BizType: withdrawalLogBizTypeCommissionRefund, + Page: 1, + Size: 10, + }) + if err != nil { + t.Fatalf("QueryWithdrawalLog unexpected error: %v", err) + } + if resp.Total != 1 || len(resp.List) != 1 { + t.Fatalf("QueryWithdrawalLog response = %+v, want one commission refund", resp) + } + got := resp.List[0] + if got.BizType != withdrawalLogBizTypeCommissionRefund { + t.Fatalf("BizType = %q, want %q", got.BizType, withdrawalLogBizTypeCommissionRefund) + } + if got.Id != 2001 || got.UserId != userID || got.Amount != 2500 || got.Content != content || got.CreatedAt != 1700000000123 || got.UpdatedAt != 1700000000123 { + t.Fatalf("commission refund item = %+v", got) + } + if got.Status != 0 || got.Method != 0 || got.Account != "" || got.QrCodeUrl != "" { + t.Fatalf("commission refund withdrawal-only fields = %+v, want zero values", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestQueryWithdrawalLog_RejectsInvalidBizType(t *testing.T) { + db, mock, cleanup := newQueryWithdrawalLogTestDB(t) + defer cleanup() + + logic := newTestQueryWithdrawalLogLogic(t, db, 42) + _, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{ + BizType: "all", + Page: 1, + Size: 10, + }) + if !isQueryWithdrawalLogErrCode(err, xerr.InvalidParams) { + t.Fatalf("QueryWithdrawalLog err = %v, want InvalidParams", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func newQueryWithdrawalLogTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) { + t.Helper() + + sqlDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherFunc(func(expectedSQL, actualSQL string) error { + if strings.Contains(actualSQL, expectedSQL) { + return nil + } + return fmt.Errorf("actual sql %q does not contain %q", actualSQL, expectedSQL) + }))) + if err != nil { + t.Fatalf("create sqlmock: %v", err) + } + + db, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{}) + if err != nil { + _ = sqlDB.Close() + t.Fatalf("open gorm db: %v", err) + } + + return db, mock, func() { + _ = sqlDB.Close() + } +} + +func newTestQueryWithdrawalLogLogic(t *testing.T, db *gorm.DB, userID int64) *QueryWithdrawalLogLogic { + t.Helper() + ctx := context.WithValue(context.Background(), constant.CtxKeyUser, &usermodel.User{Id: userID}) + return &QueryWithdrawalLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: &svc.ServiceContext{ + DB: db, + }, + } +} + +func queryWithdrawalLogErrCodeOf(err error) uint32 { + if err == nil { + return 0 + } + type coder interface { + GetErrCode() uint32 + } + cause := errors.Cause(err) + if c, ok := cause.(coder); ok { + return c.GetErrCode() + } + return 0 +} + +func isQueryWithdrawalLogErrCode(err error, code uint32) bool { + return queryWithdrawalLogErrCodeOf(err) == code +} diff --git a/internal/types/types.go b/internal/types/types.go index e865fc2..9215fca 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2540,8 +2540,9 @@ type QueryUserSubscribeNodeListResponse struct { } type QueryWithdrawalLogListRequest struct { - Page int `form:"page"` - Size int `form:"size"` + Page int `form:"page"` + Size int `form:"size"` + BizType string `form:"biz_type" validate:"omitempty,oneof=withdrawal commission_refund"` } type QueryWithdrawalLogListResponse struct { @@ -3885,6 +3886,7 @@ type WeeklyStat struct { type WithdrawalLog struct { Id int64 `json:"id"` + BizType string `json:"biz_type"` UserId int64 `json:"user_id"` Amount int64 `json:"amount"` Content string `json:"content"` diff --git a/user.json b/user.json index d59d355..0148114 100644 --- a/user.json +++ b/user.json @@ -1678,6 +1678,16 @@ "required": true, "type": "integer", "format": "int32" + }, + { + "name": "biz_type", + "in": "query", + "required": false, + "type": "string", + "enum": [ + "withdrawal", + "commission_refund" + ] } ], "tags": [ @@ -5121,6 +5131,13 @@ "size": { "type": "integer", "format": "int32" + }, + "biz_type": { + "type": "string", + "enum": [ + "withdrawal", + "commission_refund" + ] } }, "title": "QueryWithdrawalLogListRequest", @@ -7168,11 +7185,15 @@ "updated_at": { "type": "integer", "format": "int64" + }, + "biz_type": { + "type": "string" } }, "title": "WithdrawalLog", "required": [ "id", + "biz_type", "user_id", "amount", "content",