新功能(#102): 新增邀请记录接口并删除旧邀请销售接口
Build docker and publish / build (20.15.1) (push) Failing after 10m56s
Build docker and publish / build (20.15.1) (pull_request) Successful in 8m50s

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-27 22:50:16 -07:00
parent f452f80100
commit 197fed7d12
11 changed files with 426 additions and 197 deletions
+13 -13
View File
@@ -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
}
+1
View File
@@ -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
+3
View File
@@ -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=
@@ -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)
}
}
+2 -3
View File
@@ -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))
@@ -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
}
@@ -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)
}
}
@@ -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
}
+10 -9
View File
@@ -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 {
+11
View File
@@ -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()
+26 -22
View File
@@ -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": {