diff --git a/apis/public/user.api b/apis/public/user.api index 9ea237f..350d7cf 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -141,6 +141,23 @@ type ( List []WithdrawalLog `json:"list"` Total int64 `json:"total"` } + QueryCommissionReturnLogRequest { + Page int `form:"page"` + Size int `form:"size"` + } + CommissionReturnLog { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + EventType uint16 `json:"event_type"` + Content string `json:"content"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + QueryCommissionReturnLogResponse { + List []CommissionReturnLog `json:"list"` + Total int64 `json:"total"` + } GetDeviceOnlineStatsResponse { WeeklyStats []WeeklyStat `json:"weekly_stats"` ConnectionRecords ConnectionRecords `json:"connection_records"` @@ -384,10 +401,14 @@ service ppanel { @handler CancelWithdrawal post /withdrawal_cancel (CancelWithdrawalRequest) returns (WithdrawalLog) - @doc "Query Withdrawal Log" + @doc "Query Withdrawal Log (biz_type=commission_refund deprecated, use /commission_return_log)" @handler QueryWithdrawalLog get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse) + @doc "Query Commission Return Log" + @handler QueryCommissionReturnLog + get /commission_return_log (QueryCommissionReturnLogRequest) returns (QueryCommissionReturnLogResponse) + @doc "Device Online Statistics" @handler DeviceOnlineStatistics get /device_online_statistics returns (GetDeviceOnlineStatsResponse) @@ -447,3 +468,4 @@ service ppanel { @handler DeviceWsConnect get /device_ws_connect } + diff --git a/internal/handler/public/user/queryCommissionReturnLogHandler.go b/internal/handler/public/user/queryCommissionReturnLogHandler.go new file mode 100644 index 0000000..501184e --- /dev/null +++ b/internal/handler/public/user/queryCommissionReturnLogHandler.go @@ -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 Commission Return Log +func QueryCommissionReturnLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryCommissionReturnLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewQueryCommissionReturnLogLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryCommissionReturnLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/queryCommissionReturnLogHandler_test.go b/internal/handler/public/user/queryCommissionReturnLogHandler_test.go new file mode 100644 index 0000000..965752d --- /dev/null +++ b/internal/handler/public/user/queryCommissionReturnLogHandler_test.go @@ -0,0 +1,132 @@ +package user + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gin-gonic/gin" + 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/pkg/constant" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func TestCommissionReturnLogHandler_HTTPResponse(t *testing.T) { + gin.SetMode(gin.TestMode) + + db, mock, cleanup := newCommissionReturnHandlerTestDB(t) + defer cleanup() + + expectCommissionReturnHTTPQueries(mock, 42, 3) + mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?"). + WithArgs(logmodel.TypeCommission.Uint8(), int64(42), "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}). + AddRow(int64(2003), logmodel.TypeCommission.Uint8(), "2023-11-14", int64(42), `{"type":338,"amount":1500,"order_no":"ORDER-3","timestamp":1700000003123}`, time.Unix(1700000000, 0)). + AddRow(int64(2002), logmodel.TypeCommission.Uint8(), "2023-11-14", int64(42), `{"type":337,"amount":2000,"order_no":"ORDER-2","timestamp":1700000002123}`, time.Unix(1700000000, 0)). + AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", int64(42), `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000001123}`, time.Unix(1700000000, 0))) + + router := gin.New() + svcCtx := &svc.ServiceContext{DB: db} + router.Use(injectTestUser(42)) + router.GET("/v1/public/user/commission_return_log", QueryCommissionReturnLogHandler(svcCtx)) + + req := httptest.NewRequest(http.MethodGet, "/v1/public/user/commission_return_log?page=1&size=10", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + + body := strings.TrimSpace(rec.Body.String()) + t.Logf("commission_return_log response: %s", body) + if !strings.Contains(body, `"event_type":338`) || !strings.Contains(body, `"event_type":337`) || !strings.Contains(body, `"event_type":333`) { + t.Fatalf("response body = %s", body) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestWithdrawalLogHandler_CommissionRefundHTTPResponse(t *testing.T) { + gin.SetMode(gin.TestMode) + + db, mock, cleanup := newCommissionReturnHandlerTestDB(t) + defer cleanup() + + expectCommissionReturnHTTPQueries(mock, 42, 3) + mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?"). + WithArgs(logmodel.TypeCommission.Uint8(), int64(42), "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}). + AddRow(int64(2003), logmodel.TypeCommission.Uint8(), "2023-11-14", int64(42), `{"type":338,"amount":1500,"order_no":"ORDER-3","timestamp":1700000003123}`, time.Unix(1700000000, 0)). + AddRow(int64(2002), logmodel.TypeCommission.Uint8(), "2023-11-14", int64(42), `{"type":337,"amount":2000,"order_no":"ORDER-2","timestamp":1700000002123}`, time.Unix(1700000000, 0)). + AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", int64(42), `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000001123}`, time.Unix(1700000000, 0))) + + router := gin.New() + svcCtx := &svc.ServiceContext{DB: db} + router.Use(injectTestUser(42)) + router.GET("/v1/public/user/withdrawal_log", QueryWithdrawalLogHandler(svcCtx)) + + req := httptest.NewRequest(http.MethodGet, "/v1/public/user/withdrawal_log?page=1&size=10&biz_type=commission_refund", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) + } + + body := strings.TrimSpace(rec.Body.String()) + t.Logf("withdrawal_log commission_refund response: %s", body) + if !strings.Contains(body, `"biz_type":"commission_refund"`) || !strings.Contains(body, `"amount":1500`) || !strings.Contains(body, `"amount":2000`) || !strings.Contains(body, `"amount":2500`) { + t.Fatalf("response body = %s", body) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func newCommissionReturnHandlerTestDB(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 injectTestUser(userID int64) gin.HandlerFunc { + return func(c *gin.Context) { + ctx := context.WithValue(c.Request.Context(), constant.CtxKeyUser, &usermodel.User{Id: userID}) + c.Request = c.Request.WithContext(ctx) + c.Next() + } +} + +func expectCommissionReturnHTTPQueries(mock sqlmock.Sqlmock, userID int64, total int64) { + mock.ExpectQuery("SELECT count(*) FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?)"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) +} diff --git a/internal/handler/public/user/queryWithdrawalLogHandler.go b/internal/handler/public/user/queryWithdrawalLogHandler.go index 9f0bddc..aca9b83 100644 --- a/internal/handler/public/user/queryWithdrawalLogHandler.go +++ b/internal/handler/public/user/queryWithdrawalLogHandler.go @@ -8,7 +8,7 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Query Withdrawal Log +// Query Withdrawal Log (biz_type=commission_refund deprecated, use /commission_return_log) func QueryWithdrawalLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.QueryWithdrawalLogListRequest diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 58f4462..9a923bf 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -1180,6 +1180,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Verify Email publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx)) + // Query Commission Return Log + publicUserGroupRouter.GET("/commission_return_log", publicUser.QueryCommissionReturnLogHandler(serverCtx)) + // Query Withdrawal Log publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx)) } diff --git a/internal/logic/public/user/queryCommissionReturnLogLogic.go b/internal/logic/public/user/queryCommissionReturnLogLogic.go new file mode 100644 index 0000000..4c46137 --- /dev/null +++ b/internal/logic/public/user/queryCommissionReturnLogLogic.go @@ -0,0 +1,143 @@ +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" + "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 commissionReturnLogRecord struct { + LogID int64 + UserID int64 + Amount int64 + EventType uint16 + Content string + CreatedAt int64 + UpdatedAt int64 +} + +type QueryCommissionReturnLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryCommissionReturnLogLogic Query Commission Return Log +func NewQueryCommissionReturnLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryCommissionReturnLogLogic { + return &QueryCommissionReturnLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryCommissionReturnLogLogic) QueryCommissionReturnLog(req *types.QueryCommissionReturnLogRequest) (*types.QueryCommissionReturnLogResponse, error) { + 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") + } + + page, size := normalizePagination(req.Page, req.Size) + list, total, err := l.queryCommissionReturnLogRecords(u.Id, page, size) + if err != nil { + return nil, err + } + + respList := make([]types.CommissionReturnLog, 0, len(list)) + for _, item := range list { + respList = append(respList, types.CommissionReturnLog{ + Id: item.LogID, + UserId: item.UserID, + Amount: item.Amount, + EventType: item.EventType, + Content: item.Content, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + }) + } + + return &types.QueryCommissionReturnLogResponse{ + List: respList, + Total: total, + }, nil +} + +func normalizePagination(page, size int) (int, int) { + if page <= 0 { + page = 1 + } + if size <= 0 { + size = 10 + } + return page, size +} + +func (l *QueryCommissionReturnLogLogic) queryCommissionReturnLogRecords(userID int64, page, size int) ([]commissionReturnLogRecord, int64, error) { + query := l.svcCtx.DB.WithContext(l.ctx). + Model(&log.SystemLog{}). + Where("`type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?)", + log.TypeCommission.Uint8(), + userID, + "%\"type\":333%", + "%\"type\":337%", + "%\"type\":338%", + ) + + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count commission return 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, 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query commission return logs failed: %v", err) + } + + list := make([]commissionReturnLogRecord, 0, len(rows)) + for _, row := range rows { + var content log.Commission + if err := content.Unmarshal([]byte(row.Content)); err != nil { + l.Errorw("unmarshal commission return log content failed", + logger.Field("log_id", row.Id), + logger.Field("error", err.Error()), + ) + continue + } + if !isCommissionReturnEventType(content.Type) { + continue + } + + timestamp := content.Timestamp + if timestamp == 0 { + timestamp = row.CreatedAt.UnixMilli() + } + list = append(list, commissionReturnLogRecord{ + LogID: row.Id, + UserID: row.ObjectID, + Amount: content.Amount, + EventType: content.Type, + Content: row.Content, + CreatedAt: timestamp, + UpdatedAt: timestamp, + }) + } + + return list, total, nil +} + +func isCommissionReturnEventType(eventType uint16) bool { + switch eventType { + case log.CommissionTypeRefund, log.CommissionTypeWithdrawReject, log.CommissionTypeWithdrawCancel: + return true + default: + return false + } +} diff --git a/internal/logic/public/user/queryWithdrawalLogLogic.go b/internal/logic/public/user/queryWithdrawalLogLogic.go index 8e7dc17..d86a75e 100644 --- a/internal/logic/public/user/queryWithdrawalLogLogic.go +++ b/internal/logic/public/user/queryWithdrawalLogLogic.go @@ -3,7 +3,6 @@ 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" @@ -40,14 +39,7 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - page := req.Page - size := req.Size - if page <= 0 { - page = 1 - } - if size <= 0 { - size = 10 - } + page, size := normalizePagination(req.Page, req.Size) switch req.BizType { case "", withdrawalLogBizTypeWithdrawal: @@ -97,46 +89,22 @@ func (l *QueryWithdrawalLogLogic) queryWithdrawalLogs(userID int64, page, size i } 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) + queryLogic := NewQueryCommissionReturnLogLogic(l.ctx, l.svcCtx) + rows, total, err := queryLogic.queryCommissionReturnLogRecords(userID, page, size) + if err != nil { + return nil, 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, + Id: row.LogID, BizType: withdrawalLogBizTypeCommissionRefund, - UserId: row.ObjectID, - Amount: content.Amount, + UserId: row.UserID, + Amount: row.Amount, Content: row.Content, - CreatedAt: timestamp, - UpdatedAt: timestamp, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, }) } diff --git a/internal/logic/public/user/queryWithdrawalLogLogic_test.go b/internal/logic/public/user/queryWithdrawalLogLogic_test.go index 9cc13a2..f9ce32e 100644 --- a/internal/logic/public/user/queryWithdrawalLogLogic_test.go +++ b/internal/logic/public/user/queryWithdrawalLogLogic_test.go @@ -1,6 +1,7 @@ package user import ( + "bytes" "context" "fmt" "strings" @@ -58,26 +59,56 @@ func TestQueryWithdrawalLog_WithWithdrawalBizType(t *testing.T) { 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) - } + assertQueryWithdrawalLogExpectations(t, mock) } -func TestQueryWithdrawalLog_WithCommissionRefundBizType(t *testing.T) { +func TestQueryCommissionReturnLog_HappyPathIncludes333337338(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). + expectCommissionReturnQueries(mock, userID, 3) + mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 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)) + AddRow(int64(2003), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":338,"amount":1500,"order_no":"ORDER-3","timestamp":1700000003123}`, createdAt). + AddRow(int64(2002), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":337,"amount":2000,"order_no":"ORDER-2","timestamp":1700000002123}`, createdAt). + AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000001123}`, createdAt)) + + logic := NewQueryCommissionReturnLogLogic(newTestQueryCtx(userID), &svc.ServiceContext{DB: db}) + resp, err := logic.QueryCommissionReturnLog(&types.QueryCommissionReturnLogRequest{Page: 1, Size: 10}) + if err != nil { + t.Fatalf("QueryCommissionReturnLog unexpected error: %v", err) + } + if resp.Total != 3 || len(resp.List) != 3 { + t.Fatalf("QueryCommissionReturnLog response = %+v, want three logs", resp) + } + + eventTypes := []uint16{resp.List[0].EventType, resp.List[1].EventType, resp.List[2].EventType} + wantTypes := []uint16{logmodel.CommissionTypeWithdrawCancel, logmodel.CommissionTypeWithdrawReject, logmodel.CommissionTypeRefund} + if fmt.Sprint(eventTypes) != fmt.Sprint(wantTypes) { + t.Fatalf("event types = %v, want %v", eventTypes, wantTypes) + } + + assertQueryWithdrawalLogExpectations(t, mock) +} + +func TestQueryWithdrawalLog_WithCommissionRefundBizTypeIncludes333337338(t *testing.T) { + const userID = int64(42) + createdAt := time.Unix(1700000000, 0) + + db, mock, cleanup := newQueryWithdrawalLogTestDB(t) + defer cleanup() + + expectCommissionReturnQueries(mock, userID, 3) + mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}). + AddRow(int64(2003), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":338,"amount":1500,"order_no":"ORDER-3","timestamp":1700000003123}`, createdAt). + AddRow(int64(2002), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":337,"amount":2000,"order_no":"ORDER-2","timestamp":1700000002123}`, createdAt). + AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":333,"amount":2500,"order_no":"ORDER-1","timestamp":1700000001123}`, createdAt)) logic := newTestQueryWithdrawalLogLogic(t, db, userID) resp, err := logic.QueryWithdrawalLog(&types.QueryWithdrawalLogListRequest{ @@ -88,22 +119,72 @@ func TestQueryWithdrawalLog_WithCommissionRefundBizType(t *testing.T) { 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) + if resp.Total != 3 || len(resp.List) != 3 { + t.Fatalf("QueryWithdrawalLog response = %+v, want three commission returns", resp) } - got := resp.List[0] - if got.BizType != withdrawalLogBizTypeCommissionRefund { - t.Fatalf("BizType = %q, want %q", got.BizType, withdrawalLogBizTypeCommissionRefund) + for _, item := range resp.List { + if item.BizType != withdrawalLogBizTypeCommissionRefund { + t.Fatalf("BizType = %q, want %q", item.BizType, withdrawalLogBizTypeCommissionRefund) + } + if item.Status != 0 || item.Reason != "" || item.Method != 0 || item.Account != "" || item.QrCodeUrl != "" { + t.Fatalf("withdrawal-only fields should keep zero values, got %+v", item) + } } - 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) + assertQueryWithdrawalLogExpectations(t, mock) +} + +func TestQueryCommissionReturnLog_SkipsInvalidJSONAndLogsWarn(t *testing.T) { + const userID = int64(42) + createdAt := time.Unix(1700000000, 0) + + db, mock, cleanup := newQueryWithdrawalLogTestDB(t) + defer cleanup() + + expectCommissionReturnQueries(mock, userID, 2) + mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"}). + AddRow(int64(2002), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":337,"amount":2000,"order_no":"ORDER-2","timestamp":1700000002123}`, createdAt). + AddRow(int64(2001), logmodel.TypeCommission.Uint8(), "2023-11-14", userID, `{"type":333`, createdAt)) + + var buf bytes.Buffer + restoreLogger := captureTestLogs(&buf) + defer restoreLogger() + + logic := NewQueryCommissionReturnLogLogic(newTestQueryCtx(userID), &svc.ServiceContext{DB: db}) + resp, err := logic.QueryCommissionReturnLog(&types.QueryCommissionReturnLogRequest{Page: 1, Size: 10}) + if err != nil { + t.Fatalf("QueryCommissionReturnLog unexpected error: %v", err) } - if got.Status != 0 || got.Method != 0 || got.Account != "" || got.QrCodeUrl != "" { - t.Fatalf("commission refund withdrawal-only fields = %+v, want zero values", got) + if resp.Total != 2 || len(resp.List) != 1 { + t.Fatalf("QueryCommissionReturnLog response = %+v, want total=2 and one valid row", resp) } - if err := mock.ExpectationsWereMet(); err != nil { - t.Fatalf("unmet sql expectations: %v", err) + if !strings.Contains(buf.String(), "unmarshal commission return log content failed") { + t.Fatalf("expected warn log, got %q", buf.String()) } + assertQueryWithdrawalLogExpectations(t, mock) +} + +func TestQueryCommissionReturnLog_FiltersOtherUsersByObjectID(t *testing.T) { + const userID = int64(42) + + db, mock, cleanup := newQueryWithdrawalLogTestDB(t) + defer cleanup() + + expectCommissionReturnQueries(mock, userID, 0) + mock.ExpectQuery("SELECT * FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?) ORDER BY id DESC LIMIT ?"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "type", "date", "object_id", "content", "created_at"})) + + logic := NewQueryCommissionReturnLogLogic(newTestQueryCtx(userID), &svc.ServiceContext{DB: db}) + resp, err := logic.QueryCommissionReturnLog(&types.QueryCommissionReturnLogRequest{Page: 1, Size: 10}) + if err != nil { + t.Fatalf("QueryCommissionReturnLog unexpected error: %v", err) + } + if resp.Total != 0 || len(resp.List) != 0 { + t.Fatalf("QueryCommissionReturnLog response = %+v, want no rows for filtered user", resp) + } + assertQueryWithdrawalLogExpectations(t, mock) } func TestQueryWithdrawalLog_RejectsInvalidBizType(t *testing.T) { @@ -119,9 +200,7 @@ func TestQueryWithdrawalLog_RejectsInvalidBizType(t *testing.T) { 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) - } + assertQueryWithdrawalLogExpectations(t, mock) } func newQueryWithdrawalLogTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) { @@ -150,7 +229,7 @@ func newQueryWithdrawalLogTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func( func newTestQueryWithdrawalLogLogic(t *testing.T, db *gorm.DB, userID int64) *QueryWithdrawalLogLogic { t.Helper() - ctx := context.WithValue(context.Background(), constant.CtxKeyUser, &usermodel.User{Id: userID}) + ctx := newTestQueryCtx(userID) return &QueryWithdrawalLogLogic{ Logger: logger.WithContext(ctx), ctx: ctx, @@ -160,6 +239,37 @@ func newTestQueryWithdrawalLogLogic(t *testing.T, db *gorm.DB, userID int64) *Qu } } +func newTestQueryCtx(userID int64) context.Context { + return context.WithValue(context.Background(), constant.CtxKeyUser, &usermodel.User{Id: userID}) +} + +func expectCommissionReturnQueries(mock sqlmock.Sqlmock, userID int64, total int64) { + mock.ExpectQuery("SELECT count(*) FROM `system_logs` WHERE `type` = ? AND object_id = ? AND (`content` LIKE ? OR `content` LIKE ? OR `content` LIKE ?)"). + WithArgs(logmodel.TypeCommission.Uint8(), userID, "%\"type\":333%", "%\"type\":337%", "%\"type\":338%"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) +} + +func assertQueryWithdrawalLogExpectations(t *testing.T, mock sqlmock.Sqlmock) { + t.Helper() + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func captureTestLogs(buf *bytes.Buffer) func() { + prevWriter := logger.Reset() + prevLevel := logger.InfoLevel + logger.SetLevel(logger.DebugLevel) + logger.SetWriter(logger.NewWriter(buf)) + return func() { + logger.Reset() + logger.SetLevel(prevLevel) + if prevWriter != nil { + logger.SetWriter(prevWriter) + } + } +} + func queryWithdrawalLogErrCodeOf(err error) uint32 { if err == nil { return 0 diff --git a/internal/types/types.go b/internal/types/types.go index cf1b4f9..ea0dad4 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -309,6 +309,16 @@ type CommissionLog struct { Timestamp int64 `json:"timestamp"` } +type CommissionReturnLog struct { + Id int64 `json:"id"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + EventType uint16 `json:"event_type"` + Content string `json:"content"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + type CommissionWithdrawRequest struct { Amount int64 `json:"amount"` Content string `json:"content"` @@ -2376,6 +2386,16 @@ type QueryAnnouncementResponse struct { List []Announcement `json:"announcements"` } +type QueryCommissionReturnLogRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type QueryCommissionReturnLogResponse struct { + List []CommissionReturnLog `json:"list"` + Total int64 `json:"total"` +} + type QueryDocumentDetailRequest struct { Id int64 `form:"id" validate:"required"` }