c2d0db5875
inviteeAndInviterSet 是 map,按 range 收集到 slice 后顺序不固定, 传给 fillGiftBenefits 的 IN (?,?) 参数随之乱序,导致 sqlmock 按位置 匹配的单测 TestQueryBenefitsKeepsDirectInviteeGift 偶现失败(本地 10 次复现约 2-4 次)。 在收集后追加 slices.Sort,让生产代码本身的下游 SQL 参数稳定;同步把 测试期望改为升序。50 次重复 + race 全绿。 Co-authored-by: multica-agent <github@multica.ai>
214 lines
7.0 KiB
Go
214 lines
7.0 KiB
Go
package invite
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
|
|
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"`
|
|
SubscriptionUserId int64 `gorm:"column:subscription_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, subscription_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))
|
|
orderToSubscriptionUser := 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
|
|
if order.SubscriptionUserId > 0 && order.SubscriptionUserId != order.UserId {
|
|
orderToSubscriptionUser[order.OrderNo] = order.SubscriptionUserId
|
|
inviteeAndInviterSet[order.SubscriptionUserId] = struct{}{}
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
slices.Sort(inviteeAndInviterIds)
|
|
|
|
if err := fillCommissionBenefits(ctx, db, result, orderToInvitee, inviteeToInviter, orderNos, inviterIds); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := fillGiftBenefits(ctx, db, result, orderToInvitee, inviteeToInviter, orderToSubscriptionUser, 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, orderToSubscriptionUser map[string]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
|
|
case orderToSubscriptionUser[content.OrderNo]:
|
|
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
|
|
}
|