Files
hfb_sys/backend/internal/modules/invitation/repository.go
T
yml2213 0d6a7786a4 feat(admin): 用户管理新增用户明细抽屉,聚合资金与交易全链路数据
- 用户管理操作列新增「明细」入口,抽屉按权限提供 5 个 Tab:钱包流水、订单记录、提现记录、邀请返利、支付记录
- 后端:后台订单列表新增 user_id 精确过滤(owner_id / renter_id),邀请关系与返利流水新增 inviter_id / invitee_id 过滤
- 修复:返利流水的「被邀请人手机号」筛选此前后端未实现,一直无效
- 资金流水页、支付记录页支持 ?user_id= 预填,可从明细直达
- 顺带将 invitation/dto.go、repository.go 中两处既有缩进错位按 gofmt 规整
2026-10-13 14:49:36 +08:00

904 lines
30 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package invitation
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"math"
"strings"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/wallet"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
ErrDependencyUnavailable = errors.New("dependency unavailable")
ErrInvalidInviteCode = errors.New("invalid invite code")
ErrSelfInviteNotAllowed = errors.New("self invite not allowed")
ErrAlreadyInvited = errors.New("already invited")
ErrInviterNotVerified = errors.New("inviter not realname verified")
ErrConfigNotFound = errors.New("invitation rebate config not found")
)
const (
StatusPending = "pending"
StatusActive = "active"
StatusInvalid = "invalid"
CycleFirstMonth = "first_month"
CycleSecondMonth = "second_month"
CycleSubsequent = "subsequent"
DefaultFirstMonthRate = 0.6000
DefaultSecondMonthRate = 0.3000
DefaultSubsequentRate = 0.1500
)
type Repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
// GenerateUniqueInviteCode 生成 7 位小写字母数字邀请码(防碰撞重试)
func (r *Repository) GenerateUniqueInviteCode(ctx context.Context) (string, error) {
if r == nil || r.db == nil {
return "", ErrDependencyUnavailable
}
for i := 0; i < 10; i++ {
bytes := make([]byte, 4)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
code := hex.EncodeToString(bytes)[:7] // 7 位如 0d76234
var count int64
if err := r.db.WithContext(ctx).Model(&model.User{}).Where("invite_code = ?", code).Count(&count).Error; err != nil {
return "", err
}
if count == 0 {
return code, nil
}
}
return "", errors.New("failed to generate unique invite code")
}
// EnsureUserInviteCode 为已实名认证的用户确保拥有邀请码
func (r *Repository) EnsureUserInviteCode(ctx context.Context, userID uint64) (string, error) {
if r == nil || r.db == nil {
return "", ErrDependencyUnavailable
}
var user model.User
if err := r.db.WithContext(ctx).First(&user, userID).Error; err != nil {
return "", err
}
if user.InviteCode != nil && *user.InviteCode != "" {
return *user.InviteCode, nil
}
code, err := r.GenerateUniqueInviteCode(ctx)
if err != nil {
return "", err
}
if err := r.db.WithContext(ctx).Model(&user).Update("invite_code", code).Error; err != nil {
return "", err
}
return code, nil
}
// OnUserRealnameVerified 用户实名认证通过时的事务处理:
// 1. 若自身无邀请码,自动生成专属邀请码;
// 2. 若作为被邀请人,将待生效绑定关系置为 active,以当前时刻记录 verified_at(首月计时起点)。
func (r *Repository) OnUserRealnameVerified(ctx context.Context, userID uint64) error {
if r == nil || r.db == nil {
return ErrDependencyUnavailable
}
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var user model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
return err
}
now := time.Now()
// 1. 生成自身邀请码
if user.InviteCode == nil || *user.InviteCode == "" {
for i := 0; i < 10; i++ {
bytes := make([]byte, 4)
if _, err := rand.Read(bytes); err != nil {
return err
}
code := hex.EncodeToString(bytes)[:7]
var count int64
if err := tx.Model(&model.User{}).Where("invite_code = ?", code).Count(&count).Error; err != nil {
return err
}
if count == 0 {
user.InviteCode = &code
break
}
}
}
// 2. 检查是否有作为被邀请人的挂起记录
var invitation model.UserInvitation
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("invitee_id = ?", userID).
First(&invitation).Error
if err == nil {
if invitation.Status == StatusPending {
invitation.Status = StatusActive
invitation.VerifiedAt = &now
if err := tx.Save(&invitation).Error; err != nil {
return err
}
user.InviteVerifiedAt = &now
}
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
return tx.Save(&user).Error
})
}
// GetInviteCodeInfo 查询邀请码对应邀请人简要信息(用于邀请注册落地页)
func (r *Repository) GetInviteCodeInfo(ctx context.Context, code string) (*InviteCodeInfoDTO, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
cleanCode := strings.TrimSpace(code)
if cleanCode == "" {
return &InviteCodeInfoDTO{Valid: false}, nil
}
var user model.User
if err := r.db.WithContext(ctx).Where("invite_code = ?", cleanCode).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &InviteCodeInfoDTO{Valid: false, InviteCode: cleanCode}, nil
}
return nil, err
}
if user.RealnameStatus != "verified" || user.Status != "active" {
return &InviteCodeInfoDTO{Valid: false, InviteCode: cleanCode}, nil
}
nickname := user.Nickname
if nickname == "" && len(user.Phone) >= 11 {
nickname = "用户" + user.Phone[7:]
}
return &InviteCodeInfoDTO{
Valid: true,
InviteCode: cleanCode,
InviterNickname: nickname,
InviterAvatar: user.AvatarURL,
}, nil
}
// BindInvitation 新用户注册时绑定邀请关系
func (r *Repository) BindInvitation(ctx context.Context, inviteeID uint64, code string, siteID uint64, siteCode ...string) error {
if r == nil || r.db == nil {
return ErrDependencyUnavailable
}
cleanCode := strings.TrimSpace(code)
if cleanCode == "" {
return nil
}
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if siteID == 0 && len(siteCode) > 0 && strings.TrimSpace(siteCode[0]) != "" {
var ds model.DistributionSite
if err := tx.Where("site_code = ?", strings.TrimSpace(siteCode[0])).First(&ds).Error; err == nil {
siteID = ds.ID
}
}
var inviter model.User
if err := tx.Where("invite_code = ?", cleanCode).First(&inviter).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrInvalidInviteCode
}
return err
}
if inviter.ID == inviteeID {
return ErrSelfInviteNotAllowed
}
if inviter.RealnameStatus != "verified" {
return ErrInviterNotVerified
}
var count int64
if err := tx.Model(&model.UserInvitation{}).Where("invitee_id = ?", inviteeID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return ErrAlreadyInvited
}
var invitee model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&invitee, inviteeID).Error; err != nil {
return err
}
status := StatusPending
var verifiedAt *time.Time
if invitee.RealnameStatus == "verified" {
status = StatusActive
now := time.Now()
verifiedAt = &now
invitee.InviteVerifiedAt = &now
}
invitee.InvitedByID = inviter.ID
if err := tx.Save(&invitee).Error; err != nil {
return err
}
invitation := model.UserInvitation{
InviterID: inviter.ID,
InviteeID: inviteeID,
DistributionSiteID: siteID,
InviteCode: cleanCode,
Status: status,
VerifiedAt: verifiedAt,
}
return tx.Create(&invitation).Error
})
}
// GetRebateConfig 获取指定站点的返利比例配置(优先站点,回退至主站默认)
func (r *Repository) GetRebateConfig(ctx context.Context, siteID uint64) (*model.InvitationRebateConfig, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
var cfg model.InvitationRebateConfig
if siteID > 0 {
err := r.db.WithContext(ctx).Where("distribution_site_id = ?", siteID).First(&cfg).Error
if err == nil {
return &cfg, nil
}
}
// 回退主站配置 (distribution_site_id = 0)
err := r.db.WithContext(ctx).Where("distribution_site_id = 0").First(&cfg).Error
if err == nil {
return &cfg, nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
// 返回默认硬编码
return &model.InvitationRebateConfig{
DistributionSiteID: 0,
FirstMonthRate: DefaultFirstMonthRate,
SecondMonthRate: DefaultSecondMonthRate,
SubsequentRate: DefaultSubsequentRate,
Enabled: true,
}, nil
}
return nil, err
}
// UpdateRebateConfig 更新站点返利比例配置
func (r *Repository) UpdateRebateConfig(ctx context.Context, siteID uint64, req UpdateConfigRequest) (*model.InvitationRebateConfig, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
var cfg model.InvitationRebateConfig
err := r.db.WithContext(ctx).Where("distribution_site_id = ?", siteID).First(&cfg).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
if errors.Is(err, gorm.ErrRecordNotFound) {
cfg = model.InvitationRebateConfig{
DistributionSiteID: siteID,
FirstMonthRate: req.FirstMonthRate,
SecondMonthRate: req.SecondMonthRate,
SubsequentRate: req.SubsequentRate,
Enabled: true,
}
if req.Enabled != nil {
cfg.Enabled = *req.Enabled
}
if err := r.db.WithContext(ctx).Select("DistributionSiteID", "FirstMonthRate", "SecondMonthRate", "SubsequentRate", "Enabled").Create(&cfg).Error; err != nil {
return nil, err
}
return &cfg, nil
}
cfg.FirstMonthRate = req.FirstMonthRate
cfg.SecondMonthRate = req.SecondMonthRate
cfg.SubsequentRate = req.SubsequentRate
if req.Enabled != nil {
cfg.Enabled = *req.Enabled
}
if err := r.db.WithContext(ctx).Select("FirstMonthRate", "SecondMonthRate", "SubsequentRate", "Enabled").Save(&cfg).Error; err != nil {
return nil, err
}
return &cfg, nil
}
// CalculateRebateRate 根据被邀请人实名生效时间和配置计算适用的返利比例与阶段
func CalculateRebateRate(verifiedAt time.Time, now time.Time, cfg *model.InvitationRebateConfig) (float64, string, int) {
days := int(now.Sub(verifiedAt).Hours() / 24)
if days < 0 {
days = 0
}
firstRate := DefaultFirstMonthRate
secondRate := DefaultSecondMonthRate
subsequentRate := DefaultSubsequentRate
if cfg != nil {
firstRate = cfg.FirstMonthRate
secondRate = cfg.SecondMonthRate
subsequentRate = cfg.SubsequentRate
}
if days <= 30 {
return firstRate, CycleFirstMonth, days
} else if days <= 60 {
return secondRate, CycleSecondMonth, days
}
return subsequentRate, CycleSubsequent, days
}
// ProcessOrderInvitationRebate 在订单终态结算时计算并分发买卖双边邀请返利。
func (r *Repository) ProcessOrderInvitationRebate(ctx context.Context, tx *gorm.DB, orderID uint64) error {
var order model.RentalOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
return err
}
if order.Status != "completed" && order.SettlementStatus != "settled" {
return nil
}
orderGrossProfitCent := order.RentAmountCent - order.OwnerRentAmountCent
if orderGrossProfitCent <= 0 {
return nil
}
// 判定跨站或分销拆分:查 distribution_order_parties
var parties []model.DistributionOrderParty
_ = tx.Where("order_id = ?", order.ID).Find(&parties).Error
var distributorReceiveCent int64
var platformSellCent int64
var distributorSiteID uint64
for _, p := range parties {
amount := p.ProfitAmountCent
if amount <= 0 {
amount = p.IncomeAmountCent
}
if p.PartyRole == "distributor_receive" || p.PartyRole == "distributor_a" {
distributorReceiveCent += amount
if p.PartyType == "site" && p.PartyID > 0 {
distributorSiteID = p.PartyID
} else if p.SiteID > 0 {
distributorSiteID = p.SiteID
}
} else if p.PartyRole == "platform_sell" || p.PartyRole == "service_fee" || p.PartyRole == "platform_receive" || p.PartyRole == "distributor_sell" {
platformSellCent += amount
}
}
now := time.Now()
// 1. 买家端处理(租客 RenterID)
var buyerInvitation model.UserInvitation
if err := tx.Where("invitee_id = ? AND status = ?", order.RenterID, StatusActive).First(&buyerInvitation).Error; err == nil {
if buyerInvitation.VerifiedAt != nil {
var inviter model.User
if err := tx.First(&inviter, buyerInvitation.InviterID).Error; err == nil && inviter.RealnameStatus == "verified" && inviter.ID != order.RenterID {
// 买家端基数:若有分销售卖拆分按 platformSellCent,否则按整单毛利对半拆
var buyerBasisCent int64
var buyerSiteID uint64
if platformSellCent > 0 {
buyerBasisCent = int64(math.Floor(float64(platformSellCent) / 2.0))
buyerSiteID = 0
} else {
buyerBasisCent = int64(math.Floor(float64(orderGrossProfitCent) / 2.0))
if order.DistributionSiteID != nil {
buyerSiteID = *order.DistributionSiteID
}
}
if buyerBasisCent > 0 {
cfg, _ := r.GetRebateConfig(ctx, buyerSiteID)
if cfg == nil || cfg.Enabled {
rate, cycle, days := CalculateRebateRate(*buyerInvitation.VerifiedAt, now, cfg)
rebateCent := int64(math.Round(float64(buyerBasisCent) * rate))
if rebateCent > 0 {
ledgerNo := fmt.Sprintf("REB-B-%s-%d", order.OrderNo, buyerInvitation.InviterID)
ledger := model.InvitationRebateLedger{
OrderID: order.ID,
OrderNo: order.OrderNo,
DistributionSiteID: buyerSiteID,
InviterID: buyerInvitation.InviterID,
InviteeID: order.RenterID,
TradeRole: "buyer",
OrderProfitBasisCent: orderGrossProfitCent,
SingleSideBasisCent: buyerBasisCent,
RateApplied: rate,
DaysSinceVerified: days,
CycleStage: cycle,
RebateAmountCent: rebateCent,
WalletLedgerNo: ledgerNo,
Status: "settled",
SettledAt: now,
}
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&ledger).Error; err == nil && ledger.ID > 0 {
_ = wallet.AppendEntries(tx, wallet.Entry{
UserID: buyerInvitation.InviterID,
DistributionSiteID: buyerSiteID,
Direction: "in",
AmountCent: rebateCent,
BalanceType: "available",
BizType: "invite_rebate",
BizNo: order.OrderNo,
Remark: fmt.Sprintf("邀请买家成交返利(订单 %s,%s)", order.OrderNo, cycleLabel(cycle)),
})
}
}
}
}
}
}
}
// 2. 卖家端处理(号主 OwnerID)
var sellerInvitation model.UserInvitation
if err := tx.Where("invitee_id = ? AND status = ?", order.OwnerID, StatusActive).First(&sellerInvitation).Error; err == nil {
if sellerInvitation.VerifiedAt != nil {
var inviter model.User
if err := tx.First(&inviter, sellerInvitation.InviterID).Error; err == nil && inviter.RealnameStatus == "verified" && inviter.ID != order.OwnerID {
// 卖家端基数:若有分站收号拆分按 distributorReceiveCent(从分站出),否则按整单毛利对半拆
var sellerBasisCent int64
var sellerSiteID uint64
if distributorReceiveCent > 0 {
sellerBasisCent = int64(math.Floor(float64(distributorReceiveCent) / 2.0))
sellerSiteID = distributorSiteID
} else {
sellerBasisCent = int64(math.Floor(float64(orderGrossProfitCent) / 2.0))
if order.DistributionSiteID != nil {
sellerSiteID = *order.DistributionSiteID
}
}
if sellerBasisCent > 0 {
cfg, _ := r.GetRebateConfig(ctx, sellerSiteID)
if cfg == nil || cfg.Enabled {
rate, cycle, days := CalculateRebateRate(*sellerInvitation.VerifiedAt, now, cfg)
rebateCent := int64(math.Round(float64(sellerBasisCent) * rate))
if rebateCent > 0 {
ledgerNo := fmt.Sprintf("REB-S-%s-%d", order.OrderNo, sellerInvitation.InviterID)
ledger := model.InvitationRebateLedger{
OrderID: order.ID,
OrderNo: order.OrderNo,
DistributionSiteID: sellerSiteID,
InviterID: sellerInvitation.InviterID,
InviteeID: order.OwnerID,
TradeRole: "seller",
OrderProfitBasisCent: orderGrossProfitCent,
SingleSideBasisCent: sellerBasisCent,
RateApplied: rate,
DaysSinceVerified: days,
CycleStage: cycle,
RebateAmountCent: rebateCent,
WalletLedgerNo: ledgerNo,
Status: "settled",
SettledAt: now,
}
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&ledger).Error; err == nil && ledger.ID > 0 {
_ = wallet.AppendEntries(tx, wallet.Entry{
UserID: sellerInvitation.InviterID,
DistributionSiteID: sellerSiteID,
Direction: "in",
AmountCent: rebateCent,
BalanceType: "available",
BizType: "invite_rebate",
BizNo: order.OrderNo,
Remark: fmt.Sprintf("邀请卖家成交返利(订单 %s,%s)", order.OrderNo, cycleLabel(cycle)),
})
}
}
}
}
}
}
}
return nil
}
func cycleLabel(cycle string) string {
switch cycle {
case CycleFirstMonth:
return "首月高额奖励 60%"
case CycleSecondMonth:
return "次月奖励 30%"
default:
return "长期奖励 15%"
}
}
// GetUserSummary 获取用户个人中心的邀请返利数据看板
func (r *Repository) GetUserSummary(ctx context.Context, userID uint64, siteID uint64) (*UserInvitationSummaryDTO, error) {
if r == nil || r.db == nil {
return nil, ErrDependencyUnavailable
}
var user model.User
if err := r.db.WithContext(ctx).First(&user, userID).Error; err != nil {
return nil, err
}
cfg, _ := r.GetRebateConfig(ctx, siteID)
firstRate := DefaultFirstMonthRate
secondRate := DefaultSecondMonthRate
subsequentRate := DefaultSubsequentRate
if cfg != nil {
firstRate = cfg.FirstMonthRate
secondRate = cfg.SecondMonthRate
subsequentRate = cfg.SubsequentRate
}
code := ""
hasCode := false
if user.InviteCode != nil && *user.InviteCode != "" {
code = *user.InviteCode
hasCode = true
} else if user.RealnameStatus == "verified" {
c, err := r.EnsureUserInviteCode(ctx, userID)
if err == nil {
code = c
hasCode = true
}
}
var totalCount int64
var activeCount int64
var pendingCount int64
_ = r.db.WithContext(ctx).Model(&model.UserInvitation{}).Where("inviter_id = ?", userID).Count(&totalCount).Error
_ = r.db.WithContext(ctx).Model(&model.UserInvitation{}).Where("inviter_id = ? AND status = ?", userID, StatusActive).Count(&activeCount).Error
_ = r.db.WithContext(ctx).Model(&model.UserInvitation{}).Where("inviter_id = ? AND status = ?", userID, StatusPending).Count(&pendingCount).Error
var totalRebateCent int64
_ = r.db.WithContext(ctx).Model(&model.InvitationRebateLedger{}).
Where("inviter_id = ? AND status = 'settled'", userID).
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(&totalRebateCent).Error
now := time.Now()
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
var currentMonthEstCent int64
_ = r.db.WithContext(ctx).Model(&model.InvitationRebateLedger{}).
Where("inviter_id = ? AND status = 'settled' AND settled_at >= ?", userID, monthStart).
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(&currentMonthEstCent).Error
return &UserInvitationSummaryDTO{
HasInviteCode: hasCode,
InviteCode: code,
InviteLink: fmt.Sprintf("/i/%s", code),
IsRealnameVerified: user.RealnameStatus == "verified",
TotalInvitedCount: totalCount,
ActiveInvitedCount: activeCount,
PendingInvitedCount: pendingCount,
TotalRebateCent: totalRebateCent,
CurrentMonthEstCent: currentMonthEstCent,
FirstMonthRate: firstRate,
SecondMonthRate: secondRate,
SubsequentRate: subsequentRate,
}, nil
}
// ListMyInvitedUsers 获取我邀请的好友列表
func (r *Repository) ListMyInvitedUsers(ctx context.Context, userID uint64, page, pageSize int) ([]InvitedUserRecordDTO, int64, error) {
if r == nil || r.db == nil {
return nil, 0, ErrDependencyUnavailable
}
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 50 {
pageSize = 20
}
var total int64
db := r.db.WithContext(ctx).Model(&model.UserInvitation{}).Where("inviter_id = ?", userID)
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []model.UserInvitation
offset := (page - 1) * pageSize
if err := db.Order("id DESC").Offset(offset).Limit(pageSize).Find(&rows).Error; err != nil {
return nil, 0, err
}
result := make([]InvitedUserRecordDTO, 0, len(rows))
now := time.Now()
for _, row := range rows {
var user model.User
_ = r.db.WithContext(ctx).First(&user, row.InviteeID).Error
maskedPhone := ""
if len(user.Phone) >= 11 {
maskedPhone = user.Phone[:3] + "****" + user.Phone[7:]
} else if user.Phone != "" {
maskedPhone = user.Phone
}
stage := "unverified"
days := 0
if row.VerifiedAt != nil {
days = int(now.Sub(*row.VerifiedAt).Hours() / 24)
if days <= 30 {
stage = CycleFirstMonth
} else if days <= 60 {
stage = CycleSecondMonth
} else {
stage = CycleSubsequent
}
}
var earnedCent int64
_ = r.db.WithContext(ctx).Model(&model.InvitationRebateLedger{}).
Where("inviter_id = ? AND invitee_id = ? AND status = 'settled'", userID, row.InviteeID).
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(&earnedCent).Error
result = append(result, InvitedUserRecordDTO{
ID: row.ID,
InviteeID: row.InviteeID,
MaskedPhone: maskedPhone,
Nickname: user.Nickname,
AvatarURL: user.AvatarURL,
Status: row.Status,
CycleStage: stage,
DaysSinceVerified: days,
VerifiedAt: row.VerifiedAt,
RegisteredAt: row.CreatedAt,
TotalEarnedFromCent: earnedCent,
})
}
return result, total, nil
}
// ListMyRebates 获取我的返利流水列表
func (r *Repository) ListMyRebates(ctx context.Context, userID uint64, page, pageSize int) ([]RebateLedgerDTO, int64, error) {
if r == nil || r.db == nil {
return nil, 0, ErrDependencyUnavailable
}
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 50 {
pageSize = 20
}
var total int64
db := r.db.WithContext(ctx).Model(&model.InvitationRebateLedger{}).Where("inviter_id = ?", userID)
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []model.InvitationRebateLedger
offset := (page - 1) * pageSize
if err := db.Order("id DESC").Offset(offset).Limit(pageSize).Find(&rows).Error; err != nil {
return nil, 0, err
}
result := make([]RebateLedgerDTO, 0, len(rows))
for _, row := range rows {
var user model.User
_ = r.db.WithContext(ctx).First(&user, row.InviteeID).Error
maskedPhone := ""
if len(user.Phone) >= 11 {
maskedPhone = user.Phone[:3] + "****" + user.Phone[7:]
}
result = append(result, RebateLedgerDTO{
ID: row.ID,
OrderID: row.OrderID,
OrderNo: row.OrderNo,
InviteeMaskedPhone: maskedPhone,
InviteeNickname: user.Nickname,
TradeRole: row.TradeRole,
SingleSideBasisCent: row.SingleSideBasisCent,
RateApplied: row.RateApplied,
CycleStage: row.CycleStage,
RebateAmountCent: row.RebateAmountCent,
SettledAt: row.SettledAt,
})
}
return result, total, nil
}
// ListAdminInvitations 管理后台查询邀请关系
func (r *Repository) ListAdminInvitations(ctx context.Context, query AdminInvitationQuery) ([]map[string]any, int64, error) {
if r == nil || r.db == nil {
return nil, 0, ErrDependencyUnavailable
}
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 || query.PageSize > 100 {
query.PageSize = 20
}
db := r.db.WithContext(ctx).Table("user_invitations ui").
Joins("LEFT JOIN users inviter ON inviter.id = ui.inviter_id").
Joins("LEFT JOIN users invitee ON invitee.id = ui.invitee_id")
if p := strings.TrimSpace(query.InviterPhone); p != "" {
db = db.Where("inviter.phone = ?", p)
}
if p := strings.TrimSpace(query.InviteePhone); p != "" {
db = db.Where("invitee.phone = ?", p)
}
if s := strings.TrimSpace(query.Status); s != "" {
db = db.Where("ui.status = ?", s)
}
if query.DistributionSiteID != nil {
db = db.Where("ui.distribution_site_id = ?", *query.DistributionSiteID)
}
if query.InviterID > 0 {
db = db.Where("ui.inviter_id = ?", query.InviterID)
}
if query.InviteeID > 0 {
db = db.Where("ui.invitee_id = ?", query.InviteeID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
type rawRow struct {
ID uint64 `gorm:"column:id"`
InviterID uint64 `gorm:"column:inviter_id"`
InviterPhone string `gorm:"column:inviter_phone"`
InviterNickname string `gorm:"column:inviter_nickname"`
InviteeID uint64 `gorm:"column:invitee_id"`
InviteePhone string `gorm:"column:invitee_phone"`
InviteeNickname string `gorm:"column:invitee_nickname"`
DistributionSiteID uint64 `gorm:"column:distribution_site_id"`
InviteCode string `gorm:"column:invite_code"`
Status string `gorm:"column:status"`
VerifiedAt *time.Time `gorm:"column:verified_at"`
CreatedAt time.Time `gorm:"column:created_at"`
}
var rows []rawRow
offset := (query.Page - 1) * query.PageSize
err := db.Select("ui.id, ui.inviter_id, inviter.phone as inviter_phone, inviter.nickname as inviter_nickname, ui.invitee_id, invitee.phone as invitee_phone, invitee.nickname as invitee_nickname, ui.distribution_site_id, ui.invite_code, ui.status, ui.verified_at, ui.created_at").
Order("ui.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error
if err != nil {
return nil, 0, err
}
list := make([]map[string]any, 0, len(rows))
for _, row := range rows {
list = append(list, map[string]any{
"id": row.ID,
"inviter_id": row.InviterID,
"inviter_phone": row.InviterPhone,
"inviter_nickname": row.InviterNickname,
"invitee_id": row.InviteeID,
"invitee_phone": row.InviteePhone,
"invitee_nickname": row.InviteeNickname,
"distribution_site_id": row.DistributionSiteID,
"invite_code": row.InviteCode,
"status": row.Status,
"verified_at": row.VerifiedAt,
"created_at": row.CreatedAt,
})
}
return list, total, nil
}
// ListAdminRebateLedgers 管理后台查询返利流水
func (r *Repository) ListAdminRebateLedgers(ctx context.Context, query AdminRebateLedgerQuery) ([]map[string]any, int64, error) {
if r == nil || r.db == nil {
return nil, 0, ErrDependencyUnavailable
}
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 || query.PageSize > 100 {
query.PageSize = 20
}
db := r.db.WithContext(ctx).Table("invitation_rebate_ledgers irl").
Joins("LEFT JOIN users inviter ON inviter.id = irl.inviter_id").
Joins("LEFT JOIN users invitee ON invitee.id = irl.invitee_id")
if o := strings.TrimSpace(query.OrderNo); o != "" {
db = db.Where("irl.order_no = ?", o)
}
if p := strings.TrimSpace(query.InviterPhone); p != "" {
db = db.Where("inviter.phone = ?", p)
}
if p := strings.TrimSpace(query.InviteePhone); p != "" {
db = db.Where("invitee.phone = ?", p)
}
if t := strings.TrimSpace(query.TradeRole); t != "" {
db = db.Where("irl.trade_role = ?", t)
}
if query.DistributionSiteID != nil {
db = db.Where("irl.distribution_site_id = ?", *query.DistributionSiteID)
}
if query.InviterID > 0 {
db = db.Where("irl.inviter_id = ?", query.InviterID)
}
if query.InviteeID > 0 {
db = db.Where("irl.invitee_id = ?", query.InviteeID)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
type rawLedger struct {
ID uint64 `gorm:"column:id"`
OrderID uint64 `gorm:"column:order_id"`
OrderNo string `gorm:"column:order_no"`
DistributionSiteID uint64 `gorm:"column:distribution_site_id"`
InviterID uint64 `gorm:"column:inviter_id"`
InviterPhone string `gorm:"column:inviter_phone"`
InviteeID uint64 `gorm:"column:invitee_id"`
InviteePhone string `gorm:"column:invitee_phone"`
TradeRole string `gorm:"column:trade_role"`
OrderProfitBasisCent int64 `gorm:"column:order_profit_basis_cent"`
SingleSideBasisCent int64 `gorm:"column:single_side_basis_cent"`
RateApplied float64 `gorm:"column:rate_applied"`
DaysSinceVerified int `gorm:"column:days_since_verified"`
CycleStage string `gorm:"column:cycle_stage"`
RebateAmountCent int64 `gorm:"column:rebate_amount_cent"`
Status string `gorm:"column:status"`
SettledAt time.Time `gorm:"column:settled_at"`
}
var rows []rawLedger
offset := (query.Page - 1) * query.PageSize
err := db.Select("irl.id, irl.order_id, irl.order_no, irl.distribution_site_id, irl.inviter_id, inviter.phone as inviter_phone, irl.invitee_id, invitee.phone as invitee_phone, irl.trade_role, irl.order_profit_basis_cent, irl.single_side_basis_cent, irl.rate_applied, irl.days_since_verified, irl.cycle_stage, irl.rebate_amount_cent, irl.status, irl.settled_at").
Order("irl.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error
if err != nil {
return nil, 0, err
}
list := make([]map[string]any, 0, len(rows))
for _, row := range rows {
list = append(list, map[string]any{
"id": row.ID,
"order_id": row.OrderID,
"order_no": row.OrderNo,
"distribution_site_id": row.DistributionSiteID,
"inviter_id": row.InviterID,
"inviter_phone": row.InviterPhone,
"invitee_id": row.InviteeID,
"invitee_phone": row.InviteePhone,
"trade_role": row.TradeRole,
"order_profit_basis_cent": row.OrderProfitBasisCent,
"single_side_basis_cent": row.SingleSideBasisCent,
"rate_applied": row.RateApplied,
"days_since_verified": row.DaysSinceVerified,
"cycle_stage": row.CycleStage,
"rebate_amount_cent": row.RebateAmountCent,
"status": row.Status,
"settled_at": row.SettledAt,
})
}
return list, total, nil
}
// ProcessOrderInvitationRebateTx 便捷事务入口,供订单结账完成终态时调用
func ProcessOrderInvitationRebateTx(tx *gorm.DB, orderID uint64) error {
if tx == nil || orderID == 0 {
return nil
}
repo := NewRepository(tx)
return repo.ProcessOrderInvitationRebate(context.Background(), tx, orderID)
}
// OnUserRealnameVerifiedTx 便捷事务入口,供实名认证通过时调用
func OnUserRealnameVerifiedTx(tx *gorm.DB, userID uint64) error {
if tx == nil || userID == 0 {
return nil
}
repo := NewRepository(tx)
return repo.OnUserRealnameVerified(context.Background(), userID)
}