Files
hi-server/internal/logic/admin/invite/benefits.go
T
shanshanzhong147 c540091ef9 修复(#143): 邀请权益查询排序,消除 map 迭代序 flake
Closes HIF-143. 让 `inviteeAndInviterIds` 在 append 后 `slices.Sort` 升序,让 sqlmock IN 参数匹配稳定,同时让生产 SQL EXPLAIN 计划稳定。修复 GitHub Actions Deploy Staging 自 2026-06-03 03:51 起连续 8 次 `go test ./...` 失败问题。

测试: go test -count=10 修复前 4/10 fail, 修复后 30/30 pass.
改动: 2 files, +4/-2 (internal/logic/admin/invite/{benefits.go,benefits_test.go})
2026-06-02 22:04:16 -07:00

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
}