208
This commit is contained in:
@@ -30,6 +30,7 @@ type Config struct {
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
|
||||
Loki LokiConfig `yaml:"Loki"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
@@ -227,6 +228,12 @@ type OpenInstallConfig struct {
|
||||
ApiKey string `yaml:"ApiKey" default:""` // OpenInstall 数据接口 ApiKey
|
||||
}
|
||||
|
||||
// LokiConfig Loki 日志查询配置
|
||||
type LokiConfig struct {
|
||||
Enable bool `yaml:"Enable" default:"false"` // 是否启用 Loki 查询
|
||||
URL string `yaml:"URL" default:"http://localhost:3100"` // Loki 服务地址
|
||||
}
|
||||
|
||||
type Telegram struct {
|
||||
Enable bool `yaml:"Enable" default:"false"`
|
||||
BotID int64 `yaml:"BotID" default:""`
|
||||
|
||||
@@ -40,7 +40,6 @@ import (
|
||||
)
|
||||
|
||||
func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
router.Use(middleware.TraceMiddleware(serverCtx))
|
||||
|
||||
adminAdsGroupRouter := router.Group("/v1/admin/ads")
|
||||
adminAdsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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/loki"
|
||||
"github.com/perfect-panel/server/pkg/openinstall"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
@@ -20,6 +21,7 @@ type GetAgentDownloadsLogic struct {
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewGetAgentDownloadsLogic 创建 GetAgentDownloadsLogic 实例
|
||||
func NewGetAgentDownloadsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentDownloadsLogic {
|
||||
return &GetAgentDownloadsLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
@@ -28,6 +30,8 @@ func NewGetAgentDownloadsLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAgentDownloads 获取用户代理下载统计数据
|
||||
// 结合 OpenInstall (iOS/Android) 和 Loki (Windows/Mac) 数据源
|
||||
func (l *GetAgentDownloadsLogic) GetAgentDownloads(req *types.GetAgentDownloadsRequest) (resp *types.GetAgentDownloadsResponse, err error) {
|
||||
// 1. 从 context 获取用户信息
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
@@ -36,72 +40,68 @@ func (l *GetAgentDownloadsLogic) GetAgentDownloads(req *types.GetAgentDownloadsR
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// 2. 检查 OpenInstall 是否启用
|
||||
cfg := l.svcCtx.Config.OpenInstall
|
||||
if !cfg.Enable {
|
||||
l.Infow("[GetAgentDownloads] OpenInstall is disabled, returning zero stats")
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
Total: 0,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: 0,
|
||||
Android: 0,
|
||||
Windows: 0,
|
||||
Mac: 0,
|
||||
},
|
||||
}, nil
|
||||
// 初始化响应数据
|
||||
var iosCount, androidCount, windowsCount, macCount int64
|
||||
var comparisonRate *string
|
||||
|
||||
// 2. 从 OpenInstall 获取 iOS/Android 数据
|
||||
openInstallCfg := l.svcCtx.Config.OpenInstall
|
||||
if openInstallCfg.Enable && openInstallCfg.ApiKey != "" {
|
||||
client := openinstall.NewClient(openInstallCfg.ApiKey)
|
||||
platformDownloads, err := client.GetPlatformDownloads(l.ctx, u.ReferCode)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch OpenInstall platform downloads", logger.Field("error", err), logger.Field("user_id", u.Id))
|
||||
// 不返回错误,继续处理其他数据源
|
||||
} else {
|
||||
iosCount = platformDownloads.IOS
|
||||
androidCount = platformDownloads.Android
|
||||
// OpenInstall 的 Windows/Mac 数据可能为空,后面用 Loki 补充
|
||||
|
||||
// 计算环比
|
||||
if platformDownloads.Comparison != nil {
|
||||
percent := platformDownloads.Comparison.ChangePercent
|
||||
var formatted string
|
||||
if percent >= 0 {
|
||||
formatted = fmt.Sprintf("+%.1f%%", percent)
|
||||
} else {
|
||||
formatted = fmt.Sprintf("%.1f%%", percent)
|
||||
}
|
||||
comparisonRate = &formatted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查 ApiKey 是否配置
|
||||
if cfg.ApiKey == "" {
|
||||
l.Errorw("[GetAgentDownloads] OpenInstall ApiKey not configured")
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
Total: 0,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: 0,
|
||||
Android: 0,
|
||||
Windows: 0,
|
||||
Mac: 0,
|
||||
},
|
||||
}, nil
|
||||
// 3. 从 Loki 获取 Windows/Mac 数据(基于用户邀请码)
|
||||
lokiCfg := l.svcCtx.Config.Loki
|
||||
if lokiCfg.Enable && lokiCfg.URL != "" && u.ReferCode != "" {
|
||||
lokiClient := loki.NewClient(lokiCfg.URL)
|
||||
lokiStats, err := lokiClient.GetInviteCodeStats(l.ctx, u.ReferCode, 30)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch Loki stats", logger.Field("error", err), logger.Field("user_id", u.Id), logger.Field("refer_code", u.ReferCode))
|
||||
// 不返回错误,继续使用已有数据
|
||||
} else {
|
||||
// 使用 Loki 的 Windows/Mac 数据
|
||||
windowsCount = lokiStats.WindowsClicks
|
||||
macCount = lokiStats.MacClicks
|
||||
l.Infow("Fetched Loki stats successfully",
|
||||
logger.Field("user_id", u.Id),
|
||||
logger.Field("refer_code", u.ReferCode),
|
||||
logger.Field("windows", windowsCount),
|
||||
logger.Field("mac", macCount))
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 调用 OpenInstall API 获取各端下载量
|
||||
client := openinstall.NewClient(cfg.ApiKey)
|
||||
platformDownloads, err := client.GetPlatformDownloads(l.ctx)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to fetch OpenInstall platform downloads", logger.Field("error", err), logger.Field("user_id", u.Id))
|
||||
// 返回空数据而不是错误,避免影响前端显示
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
Total: 0,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: 0,
|
||||
Android: 0,
|
||||
Windows: 0,
|
||||
Mac: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
// 4. 计算总量
|
||||
total := iosCount + androidCount + windowsCount + macCount
|
||||
|
||||
// 5. 构造响应
|
||||
var comparisonRate *string
|
||||
if platformDownloads.Comparison != nil {
|
||||
percent := platformDownloads.Comparison.ChangePercent
|
||||
var formatted string
|
||||
if percent >= 0 {
|
||||
formatted = fmt.Sprintf("+%.1f%%", percent)
|
||||
} else {
|
||||
formatted = fmt.Sprintf("%.1f%%", percent)
|
||||
}
|
||||
comparisonRate = &formatted
|
||||
}
|
||||
|
||||
return &types.GetAgentDownloadsResponse{
|
||||
Total: platformDownloads.Total,
|
||||
Total: total,
|
||||
Platforms: &types.PlatformDownloads{
|
||||
IOS: platformDownloads.IOS,
|
||||
Android: platformDownloads.Android,
|
||||
Windows: platformDownloads.Windows,
|
||||
Mac: platformDownloads.Mac,
|
||||
IOS: iosCount,
|
||||
Android: androidCount,
|
||||
Windows: windowsCount,
|
||||
Mac: macCount,
|
||||
},
|
||||
ComparisonRate: comparisonRate,
|
||||
}, nil
|
||||
|
||||
@@ -40,11 +40,19 @@ func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (
|
||||
|
||||
// 2. Count total sales
|
||||
var totalSales int64
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
db := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("`order` o").
|
||||
Joins("JOIN user u ON o.user_id = u.id").
|
||||
Where("u.referer_id = ? AND o.status = ?", userId, 5).
|
||||
Count(&totalSales).Error
|
||||
Where("u.referer_id = ? AND o.status = ?", userId, 5)
|
||||
|
||||
if req.StartTime > 0 {
|
||||
db = db.Where("o.updated_at >= FROM_UNIXTIME(?)", req.StartTime)
|
||||
}
|
||||
if req.EndTime > 0 {
|
||||
db = db.Where("o.updated_at <= FROM_UNIXTIME(?)", req.EndTime)
|
||||
}
|
||||
|
||||
err = db.Count(&totalSales).Error
|
||||
if err != nil {
|
||||
l.Errorw("[GetInviteSales] count sales failed",
|
||||
logger.Field("error", err.Error()),
|
||||
@@ -75,13 +83,21 @@ func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (
|
||||
}
|
||||
|
||||
var orderData []OrderWithUser
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
query := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("`order` o").
|
||||
Select("o.amount, CAST(UNIX_TIMESTAMP(o.updated_at) * 1000 AS SIGNED) as updated_at, u.id as user_id, s.name as product_name, o.quantity").
|
||||
Joins("JOIN user u ON o.user_id = u.id").
|
||||
Joins("LEFT JOIN subscribe s ON o.subscribe_id = s.id").
|
||||
Where("u.referer_id = ? AND o.status = ?", userId, 5). // status 5: Finished
|
||||
Order("o.updated_at DESC").
|
||||
Where("u.referer_id = ? AND o.status = ?", userId, 5) // status 5: Finished
|
||||
|
||||
if req.StartTime > 0 {
|
||||
query = query.Where("o.updated_at >= FROM_UNIXTIME(?)", req.StartTime)
|
||||
}
|
||||
if req.EndTime > 0 {
|
||||
query = query.Where("o.updated_at <= FROM_UNIXTIME(?)", req.EndTime)
|
||||
}
|
||||
|
||||
err = query.Order("o.updated_at DESC").
|
||||
Limit(req.Size).
|
||||
Offset(offset).
|
||||
Scan(&orderData).Error
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/model/order"
|
||||
"github.com/perfect-panel/server/internal/model/subscribe"
|
||||
"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/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// setupTestSvcCtx 初始化测试上下文
|
||||
func setupTestSvcCtx(t *testing.T) (*svc.ServiceContext, *gorm.DB) {
|
||||
// 1. Setup Miniredis
|
||||
mr, err := miniredis.Run()
|
||||
assert.NoError(t, err)
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: mr.Addr(),
|
||||
})
|
||||
|
||||
// 2. Setup GORM with SQLite
|
||||
dbName := fmt.Sprintf("test_sales_%d.db", time.Now().UnixNano())
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
os.Remove(dbName)
|
||||
mr.Close()
|
||||
})
|
||||
|
||||
// Migrate tables
|
||||
_ = db.Migrator().CreateTable(&user.User{})
|
||||
_ = db.Migrator().CreateTable(&subscribe.Subscribe{}) // Plan definition
|
||||
_ = db.Migrator().CreateTable(&order.Order{})
|
||||
|
||||
// 3. Create ServiceContext
|
||||
svcCtx := &svc.ServiceContext{
|
||||
Redis: rdb,
|
||||
DB: db,
|
||||
Config: config.Config{},
|
||||
UserModel: user.NewModel(db, rdb),
|
||||
}
|
||||
|
||||
return svcCtx, db
|
||||
}
|
||||
|
||||
func TestGetInviteSales_TimeFilter(t *testing.T) {
|
||||
svcCtx, db := setupTestSvcCtx(t)
|
||||
|
||||
// 1. Prepare Data
|
||||
// Referrer User (Current User)
|
||||
referrer := &user.User{
|
||||
Id: 100,
|
||||
// Email removed (not in struct)
|
||||
ReferCode: "REF100",
|
||||
}
|
||||
db.Create(referrer)
|
||||
|
||||
// Invited User
|
||||
invitedUser := &user.User{
|
||||
Id: 200,
|
||||
// Email removed
|
||||
RefererId: referrer.Id, // Linked to referrer
|
||||
}
|
||||
db.Create(invitedUser)
|
||||
|
||||
// Subscribe (Plan)
|
||||
sub := &subscribe.Subscribe{
|
||||
Id: 1,
|
||||
Name: "Standard Plan",
|
||||
}
|
||||
db.Create(sub)
|
||||
|
||||
// Orders
|
||||
// Order 1: Inside Range (2023-10-15)
|
||||
timeIn := time.Date(2023, 10, 15, 12, 0, 0, 0, time.UTC)
|
||||
db.Create(&order.Order{
|
||||
UserId: invitedUser.Id,
|
||||
OrderNo: "ORD001",
|
||||
Status: 5, // Finished
|
||||
Amount: 1000,
|
||||
Quantity: 30,
|
||||
SubscribeId: sub.Id,
|
||||
UpdatedAt: timeIn,
|
||||
})
|
||||
|
||||
// Order 2: Before Range (2023-09-15)
|
||||
timeBefore := time.Date(2023, 9, 15, 12, 0, 0, 0, time.UTC)
|
||||
db.Create(&order.Order{
|
||||
UserId: invitedUser.Id,
|
||||
OrderNo: "ORD002",
|
||||
Status: 5, // Finished
|
||||
Amount: 1000,
|
||||
Quantity: 30,
|
||||
SubscribeId: sub.Id,
|
||||
UpdatedAt: timeBefore,
|
||||
})
|
||||
|
||||
// Order 3: After Range (2023-11-15)
|
||||
timeAfter := time.Date(2023, 11, 15, 12, 0, 0, 0, time.UTC)
|
||||
db.Create(&order.Order{
|
||||
UserId: invitedUser.Id,
|
||||
OrderNo: "ORD003",
|
||||
Status: 5, // Finished
|
||||
Amount: 1000,
|
||||
Quantity: 30,
|
||||
SubscribeId: sub.Id,
|
||||
UpdatedAt: timeAfter,
|
||||
})
|
||||
|
||||
// Order 4: Wrong Status (2023-10-16) - Should be ignored
|
||||
db.Create(&order.Order{
|
||||
UserId: invitedUser.Id,
|
||||
OrderNo: "ORD004",
|
||||
Status: 1, // Pending
|
||||
Amount: 1000,
|
||||
Quantity: 30,
|
||||
SubscribeId: sub.Id,
|
||||
UpdatedAt: timeIn.Add(24 * time.Hour),
|
||||
})
|
||||
|
||||
// 2. Execute Logic
|
||||
// Context with current user
|
||||
ctx := context.WithValue(context.Background(), constant.CtxKeyUser, referrer)
|
||||
l := NewGetInviteSalesLogic(ctx, svcCtx)
|
||||
|
||||
// Filter for October 2023
|
||||
startTime := time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC).Unix()
|
||||
endTime := time.Date(2023, 10, 31, 23, 59, 59, 0, time.UTC).Unix()
|
||||
|
||||
req := &types.GetInviteSalesRequest{
|
||||
Page: 1,
|
||||
Size: 10,
|
||||
StartTime: startTime, // 2023-10-01
|
||||
EndTime: endTime, // 2023-10-31
|
||||
}
|
||||
|
||||
resp, err := l.GetInviteSales(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 3. Verify Results
|
||||
// Should match exactly 1 order (ORD001)
|
||||
assert.Equal(t, int64(1), resp.Total, "Should return exactly 1 order matching time range and status")
|
||||
if assert.NotEmpty(t, resp.List) {
|
||||
assert.Equal(t, 1, len(resp.List))
|
||||
// Log result for debug
|
||||
t.Logf("Found Sale: Amount=%.2f, Time=%d", resp.List[0].Amount, resp.List[0].UpdatedAt)
|
||||
|
||||
// Verify timestamp is roughly correct (millisecond precision in logic)
|
||||
expectedMs := timeIn.Unix() * 1000
|
||||
assert.Equal(t, expectedMs, resp.List[0].UpdatedAt)
|
||||
} else {
|
||||
t.Error("Returned list is empty")
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
model "github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -31,6 +33,21 @@ func (w bodyLogWriter) Write(b []byte) (int, error) {
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// inviteCodeRegex matches invite code patterns in URLs like:
|
||||
// /v1/common/client/download/file/Hi快VPN-mac-1.0.0-ic-uuSo11uy.dmg
|
||||
// Matches: ic-XXXXX or ic_XXXXX before file extension
|
||||
var inviteCodeRegex = regexp.MustCompile(`[-_]ic[-_]([a-zA-Z0-9]+)\.[a-zA-Z0-9]+$`)
|
||||
|
||||
// extractInviteCode extracts invite code from URL path
|
||||
// Returns empty string if no invite code found
|
||||
func extractInviteCode(path string) string {
|
||||
matches := inviteCodeRegex.FindStringSubmatch(path)
|
||||
if len(matches) >= 2 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// statusByWriter returns a span status code and message for an HTTP status code
|
||||
// value returned by a server. Status codes in the 400-499 range are not
|
||||
// returned as errors.
|
||||
@@ -48,7 +65,7 @@ func requestAttributes(req *http.Request) []attribute.KeyValue {
|
||||
protoN := strings.SplitN(req.Proto, "/", 2)
|
||||
remoteAddrN := strings.SplitN(req.RemoteAddr, ":", 2)
|
||||
|
||||
return []attribute.KeyValue{
|
||||
attrs := []attribute.KeyValue{
|
||||
semconv.HTTPRequestMethodKey.String(req.Method),
|
||||
semconv.HTTPUserAgentKey.String(req.UserAgent()),
|
||||
semconv.HTTPRequestContentLengthKey.Int64(req.ContentLength),
|
||||
@@ -65,6 +82,66 @@ func requestAttributes(req *http.Request) []attribute.KeyValue {
|
||||
semconv.ClientAddressKey.String(remoteAddrN[0]),
|
||||
semconv.ClientPortKey.String(remoteAddrN[1]),
|
||||
}
|
||||
|
||||
// Extract invite code from URL path (e.g., /v1/common/client/download/file/Hi快VPN-mac-1.0.0-ic-uuSo11uy.dmg)
|
||||
if inviteCode := extractInviteCode(req.URL.Path); inviteCode != "" {
|
||||
attrs = append(attrs, attribute.String("affiliate.invite_code", inviteCode))
|
||||
attrs = append(attrs, attribute.String("affiliate.source", "download_link"))
|
||||
}
|
||||
|
||||
// Also check query parameter for invite code (e.g., ?ic=uuSo11uy)
|
||||
if ic := req.URL.Query().Get("ic"); ic != "" {
|
||||
attrs = append(attrs, attribute.String("affiliate.invite_code", ic))
|
||||
attrs = append(attrs, attribute.String("affiliate.source", "query_param"))
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
// userAttributes extracts user information from context and returns span attributes
|
||||
func userAttributes(ctx context.Context) []attribute.KeyValue {
|
||||
var attrs []attribute.KeyValue
|
||||
|
||||
// Get user info from context (set by authMiddleware)
|
||||
if userInfo := ctx.Value(constant.CtxKeyUser); userInfo != nil {
|
||||
if user, ok := userInfo.(*model.User); ok {
|
||||
var email string
|
||||
for _, method := range user.AuthMethods {
|
||||
if method.AuthType == "email" {
|
||||
email = method.AuthIdentifier
|
||||
break
|
||||
}
|
||||
}
|
||||
attrs = append(attrs,
|
||||
attribute.Int64("user.id", user.Id),
|
||||
attribute.String("user.email", email),
|
||||
attribute.Bool("user.is_admin", *user.IsAdmin),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get session ID from context
|
||||
if sessionID := ctx.Value(constant.CtxKeySessionID); sessionID != nil {
|
||||
if sid, ok := sessionID.(string); ok {
|
||||
attrs = append(attrs, attribute.String("user.session_id", sid))
|
||||
}
|
||||
}
|
||||
|
||||
// Get device ID from context
|
||||
if deviceID := ctx.Value(constant.CtxKeyDeviceID); deviceID != nil {
|
||||
if did, ok := deviceID.(int64); ok {
|
||||
attrs = append(attrs, attribute.Int64("user.device_id", did))
|
||||
}
|
||||
}
|
||||
|
||||
// Get login type from context
|
||||
if loginType := ctx.Value(constant.LoginType); loginType != nil {
|
||||
if lt, ok := loginType.(string); ok {
|
||||
attrs = append(attrs, attribute.String("user.login_type", lt))
|
||||
}
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) {
|
||||
@@ -99,6 +176,9 @@ func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) {
|
||||
semconv.HTTPRouteKey.String(c.FullPath()),
|
||||
)
|
||||
|
||||
// Add user attributes from context (set by authMiddleware)
|
||||
span.SetAttributes(userAttributes(ctx)...)
|
||||
|
||||
// Record Request Body (limit to 1MB)
|
||||
if len(reqBody) > 0 {
|
||||
limit := 1048576
|
||||
|
||||
@@ -948,8 +948,10 @@ type GetUserInviteStatsResponse struct {
|
||||
}
|
||||
|
||||
type GetInviteSalesRequest struct {
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
Page int `form:"page" validate:"required"`
|
||||
Size int `form:"size" validate:"required"`
|
||||
StartTime int64 `form:"start_time,optional"`
|
||||
EndTime int64 `form:"end_time,optional"`
|
||||
}
|
||||
|
||||
type GetInviteSalesResponse struct {
|
||||
|
||||
Reference in New Issue
Block a user