From 197fed7d121879e993323388cf3548dbabad2a7c Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Wed, 27 May 2026 22:50:16 -0700 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD(#102):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E9=82=80=E8=AF=B7=E8=AE=B0=E5=BD=95=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=B9=B6=E5=88=A0=E9=99=A4=E6=97=A7=E9=82=80=E8=AF=B7=E9=94=80?= =?UTF-8?q?=E5=94=AE=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- apis/public/user.api | 26 +-- go.mod | 1 + go.sum | 3 + ...sHandler.go => getInviteRecordsHandler.go} | 10 +- internal/handler/routes.go | 5 +- .../public/user/getInviteRecordsLogic.go | 198 ++++++++++++++++++ .../public/user/getInviteRecordsLogic_test.go | 157 ++++++++++++++ .../logic/public/user/getInviteSalesLogic.go | 145 ------------- internal/types/types.go | 19 +- pkg/hash/hash.go | 11 + user.json | 48 +++-- 11 files changed, 426 insertions(+), 197 deletions(-) rename internal/handler/public/user/{getInviteSalesHandler.go => getInviteRecordsHandler.go} (68%) create mode 100644 internal/logic/public/user/getInviteRecordsLogic.go create mode 100644 internal/logic/public/user/getInviteRecordsLogic_test.go delete mode 100644 internal/logic/public/user/getInviteSalesLogic.go diff --git a/apis/public/user.api b/apis/public/user.api index f88fc26..4378df1 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -201,21 +201,22 @@ type ( GrowthRate string `json:"growth_rate"` PaidGrowthRate string `json:"paid_growth_rate"` } - GetInviteSalesRequest { + GetInviteRecordsRequest { Page int `form:"page"` Size int `form:"size"` StartTime int64 `form:"start_time"` EndTime int64 `form:"end_time"` } - InvitedUserSale { - Amount float64 `json:"amount"` - UpdatedAt int64 `json:"updated_at"` - UserHash string `json:"user_hash"` - ProductName string `json:"product_name"` + InviteRecord { + Role string `json:"role"` + PeerHash string `json:"peer_hash"` + GiftDays int64 `json:"gift_days"` + OrderNo string `json:"order_no"` + CreatedAt int64 `json:"created_at"` } - GetInviteSalesResponse { - Total int64 `json:"total"` - List []InvitedUserSale `json:"list"` + GetInviteRecordsResponse { + Total int64 `json:"total"` + List []InviteRecord `json:"list"` } GetSubscribeStatusRequest { Email string `form:"email" json:"email" validate:"omitempty,email"` @@ -397,9 +398,9 @@ service ppanel { @handler GetAgentRealtime get /agent_realtime (GetAgentRealtimeRequest) returns (GetAgentRealtimeResponse) - @doc "Get Invite Sales" - @handler GetInviteSales - get /invite_sales (GetInviteSalesRequest) returns (GetInviteSalesResponse) + @doc "Get Invite Records" + @handler GetInviteRecords + get /invite_records (GetInviteRecordsRequest) returns (GetInviteRecordsResponse) @doc "Get Subscribe Status" @handler GetSubscribeStatus @@ -424,4 +425,3 @@ service ppanel { @handler DeviceWsConnect get /device_ws_connect } - diff --git a/go.mod b/go.mod index ca67325..3dda44b 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( ) require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/Masterminds/sprig/v3 v3.3.0 github.com/aws/aws-sdk-go-v2 v1.41.7 github.com/aws/aws-sdk-go-v2/config v1.32.17 diff --git a/go.sum b/go.sum index df90cc5..19ed347 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0= github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -264,6 +266,7 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/internal/handler/public/user/getInviteSalesHandler.go b/internal/handler/public/user/getInviteRecordsHandler.go similarity index 68% rename from internal/handler/public/user/getInviteSalesHandler.go rename to internal/handler/public/user/getInviteRecordsHandler.go index ce88ff4..7b6f18f 100644 --- a/internal/handler/public/user/getInviteSalesHandler.go +++ b/internal/handler/public/user/getInviteRecordsHandler.go @@ -8,10 +8,10 @@ import ( "github.com/perfect-panel/server/pkg/result" ) -// Get invite sales data -func GetInviteSalesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { +// Get invite gift records +func GetInviteRecordsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - var req types.GetInviteSalesRequest + var req types.GetInviteRecordsRequest if err := c.ShouldBind(&req); err != nil { result.ParamErrorResult(c, err) return @@ -23,8 +23,8 @@ func GetInviteSalesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return } - l := user.NewGetInviteSalesLogic(c.Request.Context(), svcCtx) - resp, err := l.GetInviteSales(&req) + l := user.NewGetInviteRecordsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetInviteRecords(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 2fc733a..85225d0 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -1118,9 +1118,8 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Query User Info publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx)) - // Get Invite Sales - publicUserGroupRouter.GET("/invite_sales", publicUser.GetInviteSalesHandler(serverCtx)) - publicUserGroupRouter.GET("/invite/sales", publicUser.GetInviteSalesHandler(serverCtx)) // alias: backward-compat + // Get Invite Records + publicUserGroupRouter.GET("/invite_records", publicUser.GetInviteRecordsHandler(serverCtx)) // Get User Invite Stats publicUserGroupRouter.GET("/invite_stats", publicUser.GetUserInviteStatsHandler(serverCtx)) diff --git a/internal/logic/public/user/getInviteRecordsLogic.go b/internal/logic/public/user/getInviteRecordsLogic.go new file mode 100644 index 0000000..5238928 --- /dev/null +++ b/internal/logic/public/user/getInviteRecordsLogic.go @@ -0,0 +1,198 @@ +package user + +import ( + "context" + "encoding/json" + + logmodel "github.com/perfect-panel/server/internal/model/log" + ordermodel "github.com/perfect-panel/server/internal/model/order" + "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/hash" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +const ( + inviteRecordRoleInviter = "inviter" + inviteRecordRoleInvitee = "invitee" +) + +type GetInviteRecordsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +type inviteRecordLog struct { + Id int64 `gorm:"column:id"` + ObjectId int64 `gorm:"column:object_id"` + Content string `gorm:"column:content"` + CreatedAt int64 `gorm:"column:created_at"` +} + +type inviteGiftContent struct { + OrderNo string `json:"order_no"` + Amount int64 `json:"amount"` +} + +type parsedInviteRecordLog struct { + log inviteRecordLog + content inviteGiftContent +} + +type inviteOrderUser struct { + OrderNo string `gorm:"column:order_no"` + UserId int64 `gorm:"column:user_id"` +} + +// Get invite gift records +func NewGetInviteRecordsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteRecordsLogic { + return &GetInviteRecordsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetInviteRecordsLogic) GetInviteRecords(req *types.GetInviteRecordsRequest) (resp *types.GetInviteRecordsResponse, err error) { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) + if !ok { + l.Errorw("[GetInviteRecords] user not found in context") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + normalizeInviteRecordsPagination(req) + + query := l.svcCtx.DB.WithContext(l.ctx). + Table("system_logs"). + Where("type = ? AND object_id = ?", logmodel.TypeGift.Uint8(), u.Id). + Where("JSON_VALID(content) = 1"). + Where("JSON_UNQUOTE(JSON_EXTRACT(content, '$.remark')) = ?", "邀请赠送") + if req.StartTime > 0 { + query = query.Where("created_at >= FROM_UNIXTIME(?)", req.StartTime) + } + if req.EndTime > 0 { + query = query.Where("created_at <= FROM_UNIXTIME(?)", req.EndTime) + } + + var total int64 + if err = query.Count(&total).Error; err != nil { + l.Errorw("[GetInviteRecords] count logs failed", + logger.Field("error", err.Error()), + logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count logs failed: %v", err.Error()) + } + + var logs []inviteRecordLog + if err = query. + Select("id, object_id, content, CAST(UNIX_TIMESTAMP(created_at) * 1000 AS SIGNED) AS created_at"). + Order("created_at DESC, id DESC"). + Limit(req.Size). + Offset((req.Page - 1) * req.Size). + Scan(&logs).Error; err != nil { + l.Errorw("[GetInviteRecords] query logs failed", + logger.Field("error", err.Error()), + logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query logs failed: %v", err.Error()) + } + + parsedLogs, orderNos := l.parseInviteRecordContents(logs) + if len(logs) == 0 || len(parsedLogs) == 0 { + return &types.GetInviteRecordsResponse{Total: total, List: []types.InviteRecord{}}, nil + } + + orders, err := l.queryInviteRecordOrders(orderNos) + if err != nil { + l.Errorw("[GetInviteRecords] query orders failed", + logger.Field("error", err.Error()), + logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query orders failed: %v", err.Error()) + } + + list := make([]types.InviteRecord, 0, len(parsedLogs)) + for _, parsed := range parsedLogs { + content := parsed.content + logItem := parsed.log + record := types.InviteRecord{ + Role: inviteRecordRoleInviter, + GiftDays: content.Amount, + OrderNo: content.OrderNo, + CreatedAt: logItem.CreatedAt, + } + + if orderInfo, ok := orders[content.OrderNo]; ok { + peerId := orderInfo.UserId + if orderInfo.UserId == u.Id { + record.Role = inviteRecordRoleInvitee + peerId = u.RefererId + } + if peerId > 0 { + record.PeerHash = hash.InvitePeerHash(peerId) + } + } + + list = append(list, record) + } + + return &types.GetInviteRecordsResponse{ + Total: total, + List: list, + }, nil +} + +func normalizeInviteRecordsPagination(req *types.GetInviteRecordsRequest) { + if req.Page < 1 { + req.Page = 1 + } + if req.Size < 1 { + req.Size = 10 + } + if req.Size > 100 { + req.Size = 100 + } +} + +func (l *GetInviteRecordsLogic) parseInviteRecordContents(logs []inviteRecordLog) ([]parsedInviteRecordLog, []string) { + parsedLogs := make([]parsedInviteRecordLog, 0, len(logs)) + orderNos := make([]string, 0, len(logs)) + for _, logItem := range logs { + var content inviteGiftContent + if err := json.Unmarshal([]byte(logItem.Content), &content); err != nil { + l.Infow("[GetInviteRecords] parse content failed", + logger.Field("error", err.Error()), + logger.Field("log_id", logItem.Id)) + continue + } + parsedLogs = append(parsedLogs, parsedInviteRecordLog{log: logItem, content: content}) + if content.OrderNo != "" { + orderNos = append(orderNos, content.OrderNo) + } + } + return parsedLogs, orderNos +} + +func (l *GetInviteRecordsLogic) queryInviteRecordOrders(orderNos []string) (map[string]inviteOrderUser, error) { + orders := make(map[string]inviteOrderUser, len(orderNos)) + if len(orderNos) == 0 { + return orders, nil + } + + var orderData []inviteOrderUser + err := l.svcCtx.DB.WithContext(l.ctx). + Model(&ordermodel.Order{}). + Select("order_no, user_id"). + Where("order_no IN ?", orderNos). + Scan(&orderData).Error + if err != nil { + return nil, err + } + + for _, item := range orderData { + orders[item.OrderNo] = item + } + return orders, nil +} diff --git a/internal/logic/public/user/getInviteRecordsLogic_test.go b/internal/logic/public/user/getInviteRecordsLogic_test.go new file mode 100644 index 0000000..8c46cb3 --- /dev/null +++ b/internal/logic/public/user/getInviteRecordsLogic_test.go @@ -0,0 +1,157 @@ +package user + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + modeluser "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/hash" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func TestGetInviteRecordsInviter(t *testing.T) { + svcCtx, mock, cleanup := newInviteRecordsTestSvc(t) + defer cleanup() + + mock.ExpectQuery("count(*)"). + WithArgs(34, int64(100), "邀请赠送"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery("SELECT id, object_id, content"). + WithArgs(34, int64(100), "邀请赠送", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "object_id", "content", "created_at"}). + AddRow(1, 100, `{"order_no":"order-1","amount":7,"remark":"邀请赠送"}`, 1779934580000)) + mock.ExpectQuery("SELECT order_no, user_id FROM `order`"). + WithArgs("order-1"). + WillReturnRows(sqlmock.NewRows([]string{"order_no", "user_id"}).AddRow("order-1", 200)) + + resp, err := NewGetInviteRecordsLogic(inviteRecordsContext(100, 0), svcCtx).GetInviteRecords(&types.GetInviteRecordsRequest{Page: 1, Size: 10}) + if err != nil { + t.Fatalf("GetInviteRecords returned error: %v", err) + } + assertInviteRecordResponse(t, resp, types.InviteRecord{ + Role: inviteRecordRoleInviter, + PeerHash: hash.InvitePeerHash(200), + GiftDays: 7, + OrderNo: "order-1", + CreatedAt: 1779934580000, + }) + assertInviteRecordsExpectations(t, mock) +} + +func TestGetInviteRecordsInvitee(t *testing.T) { + svcCtx, mock, cleanup := newInviteRecordsTestSvc(t) + defer cleanup() + + mock.ExpectQuery("count(*)"). + WithArgs(34, int64(200), "邀请赠送"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery("SELECT id, object_id, content"). + WithArgs(34, int64(200), "邀请赠送", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "object_id", "content", "created_at"}). + AddRow(2, 200, `{"order_no":"order-2","amount":7,"remark":"邀请赠送"}`, 1779934590000)) + mock.ExpectQuery("SELECT order_no, user_id FROM `order`"). + WithArgs("order-2"). + WillReturnRows(sqlmock.NewRows([]string{"order_no", "user_id"}).AddRow("order-2", 200)) + + resp, err := NewGetInviteRecordsLogic(inviteRecordsContext(200, 100), svcCtx).GetInviteRecords(&types.GetInviteRecordsRequest{Page: 1, Size: 10}) + if err != nil { + t.Fatalf("GetInviteRecords returned error: %v", err) + } + assertInviteRecordResponse(t, resp, types.InviteRecord{ + Role: inviteRecordRoleInvitee, + PeerHash: hash.InvitePeerHash(100), + GiftDays: 7, + OrderNo: "order-2", + CreatedAt: 1779934590000, + }) + assertInviteRecordsExpectations(t, mock) +} + +func TestGetInviteRecordsMissingOrderReturnsDirtyRecord(t *testing.T) { + svcCtx, mock, cleanup := newInviteRecordsTestSvc(t) + defer cleanup() + + mock.ExpectQuery("count(*)"). + WithArgs(34, int64(100), "邀请赠送"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + mock.ExpectQuery("SELECT id, object_id, content"). + WithArgs(34, int64(100), "邀请赠送", 10). + WillReturnRows(sqlmock.NewRows([]string{"id", "object_id", "content", "created_at"}). + AddRow(3, 100, `{"order_no":"missing-order","amount":7,"remark":"邀请赠送"}`, 1779934600000)) + mock.ExpectQuery("SELECT order_no, user_id FROM `order`"). + WithArgs("missing-order"). + WillReturnRows(sqlmock.NewRows([]string{"order_no", "user_id"})) + + resp, err := NewGetInviteRecordsLogic(inviteRecordsContext(100, 0), svcCtx).GetInviteRecords(&types.GetInviteRecordsRequest{Page: 1, Size: 10}) + if err != nil { + t.Fatalf("GetInviteRecords returned error: %v", err) + } + assertInviteRecordResponse(t, resp, types.InviteRecord{ + Role: inviteRecordRoleInviter, + GiftDays: 7, + OrderNo: "missing-order", + CreatedAt: 1779934600000, + }) + assertInviteRecordsExpectations(t, mock) +} + +func newInviteRecordsTestSvc(t *testing.T) (*svc.ServiceContext, 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 &svc.ServiceContext{DB: db}, mock, func() { + _ = sqlDB.Close() + } +} + +func inviteRecordsContext(userId, refererId int64) context.Context { + return context.WithValue(context.Background(), constant.CtxKeyUser, &modeluser.User{ + Id: userId, + RefererId: refererId, + }) +} + +func assertInviteRecordResponse(t *testing.T, resp *types.GetInviteRecordsResponse, want types.InviteRecord) { + t.Helper() + if resp == nil { + t.Fatal("response is nil") + } + if resp.Total != 1 { + t.Fatalf("Total = %d, want 1", resp.Total) + } + if len(resp.List) != 1 { + t.Fatalf("len(List) = %d, want 1", len(resp.List)) + } + if got := resp.List[0]; got != want { + t.Fatalf("record = %+v, want %+v", got, want) + } +} + +func assertInviteRecordsExpectations(t *testing.T, mock sqlmock.Sqlmock) { + t.Helper() + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} diff --git a/internal/logic/public/user/getInviteSalesLogic.go b/internal/logic/public/user/getInviteSalesLogic.go deleted file mode 100644 index aefcf25..0000000 --- a/internal/logic/public/user/getInviteSalesLogic.go +++ /dev/null @@ -1,145 +0,0 @@ -package user - -import ( - "context" - "fmt" - "hash/fnv" - "strconv" - - "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 GetInviteSalesLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetInviteSalesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteSalesLogic { - return &GetInviteSalesLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (resp *types.GetInviteSalesResponse, err error) { - // 1. Get current user - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - l.Errorw("[GetInviteSales] user not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - userId := u.Id - - // 2. Count total sales - var totalSales int64 - 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 IN ?", userId, []int{2, 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()), - logger.Field("user_id", userId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), - "count sales failed: %v", err.Error()) - } - - // 3. Pagination - if req.Page < 1 { - req.Page = 1 - } - if req.Size < 1 { - req.Size = 10 - } - if req.Size > 100 { - req.Size = 100 - } - offset := (req.Page - 1) * req.Size - - // 4. Get sales data - type OrderWithUser struct { - Amount int64 `gorm:"column:amount"` - UpdatedAt int64 `gorm:"column:updated_at"` - UserId int64 `gorm:"column:user_id"` - ProductName string `gorm:"column:product_name"` - Quantity int64 `gorm:"column:quantity"` - } - - var orderData []OrderWithUser - 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 IN ?", userId, []int{2, 5}) // status 2: Active, 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 - if err != nil { - l.Errorw("[GetInviteSales] query sales failed", - logger.Field("error", err.Error()), - logger.Field("user_id", userId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), - "query sales failed: %v", err.Error()) - } - - // 5. Get sales list - const HashSalt = "ppanel_invite_sales_v1" // Fixed Key - var list []types.InvitedUserSale - for _, order := range orderData { - // Calculate unique numeric hash (FNV-64a) - h := fnv.New64a() - h.Write([]byte(HashSalt)) - h.Write([]byte(strconv.FormatInt(order.UserId, 10))) - // Truncate to 10 digits using modulo 10^10 - hashVal := h.Sum64() % 10000000000 - userHashStr := fmt.Sprintf("%010d", hashVal) - - // Format product name: prefer subscribe name, fallback to quantity-based label - productName := order.ProductName - if productName == "" { - productName = fmt.Sprintf("%d天VPN服务", order.Quantity) - if order.Quantity <= 0 { - productName = "VPN服务" - } - } - - list = append(list, types.InvitedUserSale{ - Amount: float64(order.Amount) / 100.0, // Convert cents to dollars - UpdatedAt: order.UpdatedAt, - UserHash: userHashStr, - ProductName: productName, - }) - } - - return &types.GetInviteSalesResponse{ - Total: totalSales, - List: list, - }, nil -} diff --git a/internal/types/types.go b/internal/types/types.go index 59f6361..e2e65df 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1327,16 +1327,16 @@ type GetGroupHistoryResponse struct { List []GroupHistory `json:"list"` } -type GetInviteSalesRequest struct { +type GetInviteRecordsRequest struct { Page int `form:"page"` Size int `form:"size"` StartTime int64 `form:"start_time"` EndTime int64 `form:"end_time"` } -type GetInviteSalesResponse struct { - Total int64 `json:"total"` - List []InvitedUserSale `json:"list"` +type GetInviteRecordsResponse struct { + Total int64 `json:"total"` + List []InviteRecord `json:"list"` } type GetLoginLogRequest struct { @@ -1768,11 +1768,12 @@ type InviteConfig struct { GiftDays int64 `json:"gift_days"` } -type InvitedUserSale struct { - Amount float64 `json:"amount"` - UpdatedAt int64 `json:"updated_at"` - UserHash string `json:"user_hash"` - ProductName string `json:"product_name"` +type InviteRecord struct { + Role string `json:"role"` + PeerHash string `json:"peer_hash"` + GiftDays int64 `json:"gift_days"` + OrderNo string `json:"order_no"` + CreatedAt int64 `json:"created_at"` } type KickOfflineRequest struct { diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go index 8bd87b5..b42ca7b 100644 --- a/pkg/hash/hash.go +++ b/pkg/hash/hash.go @@ -3,15 +3,26 @@ package hash import ( "crypto/md5" "fmt" + "hash/fnv" + "strconv" "github.com/spaolacci/murmur3" ) +const invitePeerHashSalt = "ppanel_invite_" + "sales_v1" + // Hash returns the hash value of data. func Hash(data []byte) uint64 { return murmur3.Sum64(data) } +func InvitePeerHash(userId int64) string { + h := fnv.New64a() + _, _ = h.Write([]byte(invitePeerHashSalt)) + _, _ = h.Write([]byte(strconv.FormatInt(userId, 10))) + return fmt.Sprintf("%010d", h.Sum64()%10000000000) +} + // Md5 returns the md5 bytes of data. func Md5(data []byte) []byte { digest := md5.New() diff --git a/user.json b/user.json index a026dfb..d59d355 100644 --- a/user.json +++ b/user.json @@ -1183,15 +1183,15 @@ ] } }, - "/v1/public/user/invite_sales": { + "/v1/public/user/invite_records": { "get": { - "summary": "Get Invite Sales", - "operationId": "GetInviteSales", + "summary": "Get Invite Records", + "operationId": "GetInviteRecords", "responses": { "200": { "description": "A successful response.", "schema": { - "$ref": "#/definitions/GetInviteSalesResponse" + "$ref": "#/definitions/GetInviteRecordsResponse" } } }, @@ -3166,7 +3166,7 @@ "connection_records" ] }, - "GetInviteSalesRequest": { + "GetInviteRecordsRequest": { "type": "object", "properties": { "page": { @@ -3186,7 +3186,7 @@ "format": "int64" } }, - "title": "GetInviteSalesRequest", + "title": "GetInviteRecordsRequest", "required": [ "page", "size", @@ -3194,7 +3194,7 @@ "end_time" ] }, - "GetInviteSalesResponse": { + "GetInviteRecordsResponse": { "type": "object", "properties": { "total": { @@ -3204,11 +3204,11 @@ "list": { "type": "array", "items": { - "$ref": "#/definitions/InvitedUserSale" + "$ref": "#/definitions/InviteRecord" } } }, - "title": "GetInviteSalesResponse", + "title": "GetInviteRecordsResponse", "required": [ "total", "list" @@ -3558,30 +3558,34 @@ "gift_days" ] }, - "InvitedUserSale": { + "InviteRecord": { "type": "object", "properties": { - "amount": { - "type": "number", - "format": "double" + "role": { + "type": "string" }, - "updated_at": { + "peer_hash": { + "type": "string" + }, + "gift_days": { "type": "integer", "format": "int64" }, - "user_hash": { + "order_no": { "type": "string" }, - "product_name": { - "type": "string" + "created_at": { + "type": "integer", + "format": "int64" } }, - "title": "InvitedUserSale", + "title": "InviteRecord", "required": [ - "amount", - "updated_at", - "user_hash", - "product_name" + "role", + "peer_hash", + "gift_days", + "order_no", + "created_at" ] }, "MessageLog": {