合并 internal → main: 退款/提现/激活/CI 全面优化 #4
@@ -117,6 +117,7 @@ type (
|
|||||||
}
|
}
|
||||||
WithdrawalLog {
|
WithdrawalLog {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
|
BizType string `json:"biz_type"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
@@ -132,8 +133,9 @@ type (
|
|||||||
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
|
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
QueryWithdrawalLogListRequest {
|
QueryWithdrawalLogListRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
|
BizType string `form:"biz_type" validate:"omitempty,oneof=withdrawal commission_refund"`
|
||||||
}
|
}
|
||||||
QueryWithdrawalLogListResponse {
|
QueryWithdrawalLogListResponse {
|
||||||
List []WithdrawalLog `json:"list"`
|
List []WithdrawalLog `json:"list"`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
@@ -12,6 +13,11 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
withdrawalLogBizTypeWithdrawal = "withdrawal"
|
||||||
|
withdrawalLogBizTypeCommissionRefund = "commission_refund"
|
||||||
|
)
|
||||||
|
|
||||||
type QueryWithdrawalLogLogic struct {
|
type QueryWithdrawalLogLogic struct {
|
||||||
logger.Logger
|
logger.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -43,15 +49,26 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL
|
|||||||
size = 10
|
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
|
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)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count withdrawal logs failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []user.Withdrawal
|
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)
|
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 {
|
for _, row := range rows {
|
||||||
list = append(list, types.WithdrawalLog{
|
list = append(list, types.WithdrawalLog{
|
||||||
Id: row.Id,
|
Id: row.Id,
|
||||||
|
BizType: withdrawalLogBizTypeWithdrawal,
|
||||||
UserId: row.UserId,
|
UserId: row.UserId,
|
||||||
Amount: row.Amount,
|
Amount: row.Amount,
|
||||||
Content: row.Content,
|
Content: row.Content,
|
||||||
@@ -77,3 +95,53 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL
|
|||||||
Total: total,
|
Total: total,
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -2540,8 +2540,9 @@ type QueryUserSubscribeNodeListResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type QueryWithdrawalLogListRequest struct {
|
type QueryWithdrawalLogListRequest struct {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
|
BizType string `form:"biz_type" validate:"omitempty,oneof=withdrawal commission_refund"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryWithdrawalLogListResponse struct {
|
type QueryWithdrawalLogListResponse struct {
|
||||||
@@ -3885,6 +3886,7 @@ type WeeklyStat struct {
|
|||||||
|
|
||||||
type WithdrawalLog struct {
|
type WithdrawalLog struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
|
BizType string `json:"biz_type"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
|||||||
@@ -1678,6 +1678,16 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "biz_type",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"withdrawal",
|
||||||
|
"commission_refund"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -5121,6 +5131,13 @@
|
|||||||
"size": {
|
"size": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
|
},
|
||||||
|
"biz_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"withdrawal",
|
||||||
|
"commission_refund"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "QueryWithdrawalLogListRequest",
|
"title": "QueryWithdrawalLogListRequest",
|
||||||
@@ -7168,11 +7185,15 @@
|
|||||||
"updated_at": {
|
"updated_at": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64"
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"biz_type": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "WithdrawalLog",
|
"title": "WithdrawalLog",
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
|
"biz_type",
|
||||||
"user_id",
|
"user_id",
|
||||||
"amount",
|
"amount",
|
||||||
"content",
|
"content",
|
||||||
|
|||||||
Reference in New Issue
Block a user