diff --git a/internal/handler/admin/invite/getInviteManageListHandler.go b/internal/handler/admin/invite/getInviteManageListHandler.go new file mode 100644 index 0000000..e98c2ec --- /dev/null +++ b/internal/handler/admin/invite/getInviteManageListHandler.go @@ -0,0 +1,25 @@ +package invite + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/invite" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get invite manage list +func GetInviteManageListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetInviteManageListRequest + _ = c.ShouldBind(&req) + if err := svcCtx.Validate(&req); err != nil { + result.ParamErrorResult(c, err) + return + } + + l := invite.NewGetInviteManageListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetInviteManageList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 09e9f7a..2fc733a 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -13,6 +13,7 @@ import ( adminCoupon "github.com/perfect-panel/server/internal/handler/admin/coupon" adminDocument "github.com/perfect-panel/server/internal/handler/admin/document" adminGroup "github.com/perfect-panel/server/internal/handler/admin/group" + adminInvite "github.com/perfect-panel/server/internal/handler/admin/invite" adminLog "github.com/perfect-panel/server/internal/handler/admin/log" adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing" adminOrder "github.com/perfect-panel/server/internal/handler/admin/order" @@ -194,6 +195,14 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminDocumentGroupRouter.GET("/list", adminDocument.GetDocumentListHandler(serverCtx)) } + adminInviteGroupRouter := router.Group("/v1/admin/invite") + adminInviteGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get invite manage list + adminInviteGroupRouter.GET("/list", adminInvite.GetInviteManageListHandler(serverCtx)) + } + adminGroupGroupRouter := router.Group("/v1/admin/group") adminGroupGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) diff --git a/internal/logic/admin/invite/benefits.go b/internal/logic/admin/invite/benefits.go new file mode 100644 index 0000000..69076b7 --- /dev/null +++ b/internal/logic/admin/invite/benefits.go @@ -0,0 +1,203 @@ +package invite + +import ( + "context" + + modellog "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type Benefits struct { + OrderCount int64 + HasPurchased bool + InviterCommission int64 + InviterGiftDays int64 + InviteeGiftDays int64 +} + +type InviteRelation struct { + InviteeId int64 + InviterId int64 +} + +type paidOrderRow struct { + UserId int64 `gorm:"column:user_id"` + OrderNo string `gorm:"column:order_no"` +} + +type systemLogRow struct { + ObjectID int64 `gorm:"column:object_id"` + Content string `gorm:"column:content"` +} + +func NormalizePage(page, size int) (int, int) { + if page < 1 { + page = 1 + } + if size < 1 { + size = 10 + } + if size > 100 { + size = 100 + } + return page, size +} + +func QueryBenefits(ctx context.Context, db *gorm.DB, relations []InviteRelation) (map[int64]Benefits, error) { + result := make(map[int64]Benefits, len(relations)) + if len(relations) == 0 { + return result, nil + } + + inviteeIds := make([]int64, 0, len(relations)) + inviteeToInviter := make(map[int64]int64, len(relations)) + for _, relation := range relations { + inviteeIds = append(inviteeIds, relation.InviteeId) + inviteeToInviter[relation.InviteeId] = relation.InviterId + result[relation.InviteeId] = Benefits{} + } + + var orderCounts []struct { + UserId int64 `gorm:"column:user_id"` + Cnt int64 `gorm:"column:cnt"` + } + if err := db.WithContext(ctx). + Table("`order`"). + Select("user_id, COUNT(*) as cnt"). + Where("user_id IN ? AND status IN ?", inviteeIds, []int{2, 5}). + Group("user_id"). + Scan(&orderCounts).Error; err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count invitee paid orders failed: %v", err) + } + for _, row := range orderCounts { + benefit := result[row.UserId] + benefit.OrderCount = row.Cnt + benefit.HasPurchased = row.Cnt > 0 + result[row.UserId] = benefit + } + + var paidOrders []paidOrderRow + if err := db.WithContext(ctx). + Table("`order`"). + Select("user_id, order_no"). + Where("user_id IN ? AND status IN ?", inviteeIds, []int{2, 5}). + Scan(&paidOrders).Error; err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query invitee paid orders failed: %v", err) + } + if len(paidOrders) == 0 { + return result, nil + } + + orderNos := make([]string, 0, len(paidOrders)) + orderToInvitee := make(map[string]int64, len(paidOrders)) + inviterIds := make([]int64, 0, len(relations)) + inviteeAndInviterSet := make(map[int64]struct{}, len(relations)*2) + for _, order := range paidOrders { + orderNos = append(orderNos, order.OrderNo) + orderToInvitee[order.OrderNo] = order.UserId + } + for _, relation := range relations { + inviterIds = append(inviterIds, relation.InviterId) + inviteeAndInviterSet[relation.InviteeId] = struct{}{} + inviteeAndInviterSet[relation.InviterId] = struct{}{} + } + inviteeAndInviterIds := make([]int64, 0, len(inviteeAndInviterSet)) + for userId := range inviteeAndInviterSet { + inviteeAndInviterIds = append(inviteeAndInviterIds, userId) + } + + if err := fillCommissionBenefits(ctx, db, result, orderToInvitee, inviteeToInviter, orderNos, inviterIds); err != nil { + return nil, err + } + if err := fillGiftBenefits(ctx, db, result, orderToInvitee, inviteeToInviter, orderNos, inviteeAndInviterIds); err != nil { + return nil, err + } + + return result, nil +} + +func fillCommissionBenefits(ctx context.Context, db *gorm.DB, benefits map[int64]Benefits, orderToInvitee map[string]int64, inviteeToInviter map[int64]int64, orderNos []string, inviterIds []int64) error { + var rows []systemLogRow + if err := db.WithContext(ctx). + Table("system_logs"). + Select("object_id, content"). + Where("type = ? AND object_id IN ? AND JSON_UNQUOTE(JSON_EXTRACT(content, '$.order_no')) IN ? AND JSON_EXTRACT(content, '$.type') IN ?", modellog.TypeCommission.Uint8(), inviterIds, orderNos, []int{int(modellog.CommissionTypePurchase), int(modellog.CommissionTypeRenewal)}). + Scan(&rows).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query invite commission logs failed: %v", err) + } + for _, row := range rows { + content := modellog.Commission{} + if err := content.Unmarshal([]byte(row.Content)); err != nil { + continue + } + inviteeId, ok := orderToInvitee[content.OrderNo] + if !ok || inviteeToInviter[inviteeId] != row.ObjectID { + continue + } + benefit := benefits[inviteeId] + benefit.InviterCommission += content.Amount + benefits[inviteeId] = benefit + } + return nil +} + +func fillGiftBenefits(ctx context.Context, db *gorm.DB, benefits map[int64]Benefits, orderToInvitee map[string]int64, inviteeToInviter map[int64]int64, orderNos []string, userIds []int64) error { + var rows []systemLogRow + if err := db.WithContext(ctx). + Table("system_logs"). + Select("object_id, content"). + Where("type = ? AND object_id IN ? AND JSON_UNQUOTE(JSON_EXTRACT(content, '$.order_no')) IN ?", modellog.TypeGift.Uint8(), userIds, orderNos). + Scan(&rows).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query invite gift logs failed: %v", err) + } + for _, row := range rows { + content := modellog.Gift{} + if err := content.Unmarshal([]byte(row.Content)); err != nil { + continue + } + if content.Type != modellog.GiftTypeIncrease { + continue + } + inviteeId, ok := orderToInvitee[content.OrderNo] + if !ok { + continue + } + benefit := benefits[inviteeId] + switch row.ObjectID { + case inviteeToInviter[inviteeId]: + benefit.InviterGiftDays += content.Amount + case inviteeId: + benefit.InviteeGiftDays += content.Amount + default: + continue + } + benefits[inviteeId] = benefit + } + return nil +} + +func QueryIdentifiers(ctx context.Context, db *gorm.DB, userIds []int64) (map[int64]string, error) { + identifiers := make(map[int64]string, len(userIds)) + if len(userIds) == 0 { + return identifiers, nil + } + + type identifierRow struct { + UserId int64 `gorm:"column:user_id"` + Identifier string `gorm:"column:identifier"` + } + var rows []identifierRow + if err := db.WithContext(ctx). + Table("user_auth_methods uam"). + Select("uam.user_id, uam.auth_identifier as identifier"). + Joins("JOIN (SELECT user_id, MIN(id) AS id FROM user_auth_methods WHERE user_id IN ? GROUP BY user_id) first_uam ON first_uam.id = uam.id", userIds). + Scan(&rows).Error; err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user identifiers failed: %v", err) + } + for _, row := range rows { + identifiers[row.UserId] = row.Identifier + } + return identifiers, nil +} diff --git a/internal/logic/admin/invite/getInviteManageListLogic.go b/internal/logic/admin/invite/getInviteManageListLogic.go new file mode 100644 index 0000000..a4be828 --- /dev/null +++ b/internal/logic/admin/invite/getInviteManageListLogic.go @@ -0,0 +1,117 @@ +package invite + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type GetInviteManageListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetInviteManageListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteManageListLogic { + return &GetInviteManageListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetInviteManageListLogic) GetInviteManageList(req *types.GetInviteManageListRequest) (resp *types.GetInviteManageListResponse, err error) { + req.Page, req.Size = NormalizePage(req.Page, req.Size) + + type inviteRow struct { + InviteeId int64 `gorm:"column:invitee_id"` + InviteeAvatar string `gorm:"column:invitee_avatar"` + InviteeEnable bool `gorm:"column:invitee_enable"` + InvitedAt int64 `gorm:"column:invited_at"` + InviterId int64 `gorm:"column:inviter_id"` + } + + baseQuery := applyInviteManageFilters(l.svcCtx.DB.WithContext(l.ctx). + Table("user invitee"). + Where("invitee.referer_id > 0 AND invitee.deleted_at IS NULL"), req) + + var total int64 + if err = baseQuery.Count(&total).Error; err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count invite manage records failed: %v", err) + } + + var rows []inviteRow + if err = applyInviteManageFilters(l.svcCtx.DB.WithContext(l.ctx). + Table("user invitee"). + Select("invitee.id as invitee_id, invitee.avatar as invitee_avatar, invitee.enable as invitee_enable, UNIX_TIMESTAMP(invitee.created_at) as invited_at, invitee.referer_id as inviter_id"). + Where("invitee.referer_id > 0 AND invitee.deleted_at IS NULL"), req). + Order("invitee.created_at DESC"). + Limit(req.Size). + Offset((req.Page - 1) * req.Size). + Scan(&rows).Error; err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query invite manage records failed: %v", err) + } + + relations := make([]InviteRelation, 0, len(rows)) + userIds := make([]int64, 0, len(rows)*2) + for _, row := range rows { + relations = append(relations, InviteRelation{InviteeId: row.InviteeId, InviterId: row.InviterId}) + userIds = append(userIds, row.InviteeId, row.InviterId) + } + + benefits, err := QueryBenefits(l.ctx, l.svcCtx.DB, relations) + if err != nil { + return nil, err + } + identifiers, err := QueryIdentifiers(l.ctx, l.svcCtx.DB, userIds) + if err != nil { + return nil, err + } + + list := make([]types.InviteManageRecord, 0, len(rows)) + for _, row := range rows { + benefit := benefits[row.InviteeId] + list = append(list, types.InviteManageRecord{ + InviterId: row.InviterId, + InviterIdentifier: identifiers[row.InviterId], + InviteeId: row.InviteeId, + InviteeIdentifier: identifiers[row.InviteeId], + InviteeAvatar: row.InviteeAvatar, + InviteeEnable: row.InviteeEnable, + InvitedAt: row.InvitedAt, + OrderCount: benefit.OrderCount, + HasPurchased: benefit.HasPurchased, + InviterCommission: benefit.InviterCommission, + InviterGiftDays: benefit.InviterGiftDays, + InviteeGiftDays: benefit.InviteeGiftDays, + }) + } + + return &types.GetInviteManageListResponse{ + Total: total, + List: list, + }, nil +} + +func applyInviteManageFilters(db *gorm.DB, req *types.GetInviteManageListRequest) *gorm.DB { + if req.InviterId > 0 { + db = db.Where("invitee.referer_id = ?", req.InviterId) + } + if req.InviteeId > 0 { + db = db.Where("invitee.id = ?", req.InviteeId) + } + if req.Search != "" { + search := "%" + req.Search + "%" + db = db.Where( + "(EXISTS (SELECT 1 FROM user_auth_methods inviter_auth WHERE inviter_auth.user_id = invitee.referer_id AND inviter_auth.auth_identifier LIKE ?) OR EXISTS (SELECT 1 FROM user_auth_methods invitee_auth WHERE invitee_auth.user_id = invitee.id AND invitee_auth.auth_identifier LIKE ?))", + search, + search, + ) + } + return db +} diff --git a/internal/logic/admin/user/getAdminUserInviteListLogic.go b/internal/logic/admin/user/getAdminUserInviteListLogic.go index 4d0b12e..8d1aca2 100644 --- a/internal/logic/admin/user/getAdminUserInviteListLogic.go +++ b/internal/logic/admin/user/getAdminUserInviteListLogic.go @@ -3,11 +3,13 @@ package user import ( "context" + adminInvite "github.com/perfect-panel/server/internal/logic/admin/invite" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" + "gorm.io/gorm" ) type GetAdminUserInviteListLogic struct { @@ -25,15 +27,7 @@ func NewGetAdminUserInviteListLogic(ctx context.Context, svcCtx *svc.ServiceCont } func (l *GetAdminUserInviteListLogic) GetAdminUserInviteList(req *types.GetAdminUserInviteListRequest) (resp *types.GetAdminUserInviteListResponse, err error) { - if req.Page < 1 { - req.Page = 1 - } - if req.Size < 1 { - req.Size = 10 - } - if req.Size > 100 { - req.Size = 100 - } + req.Page, req.Size = adminInvite.NormalizePage(req.Page, req.Size) type InvitedUser struct { Id int64 `gorm:"column:id"` @@ -44,19 +38,19 @@ func (l *GetAdminUserInviteListLogic) GetAdminUserInviteList(req *types.GetAdmin } var total int64 - baseQuery := l.svcCtx.DB.WithContext(l.ctx). + baseQuery := applyAdminUserInviteFilters(l.svcCtx.DB.WithContext(l.ctx). Table("user u"). - Where("u.referer_id = ? AND u.deleted_at IS NULL", req.UserId) + Where("u.referer_id = ? AND u.deleted_at IS NULL", req.UserId), req) if err = baseQuery.Count(&total).Error; err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count invited users failed: %v", err) } var rows []InvitedUser - err = l.svcCtx.DB.WithContext(l.ctx). + err = applyAdminUserInviteFilters(l.svcCtx.DB.WithContext(l.ctx). Table("user u"). Select("u.id, u.avatar, u.enable, UNIX_TIMESTAMP(u.created_at) as created_at, COALESCE((SELECT uam.auth_identifier FROM user_auth_methods uam WHERE uam.user_id = u.id ORDER BY uam.id ASC LIMIT 1), '') as identifier"). - Where("u.referer_id = ? AND u.deleted_at IS NULL", req.UserId). + Where("u.referer_id = ? AND u.deleted_at IS NULL", req.UserId), req). Order("u.created_at DESC"). Limit(req.Size). Offset((req.Page - 1) * req.Size). @@ -65,14 +59,29 @@ func (l *GetAdminUserInviteListLogic) GetAdminUserInviteList(req *types.GetAdmin return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query invited users failed: %v", err) } + relations := make([]adminInvite.InviteRelation, 0, len(rows)) + for _, r := range rows { + relations = append(relations, adminInvite.InviteRelation{InviteeId: r.Id, InviterId: req.UserId}) + } + benefits, err := adminInvite.QueryBenefits(l.ctx, l.svcCtx.DB, relations) + if err != nil { + return nil, err + } + list := make([]types.AdminInvitedUser, 0, len(rows)) for _, r := range rows { + benefit := benefits[r.Id] list = append(list, types.AdminInvitedUser{ - Id: r.Id, - Avatar: r.Avatar, - Identifier: r.Identifier, - Enable: r.Enable, - CreatedAt: r.CreatedAt, + Id: r.Id, + Avatar: r.Avatar, + Identifier: r.Identifier, + Enable: r.Enable, + CreatedAt: r.CreatedAt, + OrderCount: benefit.OrderCount, + HasPurchased: benefit.HasPurchased, + InviterCommission: benefit.InviterCommission, + InviterGiftDays: benefit.InviterGiftDays, + InviteeGiftDays: benefit.InviteeGiftDays, }) } @@ -81,3 +90,16 @@ func (l *GetAdminUserInviteListLogic) GetAdminUserInviteList(req *types.GetAdmin List: list, }, nil } + +func applyAdminUserInviteFilters(db *gorm.DB, req *types.GetAdminUserInviteListRequest) *gorm.DB { + if req.Search != "" { + db = db.Where("EXISTS (SELECT 1 FROM user_auth_methods uam WHERE uam.user_id = u.id AND uam.auth_identifier LIKE ?)", "%"+req.Search+"%") + } + if req.Enable != nil { + db = db.Where("u.enable = ?", *req.Enable) + } + if req.UserIdSearch > 0 { + db = db.Where("u.id = ?", req.UserIdSearch) + } + return db +} diff --git a/internal/types/types.go b/internal/types/types.go index c1a599a..59f6361 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -3800,17 +3800,25 @@ type GetAdminUserInviteStatsResponse struct { } type GetAdminUserInviteListRequest struct { - UserId int64 `form:"user_id" validate:"required"` - Page int `form:"page"` - Size int `form:"size"` + UserId int64 `form:"user_id" validate:"required"` + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search"` + Enable *int `form:"enable"` + UserIdSearch int64 `form:"user_id_search"` } type AdminInvitedUser struct { - Id int64 `json:"id"` - Avatar string `json:"avatar"` - Identifier string `json:"identifier"` - Enable bool `json:"enable"` - CreatedAt int64 `json:"created_at"` + Id int64 `json:"id"` + Avatar string `json:"avatar"` + Identifier string `json:"identifier"` + Enable bool `json:"enable"` + CreatedAt int64 `json:"created_at"` + OrderCount int64 `json:"order_count"` + HasPurchased bool `json:"has_purchased"` + InviterCommission int64 `json:"inviter_commission"` + InviterGiftDays int64 `json:"inviter_gift_days"` + InviteeGiftDays int64 `json:"invitee_gift_days"` } type GetAdminUserInviteListResponse struct { @@ -3818,6 +3826,34 @@ type GetAdminUserInviteListResponse struct { List []AdminInvitedUser `json:"list"` } +type GetInviteManageListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search"` + InviterId int64 `form:"inviter_id"` + InviteeId int64 `form:"invitee_id"` +} + +type InviteManageRecord struct { + InviterId int64 `json:"inviter_id"` + InviterIdentifier string `json:"inviter_identifier"` + InviteeId int64 `json:"invitee_id"` + InviteeIdentifier string `json:"invitee_identifier"` + InviteeAvatar string `json:"invitee_avatar"` + InviteeEnable bool `json:"invitee_enable"` + InvitedAt int64 `json:"invited_at"` + OrderCount int64 `json:"order_count"` + HasPurchased bool `json:"has_purchased"` + InviterCommission int64 `json:"inviter_commission"` + InviterGiftDays int64 `json:"inviter_gift_days"` + InviteeGiftDays int64 `json:"invitee_gift_days"` +} + +type GetInviteManageListResponse struct { + Total int64 `json:"total"` + List []InviteManageRecord `json:"list"` +} + type GetLogMessageRawRequest struct { Id int64 `form:"id" validate:"required"` }