Files
ycc-proxy-server/app/main/api/internal/service/agentService.go

1071 lines
40 KiB
Go
Raw Normal View History

2025-11-27 13:09:54 +08:00
package service
import (
"context"
"database/sql"
2025-12-02 19:57:10 +08:00
"fmt"
2025-11-27 13:09:54 +08:00
"strconv"
"time"
"ycc-server/app/main/api/internal/config"
"ycc-server/app/main/model"
"ycc-server/common/globalkey"
"ycc-server/pkg/lzkit/lzUtils"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
// AgentService 新代理系统服务
type AgentService struct {
2025-12-02 19:57:10 +08:00
config config.Config
OrderModel model.OrderModel
AgentModel model.AgentModel
AgentWalletModel model.AgentWalletModel
AgentRelationModel model.AgentRelationModel
AgentLinkModel model.AgentLinkModel
AgentOrderModel model.AgentOrderModel
AgentCommissionModel model.AgentCommissionModel
AgentRebateModel model.AgentRebateModel
AgentUpgradeModel model.AgentUpgradeModel
AgentWithdrawalModel model.AgentWithdrawalModel
AgentConfigModel model.AgentConfigModel
2025-11-27 13:09:54 +08:00
AgentProductConfigModel model.AgentProductConfigModel
2025-12-02 19:57:10 +08:00
AgentRealNameModel model.AgentRealNameModel
2025-11-27 13:09:54 +08:00
AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel
2025-12-02 19:57:10 +08:00
AgentFreezeTaskModel model.AgentFreezeTaskModel // 冻结任务模型需要先运行SQL并生成model
2025-11-27 13:09:54 +08:00
}
// NewAgentService 创建新的代理服务
func NewAgentService(
c config.Config,
orderModel model.OrderModel,
agentModel model.AgentModel,
agentWalletModel model.AgentWalletModel,
agentRelationModel model.AgentRelationModel,
agentLinkModel model.AgentLinkModel,
agentOrderModel model.AgentOrderModel,
agentCommissionModel model.AgentCommissionModel,
agentRebateModel model.AgentRebateModel,
agentUpgradeModel model.AgentUpgradeModel,
agentWithdrawalModel model.AgentWithdrawalModel,
agentConfigModel model.AgentConfigModel,
agentProductConfigModel model.AgentProductConfigModel,
agentRealNameModel model.AgentRealNameModel,
agentWithdrawalTaxModel model.AgentWithdrawalTaxModel,
2025-12-02 19:57:10 +08:00
agentFreezeTaskModel model.AgentFreezeTaskModel, // 冻结任务模型需要先运行SQL并生成model
2025-11-27 13:09:54 +08:00
) *AgentService {
return &AgentService{
2025-12-02 19:57:10 +08:00
config: c,
OrderModel: orderModel,
AgentModel: agentModel,
AgentWalletModel: agentWalletModel,
AgentRelationModel: agentRelationModel,
AgentLinkModel: agentLinkModel,
AgentOrderModel: agentOrderModel,
AgentCommissionModel: agentCommissionModel,
AgentRebateModel: agentRebateModel,
AgentUpgradeModel: agentUpgradeModel,
AgentWithdrawalModel: agentWithdrawalModel,
AgentConfigModel: agentConfigModel,
2025-11-27 13:09:54 +08:00
AgentProductConfigModel: agentProductConfigModel,
2025-12-02 19:57:10 +08:00
AgentRealNameModel: agentRealNameModel,
2025-11-27 13:09:54 +08:00
AgentWithdrawalTaxModel: agentWithdrawalTaxModel,
2025-12-02 19:57:10 +08:00
AgentFreezeTaskModel: agentFreezeTaskModel,
2025-11-27 13:09:54 +08:00
}
}
// AgentProcess 处理代理订单(新系统)
// 根据新代理系统的收益分配规则处理订单
func (s *AgentService) AgentProcess(ctx context.Context, order *model.Order) error {
// 1. 检查是否是代理推广订单
agentOrder, err := s.AgentOrderModel.FindOneByOrderId(ctx, order.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 不是代理订单,直接返回
return nil
}
return errors.Wrapf(err, "查询代理订单失败, orderId: %d", order.Id)
}
// 2. 检查订单是否已处理
if agentOrder.ProcessStatus == 1 {
logx.Infof("订单已处理, orderId: %d", order.Id)
return nil
}
// 3. 获取代理信息
agent, err := s.AgentModel.FindOne(ctx, agentOrder.AgentId)
if err != nil {
return errors.Wrapf(err, "查询代理信息失败, agentId: %d", agentOrder.AgentId)
}
2025-12-02 19:57:10 +08:00
// 4. 获取产品配置(必须存在)
productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId)
2025-11-27 13:09:54 +08:00
if err != nil {
2025-12-02 19:57:10 +08:00
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "产品配置不存在, productId: %d请先在后台配置产品价格参数", order.ProductId)
}
return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId)
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 5. 使用事务处理订单
err = s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
// 5.1 获取等级加成
levelBonus, err := s.getLevelBonus(transCtx, agent.Level)
if err != nil {
return errors.Wrapf(err, "获取等级加成配置失败")
}
// 5.2 使用产品配置的底价计算实际底价
basePrice := productConfig.BasePrice
2025-11-27 13:09:54 +08:00
actualBasePrice := basePrice + float64(levelBonus)
2025-12-02 19:57:10 +08:00
// 5.3 计算提价成本(使用产品配置)
priceThreshold := 0.0
priceFeeRate := 0.0
if productConfig.PriceThreshold.Valid {
priceThreshold = productConfig.PriceThreshold.Float64
}
if productConfig.PriceFeeRate.Valid {
priceFeeRate = productConfig.PriceFeeRate.Float64
}
2025-11-27 13:09:54 +08:00
priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate)
2025-12-02 19:57:10 +08:00
// 5.4 计算代理收益
2025-11-27 13:09:54 +08:00
agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost
2025-12-02 19:57:10 +08:00
// 5.5 更新代理订单记录
2025-11-27 13:09:54 +08:00
agentOrder.ProcessStatus = 1
agentOrder.ProcessTime = lzUtils.TimeToNullTime(time.Now())
agentOrder.ProcessRemark = lzUtils.StringToNullString("处理成功")
if err := s.AgentOrderModel.UpdateWithVersion(transCtx, session, agentOrder); err != nil {
return errors.Wrapf(err, "更新代理订单失败")
}
2025-12-02 19:57:10 +08:00
// 5.6 发放代理佣金(传入订单单价用于冻结判断)
// 注意:冻结任务会在 agentProcess.go 中通过查询订单的冻结任务来发送解冻任务
_, commissionErr := s.giveAgentCommission(transCtx, session, agentOrder.AgentId, order.Id, order.ProductId, agentProfit, agentOrder.SetPrice)
if commissionErr != nil {
return errors.Wrapf(commissionErr, "发放代理佣金失败")
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 5.7 分配等级加成返佣给上级链
2025-11-27 13:09:54 +08:00
if levelBonus > 0 {
if err := s.distributeLevelBonus(transCtx, session, agent, order.Id, order.ProductId, float64(levelBonus), levelBonus); err != nil {
return errors.Wrapf(err, "分配等级加成返佣失败")
}
}
return nil
})
2025-12-02 19:57:10 +08:00
return err
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// getLevelBonus 获取等级加成(从配置表读取)
func (s *AgentService) getLevelBonus(ctx context.Context, level int64) (int64, error) {
var configKey string
2025-11-27 13:09:54 +08:00
switch level {
case 1: // 普通
2025-12-02 19:57:10 +08:00
configKey = "level_1_bonus"
2025-11-27 13:09:54 +08:00
case 2: // 黄金
2025-12-02 19:57:10 +08:00
configKey = "level_2_bonus"
2025-11-27 13:09:54 +08:00
case 3: // 钻石
2025-12-02 19:57:10 +08:00
configKey = "level_3_bonus"
2025-11-27 13:09:54 +08:00
default:
2025-12-02 19:57:10 +08:00
return 0, nil
}
bonus, err := s.getConfigFloat(ctx, configKey)
if err != nil {
// 配置不存在时返回默认值
logx.Errorf("获取等级加成配置失败, level: %d, key: %s, err: %v使用默认值", level, configKey, err)
switch level {
case 1:
return 6, nil
case 2:
return 3, nil
case 3:
return 0, nil
}
return 0, nil
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
return int64(bonus), nil
2025-11-27 13:09:54 +08:00
}
// calculatePriceCost 计算提价成本
func (s *AgentService) calculatePriceCost(setPrice, priceThreshold, priceFeeRate float64) float64 {
if setPrice <= priceThreshold {
return 0
}
return (setPrice - priceThreshold) * priceFeeRate
}
// giveAgentCommission 发放代理佣金
2025-12-02 19:57:10 +08:00
// orderPrice: 订单单价,用于判断是否需要冻结
// 返回freezeTaskId如果有冻结任务error
func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Session, agentId, orderId, productId int64, amount float64, orderPrice float64) (int64, error) {
2025-11-27 13:09:54 +08:00
// 1. 创建佣金记录
commission := &model.AgentCommission{
AgentId: agentId,
OrderId: orderId,
ProductId: productId,
Amount: amount,
Status: 1, // 已发放
}
2025-12-02 19:57:10 +08:00
commissionResult, err := s.AgentCommissionModel.Insert(ctx, session, commission)
if err != nil {
return 0, errors.Wrapf(err, "创建佣金记录失败")
}
commissionId, _ := commissionResult.LastInsertId()
// 2. 判断是否需要冻结
// 2.1 获取冻结阈值配置默认100元
freezeThreshold, err := s.getConfigFloat(ctx, "commission_freeze_threshold")
if err != nil {
// 配置不存在时使用默认值100元
freezeThreshold = 100.0
logx.Errorf("获取冻结阈值配置失败使用默认值100元, orderId: %d, err: %v", orderId, err)
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 2.2 判断订单单价是否达到冻结阈值
freezeAmount := 0.0
var freezeTaskId int64 = 0
if orderPrice >= freezeThreshold {
// 2.3 获取冻结比例配置默认10%
freezeRatio, err := s.getConfigFloat(ctx, "commission_freeze_ratio")
if err != nil {
// 配置不存在时使用默认值0.110%
freezeRatio = 0.1
logx.Errorf("获取冻结比例配置失败使用默认值10%%, orderId: %d, err: %v", orderId, err)
}
// 计算冻结金额订单单价的10%
freezeAmountByPrice := orderPrice * freezeRatio
// 冻结金额不能超过佣金金额
if freezeAmountByPrice > amount {
freezeAmount = amount
} else {
freezeAmount = freezeAmountByPrice
}
// 如果冻结金额大于0创建冻结任务
if freezeAmount > 0 {
// 2.4 获取解冻天数配置默认30天即1个月
unfreezeDays, err := s.getConfigInt(ctx, "commission_freeze_days")
if err != nil {
// 配置不存在时使用默认值30天
unfreezeDays = 30
logx.Errorf("获取解冻天数配置失败使用默认值30天, orderId: %d, err: %v", orderId, err)
}
// 计算解冻时间(从配置读取的天数后)
// 注意:配置只在创建任务时读取,已创建的任务不受后续配置修改影响
unfreezeTime := time.Now().AddDate(0, 0, int(unfreezeDays))
// 创建冻结任务记录
freezeTask := &model.AgentFreezeTask{
AgentId: agentId,
OrderId: orderId,
CommissionId: commissionId,
FreezeAmount: freezeAmount,
OrderPrice: orderPrice,
FreezeRatio: freezeRatio,
Status: 1, // 待解冻
FreezeTime: time.Now(),
UnfreezeTime: unfreezeTime,
Remark: lzUtils.StringToNullString(fmt.Sprintf("订单单价%.2f元,冻结比例%.2f%%,解冻天数%d天", orderPrice, freezeRatio*100, unfreezeDays)),
}
freezeTaskResult, err := s.AgentFreezeTaskModel.Insert(ctx, session, freezeTask)
if err != nil {
return 0, errors.Wrapf(err, "创建冻结任务失败")
}
freezeTaskId, _ = freezeTaskResult.LastInsertId()
}
}
// 3. 更新钱包余额
2025-11-27 13:09:54 +08:00
wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, agentId)
if err != nil {
2025-12-02 19:57:10 +08:00
return 0, errors.Wrapf(err, "查询钱包失败, agentId: %d", agentId)
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 实际到账金额 = 佣金金额 - 冻结金额
actualAmount := amount - freezeAmount
wallet.Balance += actualAmount
wallet.FrozenBalance += freezeAmount
wallet.TotalEarnings += amount // 累计收益包含冻结部分
2025-11-27 13:09:54 +08:00
if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
2025-12-02 19:57:10 +08:00
return 0, errors.Wrapf(err, "更新钱包失败")
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
return freezeTaskId, nil
2025-11-27 13:09:54 +08:00
}
// distributeLevelBonus 分配等级加成返佣给上级链
func (s *AgentService) distributeLevelBonus(ctx context.Context, session sqlx.Session, agent *model.Agent, orderId, productId int64, levelBonus float64, levelBonusInt int64) error {
// 钻石代理等级加成为0无返佣分配
if agent.Level == 3 {
return nil
}
// 黄金代理等级加成3元全部给钻石上级
if agent.Level == 2 {
diamondParent, err := s.findDiamondParent(ctx, agent.Id)
if err != nil {
return errors.Wrapf(err, "查找钻石上级失败")
}
if diamondParent != nil {
return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, levelBonus, levelBonusInt, 2) // 2=钻石上级返佣
}
// 找不到钻石上级,返佣归平台(异常情况)
return nil
}
// 普通代理等级加成6元按规则分配给上级链
if agent.Level == 1 {
return s.distributeNormalAgentBonus(ctx, session, agent, orderId, productId, levelBonus, levelBonusInt)
}
return nil
}
2025-12-02 19:57:10 +08:00
// distributeNormalAgentBonus 普通代理的等级加成返佣分配
//
// 功能说明:根据普通代理的直接上级等级,按照规则分配等级加成返佣
//
// 参数说明:
// - amount: 等级加成总额例如6元
// - levelBonusInt: 等级加成整数(用于记录)
//
// 分配规则总览:
// 1. 直接上级是钻石:等级加成全部给钻石
// 2. 直接上级是黄金一部分给黄金配置direct_parent_amount_gold默认3元剩余给钻石上级
// 3. 直接上级是普通一部分给直接上级配置direct_parent_amount_normal默认2元剩余给钻石/黄金上级
//
// 覆盖的所有情况:
//
// 情况1普通(推广人) -> 钻石(直接上级)
// => 全部给钻石
//
// 情况2普通(推广人) -> 黄金(直接上级) -> 钻石
// => 一部分给黄金,剩余给钻石
//
// 情况3普通(推广人) -> 黄金(直接上级) -> 无钻石上级
// => 一部分给黄金,剩余归平台
//
// 情况4普通(推广人) -> 普通(直接上级) -> 钻石
// => 一部分给直接上级普通,剩余全部给钻石
//
// 情况5普通(推广人) -> 普通(直接上级) -> 黄金 -> 钻石
// => 一部分给直接上级普通例如2元一部分给黄金等级加成差减去给普通的例如3-2=1元剩余给钻石例如3元
//
// 情况6普通(推广人) -> 普通(直接上级) -> 黄金(无钻石)
// => 一部分给直接上级普通剩余一部分给黄金最多3元超出归平台
//
// 情况7普通(推广人) -> 普通(直接上级) -> 普通 -> 钻石
// => 一部分给直接上级普通,剩余全部给钻石(跳过中间普通代理)
//
// 情况8普通(推广人) -> 普通(直接上级) -> 普通 -> 黄金(无钻石)
// => 一部分给直接上级普通剩余一部分给黄金最多3元超出归平台跳过中间普通代理
//
// 情况9普通(推广人) -> 普通(直接上级) -> 普通 -> 普通...(全部是普通)
// => 一部分给直接上级普通,剩余归平台
//
// 注意findDiamondParent 和 findGoldParent 会自动跳过中间的所有普通代理,
//
// 直接向上查找到第一个钻石或黄金代理
2025-11-27 13:09:54 +08:00
func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session sqlx.Session, agent *model.Agent, orderId, productId int64, amount float64, levelBonusInt int64) error {
// 1. 查找直接上级
parent, err := s.findDirectParent(ctx, agent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "查找直接上级失败")
}
if parent == nil {
// 无上级,全部归平台
return nil
}
2025-12-02 19:57:10 +08:00
// 2. 根据直接上级等级分配
2025-11-27 13:09:54 +08:00
switch parent.Level {
2025-12-02 19:57:10 +08:00
case 3: // 直接上级是钻石代理的情况
// ========== 直接上级是钻石:等级加成全部给钻石上级 ==========
// 场景示例:
// - 普通(推广人) -> 钻石(直接上级)等级加成6元全部给钻石
// 说明:如果直接上级就是钻石,不需要再向上查找,全部返佣给直接上级钻石
// rebateType = 2表示钻石上级返佣
return s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, amount, levelBonusInt, 2)
case 2: // 直接上级是黄金代理的情况
// ========== 步骤1给直接上级黄金代理返佣 ==========
// 配置键direct_parent_amount_gold普通代理给直接上级黄金代理的返佣金额
// 默认值3.0元
// 说明:这部分金额给直接上级黄金代理,剩余部分继续向上分配给钻石上级
goldRebateAmount, err := s.getRebateConfigFloat(ctx, "direct_parent_amount_gold", 3.0)
if err != nil {
logx.Errorf("获取黄金返佣配置失败使用默认值3元: %v", err)
goldRebateAmount = 3.0 // 配置读取失败时使用默认值3元
}
2025-11-27 13:09:54 +08:00
2025-12-02 19:57:10 +08:00
// 计算给直接上级黄金代理的返佣金额(不能超过总等级加成金额)
goldAmount := goldRebateAmount // 默认3元
if goldAmount > amount {
// 如果配置金额大于等级加成总额,则只给总额(防止配置错误导致负数)
goldAmount = amount
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 发放返佣给直接上级黄金代理
// rebateType = 1表示直接上级返佣
if goldAmount > 0 {
if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 1); err != nil {
return errors.Wrapf(err, "给黄金上级返佣失败")
}
}
// ========== 步骤2计算剩余金额并分配给钻石上级 ==========
// 剩余金额 = 总等级加成 - 已给黄金上级的金额
// 例如等级加成6元 - 给黄金上级3元 = 剩余3元
remaining := amount - goldAmount
if remaining > 0 {
// 从黄金上级开始向上查找钻石上级
// 场景示例:
// - 普通(推广人) -> 黄金(直接上级) -> 钻石剩余3元给钻石
// - 普通(推广人) -> 黄金(直接上级) -> 普通 -> 钻石剩余3元给钻石跳过中间普通代理
// - 普通(推广人) -> 黄金(直接上级) -> 无上级剩余3元归平台没有钻石上级
diamondParent, err := s.findDiamondParent(ctx, parent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "查找钻石上级失败")
}
if diamondParent != nil {
// 找到钻石上级,剩余金额全部给钻石上级
// rebateType = 2表示钻石上级返佣
return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2)
}
// 找不到钻石上级,剩余金额归平台(不需要记录)
// 例如等级加成6元给黄金3元剩余3元但找不到钻石上级则剩余3元归平台
}
2025-11-27 13:09:54 +08:00
return nil
2025-12-02 19:57:10 +08:00
case 1: // 直接上级是普通代理的情况
// ========== 步骤1给直接上级普通代理返佣 ==========
// 配置键direct_parent_amount_normal普通代理给直接上级普通代理的返佣金额
// 默认值2.0元
// 说明:无论后续层级有多少普通代理,这部分金额只给推广人的直接上级
normalRebateAmount, err := s.getRebateConfigFloat(ctx, "direct_parent_amount_normal", 2.0)
if err != nil {
logx.Errorf("获取普通返佣配置失败使用默认值2元: %v", err)
normalRebateAmount = 2.0 // 配置读取失败时使用默认值2元
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 计算给直接上级的返佣金额(不能超过总等级加成金额)
directAmount := normalRebateAmount // 默认2元
if directAmount > amount {
// 如果配置金额大于等级加成总额,则只给总额(防止配置错误导致负数)
directAmount = amount
}
// 发放返佣给直接上级普通代理
// rebateType = 1表示直接上级返佣
if directAmount > 0 {
if err := s.giveRebate(ctx, session, parent.Id, agent.Id, orderId, productId, directAmount, levelBonusInt, 1); err != nil {
return errors.Wrapf(err, "给直接上级返佣失败")
}
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// ========== 步骤2计算剩余金额 ==========
// 剩余金额 = 总等级加成 - 已给直接上级的金额
// 例如等级加成6元 - 给直接上级2元 = 剩余4元
remaining := amount - directAmount
if remaining <= 0 {
// 如果没有剩余,直接返回(所有金额已分配给直接上级)
return nil
}
// ========== 步骤3从直接上级开始向上查找钻石和黄金代理 ==========
// 注意findDiamondParent 和 findGoldParent 会自动跳过中间的所有普通代理
// 例如:
// - 普通 -> 普通 -> 普通 -> 钻石:会跳过中间的普通代理,直接找到钻石
// - 普通 -> 普通 -> 黄金 -> 钻石:会找到钻石(优先级更高)
// - 普通 -> 黄金:会找到黄金
diamondParent, _ := s.findDiamondParent(ctx, parent.Id) // 向上查找钻石上级(跳过所有普通和黄金)
goldParent, _ := s.findGoldParent(ctx, parent.Id) // 向上查找黄金上级(跳过所有普通)
// ========== 步骤4按优先级分配剩余金额 ==========
// 优先级规则:
// 1. 优先给钻石上级(如果存在)
// 2. 其次给黄金上级(如果钻石不存在)
// 3. 最后归平台(如果钻石和黄金都不存在)
2025-11-27 13:09:54 +08:00
if diamondParent != nil {
2025-12-02 19:57:10 +08:00
// ========== 情况A找到钻石上级 ==========
// 场景示例:
// - 普通(推广人) -> 普通(直接上级) -> 钻石给普通2元剩余4元给钻石
// - 普通(推广人) -> 普通(直接上级) -> 黄金 -> 钻石给普通2元给黄金1元剩余3元给钻石
// - 普通(推广人) -> 普通(直接上级) -> 普通 -> 普通 -> 钻石给普通2元剩余4元给钻石跳过中间普通代理
//
// 分配规则:当有钻石上级时,需要给黄金上级一部分返佣
// 1. 等级加成的差 = 普通等级加成 - 黄金等级加成例如6元 - 3元 = 3元
// 2. 从这个差中先给直接上级普通代理已给例如2元
// 3. 差减去给普通的部分剩余给黄金代理例如3元 - 2元 = 1元
// 4. 最后剩余部分给钻石例如6元 - 2元 - 1元 = 3元
// 步骤A1计算等级加成的差
// 获取普通代理等级加成当前代理的等级加成即amount
normalBonus := amount // 例如6元
// 获取黄金代理等级加成
goldBonus, err := s.getLevelBonus(ctx, 2) // 黄金等级加成
if err != nil {
logx.Errorf("获取黄金等级加成配置失败使用默认值3元: %v", err)
goldBonus = 3 // 默认3元
}
// 计算等级加成的差
bonusDiff := normalBonus - float64(goldBonus) // 例如6元 - 3元 = 3元
// 步骤A2如果有黄金上级给黄金上级一部分返佣
// 规则说明:
// - 等级加成的差bonusDiff代表普通代理和黄金代理的等级加成差异
// - 从这个差中先分配给直接上级普通代理directAmount已分配
// - 剩余的差分配给黄金代理goldRebateAmount = bonusDiff - directAmount
// - 如果差小于等于已给普通的部分,则不给黄金(这种情况理论上不应该发生,因为差应该>=给普通的部分)
if goldParent != nil && bonusDiff > 0 {
// 计算给黄金上级的金额 = 等级加成差 - 已给普通上级的金额
// 例如等级加成差3元 - 已给普通2元 = 给黄金1元
goldRebateAmount := bonusDiff - directAmount
// 如果计算出的金额小于等于0说明差已经被普通代理全部占用了不给黄金
// 例如如果差是2元已给普通2元则 goldRebateAmount = 0不给黄金
if goldRebateAmount > 0 {
// 边界检查:确保不超过剩余金额(理论上应该不会超过,但保险起见)
if goldRebateAmount > remaining {
goldRebateAmount = remaining
}
// 发放返佣给黄金上级
// rebateType = 3表示黄金上级返佣
if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldRebateAmount, levelBonusInt, 3); err != nil {
return errors.Wrapf(err, "给黄金上级返佣失败")
}
// 更新剩余金额(用于后续分配给钻石)
// 例如剩余4元 - 给黄金1元 = 剩余3元给钻石
remaining = remaining - goldRebateAmount
}
}
// 步骤A3剩余金额全部给钻石上级
// rebateType = 2表示钻石上级返佣
if remaining > 0 {
return s.giveRebate(ctx, session, diamondParent.Id, agent.Id, orderId, productId, remaining, levelBonusInt, 2)
}
return nil
2025-11-27 13:09:54 +08:00
} else if goldParent != nil {
2025-12-02 19:57:10 +08:00
// ========== 情况B没有钻石上级但有黄金上级 ==========
// 场景示例:
// - 普通(推广人) -> 普通(直接上级) -> 黄金(没有钻石):给黄金一部分,剩余归平台
// - 普通(推广人) -> 普通(直接上级) -> 普通 -> 黄金(没有钻石):给黄金一部分,剩余归平台(跳过中间普通代理)
// 配置键max_gold_rebate_amount普通代理给黄金上级的最大返佣金额
// 默认值3.0元
// 说明即使剩余金额超过这个值也只能给黄金上级最多3元超出部分归平台
maxGoldRebate, err := s.getRebateConfigFloat(ctx, "max_gold_rebate_amount", 3.0)
if err != nil {
logx.Errorf("获取黄金最大返佣配置失败使用默认值3元: %v", err)
maxGoldRebate = 3.0 // 配置读取失败时使用默认值3元
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 计算给黄金上级的返佣金额
goldAmount := remaining // 剩余金额
if goldAmount > maxGoldRebate {
// 如果剩余金额超过最大限额则只给最大限额例如剩余4元但最多只能给3元
goldAmount = maxGoldRebate
}
// 例如剩余4元最大限额3元则给黄金3元剩余1元归平台
// 发放返佣给黄金上级
// rebateType = 3表示黄金上级返佣
if goldAmount > 0 {
if err := s.giveRebate(ctx, session, goldParent.Id, agent.Id, orderId, productId, goldAmount, levelBonusInt, 3); err != nil {
return errors.Wrapf(err, "给黄金上级返佣失败")
}
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 超出最大限额的部分归平台(不需要记录)
// 例如剩余4元给黄金3元剩余1元归平台
return nil
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// ========== 情况C既没有钻石上级也没有黄金上级 ==========
// 场景示例:
// - 普通(推广人) -> 普通(直接上级) -> 普通 -> 普通...(整个链路都是普通代理)
// - 普通(推广人) -> 普通(直接上级) -> 无上级(已经是最顶层)
// 剩余金额全部归平台(不需要记录)
return nil
default:
// 未知等级,全部归平台
return nil
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
}
2025-11-27 13:09:54 +08:00
2025-12-02 19:57:10 +08:00
// getRebateConfigFloat 获取返佣配置值(浮点数),如果配置不存在则返回默认值
func (s *AgentService) getRebateConfigFloat(ctx context.Context, configKey string, defaultValue float64) (float64, error) {
config, err := s.AgentConfigModel.FindOneByConfigKey(ctx, configKey)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return defaultValue, nil
}
return defaultValue, err
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return defaultValue, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return value, nil
2025-11-27 13:09:54 +08:00
}
// giveRebate 发放返佣
func (s *AgentService) giveRebate(ctx context.Context, session sqlx.Session, agentId, sourceAgentId, orderId, productId int64, amount float64, levelBonus int64, rebateType int64) error {
// 1. 创建返佣记录
rebate := &model.AgentRebate{
AgentId: agentId,
SourceAgentId: sourceAgentId,
OrderId: orderId,
ProductId: productId,
RebateType: rebateType,
LevelBonus: float64(levelBonus), // 等级加成金额
RebateAmount: amount,
Status: 1, // 已发放
}
if _, err := s.AgentRebateModel.Insert(ctx, session, rebate); err != nil {
return errors.Wrapf(err, "创建返佣记录失败")
}
// 2. 更新钱包余额
wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, agentId)
if err != nil {
return errors.Wrapf(err, "查询钱包失败, agentId: %d", agentId)
}
wallet.Balance += amount
wallet.TotalEarnings += amount
if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
return errors.Wrapf(err, "更新钱包失败")
}
return nil
}
// FindDirectParent 查找直接上级(公开方法)
func (s *AgentService) FindDirectParent(ctx context.Context, agentId int64) (*model.Agent, error) {
return s.findDirectParent(ctx, agentId)
}
// findDirectParent 查找直接上级
func (s *AgentService) findDirectParent(ctx context.Context, agentId int64) (*model.Agent, error) {
// 查找关系类型为1直接关系的上级
builder := s.AgentRelationModel.SelectBuilder()
builder = builder.Where("child_id = ? AND relation_type = ? AND del_state = ?", agentId, 1, globalkey.DelStateNo)
relations, err := s.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return nil, err
}
if len(relations) == 0 {
return nil, model.ErrNotFound
}
// 返回第一个直接上级
return s.AgentModel.FindOne(ctx, relations[0].ParentId)
}
// findDiamondParent 向上查找钻石上级
2025-12-02 19:57:10 +08:00
//
// 功能说明从指定代理开始向上逐级查找第一个钻石代理Level == 3
//
// 查找规则:
// - 自动跳过所有普通代理Level == 1和黄金代理Level == 2
// - 只返回第一个找到的钻石代理
// - 如果没有找到钻石代理,返回 ErrNotFound
//
// 示例场景:
// - 普通 -> 普通 -> 钻石:会找到钻石(跳过中间的普通代理)
// - 普通 -> 黄金 -> 钻石:会找到钻石(跳过黄金代理)
// - 普通 -> 普通 -> 黄金:返回 ErrNotFound没有钻石
2025-11-27 13:09:54 +08:00
func (s *AgentService) findDiamondParent(ctx context.Context, agentId int64) (*model.Agent, error) {
currentId := agentId
maxDepth := 100 // 防止无限循环
depth := 0
for depth < maxDepth {
parent, err := s.findDirectParent(ctx, currentId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, model.ErrNotFound
}
return nil, err
}
if parent.Level == 3 { // 钻石
return parent, nil
}
currentId = parent.Id
depth++
}
return nil, model.ErrNotFound
}
// findGoldParent 向上查找黄金上级
2025-12-02 19:57:10 +08:00
//
// 功能说明从指定代理开始向上逐级查找第一个黄金代理Level == 2
//
// 查找规则:
// - 自动跳过所有普通代理Level == 1
// - 如果先遇到钻石代理Level == 3不会跳过但会继续查找黄金代理
// 注意:在实际使用中,应该先调用 findDiamondParent如果没找到钻石再调用此方法
// - 只返回第一个找到的黄金代理
// - 如果没有找到黄金代理,返回 ErrNotFound
//
// 示例场景:
// - 普通 -> 普通 -> 黄金:会找到黄金(跳过中间的普通代理)
// - 普通 -> 黄金:会找到黄金
// - 普通 -> 普通 -> 钻石:返回 ErrNotFound跳过钻石继续查找黄金但找不到
2025-11-27 13:09:54 +08:00
func (s *AgentService) findGoldParent(ctx context.Context, agentId int64) (*model.Agent, error) {
currentId := agentId
maxDepth := 100 // 防止无限循环
depth := 0
for depth < maxDepth {
parent, err := s.findDirectParent(ctx, currentId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, model.ErrNotFound
}
return nil, err
}
if parent.Level == 2 { // 黄金
return parent, nil
}
currentId = parent.Id
depth++
}
return nil, model.ErrNotFound
}
// getConfigFloat 获取配置值(浮点数)
func (s *AgentService) getConfigFloat(ctx context.Context, configKey string) (float64, error) {
config, err := s.AgentConfigModel.FindOneByConfigKey(ctx, configKey)
if err != nil {
return 0, err
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return value, nil
}
// getConfigInt 获取配置值(整数)
func (s *AgentService) getConfigInt(ctx context.Context, configKey string) (int64, error) {
config, err := s.AgentConfigModel.FindOneByConfigKey(ctx, configKey)
if err != nil {
return 0, err
}
value, err := strconv.ParseInt(config.ConfigValue, 10, 64)
if err != nil {
return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return value, nil
}
// ProcessUpgrade 处理代理升级
func (s *AgentService) ProcessUpgrade(ctx context.Context, agentId, toLevel int64, upgradeType int64, upgradeFee, rebateAmount float64, orderNo string, operatorAgentId int64) error {
return s.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error {
// 1. 获取代理信息
agent, err := s.AgentModel.FindOne(transCtx, agentId)
if err != nil {
return errors.Wrapf(err, "查询代理信息失败, agentId: %d", agentId)
}
// 2. 如果是自主付费升级,处理返佣
if upgradeType == 1 { // 自主付费
// 查找原直接上级
parent, err := s.findDirectParent(transCtx, agentId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "查找直接上级失败")
}
if parent != nil && rebateAmount > 0 {
// 返佣给原直接上级
2025-12-02 19:57:10 +08:00
if err := s.giveRebateForUpgrade(transCtx, session, parent.Id, agentId, rebateAmount, orderNo); err != nil {
2025-11-27 13:09:54 +08:00
return errors.Wrapf(err, "返佣给上级失败")
}
}
}
// 3. 执行升级操作
agent.Level = toLevel
// 4. 检查是否需要脱离直接上级关系
needDetach, err := s.needDetachFromParent(transCtx, agent, toLevel)
if err != nil {
return errors.Wrapf(err, "检查是否需要脱离关系失败")
}
if needDetach {
2025-12-02 19:57:10 +08:00
// 脱离前先获取原直接上级及其上级的信息(用于后续重新连接)
oldParent, oldParentErr := s.findDirectParent(transCtx, agentId)
var grandparentId int64 = 0
if oldParentErr == nil && oldParent != nil {
// 查找原上级的上级
grandparent, grandparentErr := s.findDirectParent(transCtx, oldParent.Id)
if grandparentErr == nil && grandparent != nil {
grandparentId = grandparent.Id
}
}
2025-11-27 13:09:54 +08:00
// 脱离直接上级关系
if err := s.detachFromParent(transCtx, session, agentId); err != nil {
return errors.Wrapf(err, "脱离直接上级关系失败")
}
2025-12-02 19:57:10 +08:00
// 脱离后,尝试连接到原上级的上级
if grandparentId > 0 {
if err := s.reconnectToGrandparent(transCtx, session, agentId, toLevel, grandparentId); err != nil {
return errors.Wrapf(err, "重新连接上级关系失败")
}
}
2025-11-27 13:09:54 +08:00
}
// 5. 如果升级为钻石,独立成新团队
if toLevel == 3 {
agent.TeamLeaderId = sql.NullInt64{Int64: agentId, Valid: true}
// 更新所有下级的团队首领
if err := s.updateChildrenTeamLeader(transCtx, session, agentId, agentId); err != nil {
return errors.Wrapf(err, "更新下级团队首领失败")
}
} else {
// 更新团队首领(查找上级链中的钻石代理)
teamLeaderId, err := s.findTeamLeaderId(transCtx, agentId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(err, "查找团队首领失败")
}
if teamLeaderId > 0 {
agent.TeamLeaderId = sql.NullInt64{Int64: teamLeaderId, Valid: true}
}
}
// 6. 更新代理记录
if err := s.AgentModel.UpdateWithVersion(transCtx, session, agent); err != nil {
return errors.Wrapf(err, "更新代理记录失败")
}
// 7. 更新升级记录状态
// 这里需要先查询升级记录暂时先跳过在logic中处理
return nil
})
}
// needDetachFromParent 检查是否需要脱离直接上级关系
func (s *AgentService) needDetachFromParent(ctx context.Context, agent *model.Agent, newLevel int64) (bool, error) {
parent, err := s.findDirectParent(ctx, agent.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return false, nil // 没有上级,不需要脱离
}
return false, err
}
// 规则1下级不能比上级等级高
if newLevel > parent.Level {
return true, nil
}
// 规则2同级不能作为上下级除了普通代理
if newLevel == parent.Level {
2025-12-02 19:57:10 +08:00
if newLevel == 2 || newLevel == 3 { // 黄金或钻石同级需要脱离
2025-11-27 13:09:54 +08:00
return true, nil
}
2025-12-02 19:57:10 +08:00
// 普通代理同级newLevel == 1 && parent.Level == 1不需要脱离
2025-11-27 13:09:54 +08:00
}
return false, nil
}
// detachFromParent 脱离直接上级关系
func (s *AgentService) detachFromParent(ctx context.Context, session sqlx.Session, agentId int64) error {
// 查找直接关系
builder := s.AgentRelationModel.SelectBuilder().
Where("child_id = ? AND relation_type = ? AND del_state = ?", agentId, 1, globalkey.DelStateNo)
relations, err := s.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return err
}
if len(relations) == 0 {
return nil // 没有关系,不需要脱离
}
// 将直接关系标记为已脱离
relation := relations[0]
relation.RelationType = 2 // 已脱离
relation.DetachReason = lzUtils.StringToNullString("upgrade")
relation.DetachTime = lzUtils.TimeToNullTime(time.Now())
if err := s.AgentRelationModel.UpdateWithVersion(ctx, session, relation); err != nil {
return errors.Wrapf(err, "更新关系记录失败")
}
return nil
}
2025-12-02 19:57:10 +08:00
// reconnectToGrandparent 重新连接到原上级的上级(如果存在且符合条件)
func (s *AgentService) reconnectToGrandparent(ctx context.Context, session sqlx.Session, agentId int64, newLevel int64, grandparentId int64) error {
// 获取原上级的上级信息
grandparent, err := s.AgentModel.FindOne(ctx, grandparentId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 原上级的上级不存在,不需要重新连接
return nil
}
return errors.Wrapf(err, "查询原上级的上级失败")
}
// 验证是否可以连接到原上级的上级
// 规则:新等级必须低于或等于原上级的上级等级,且不能同级(除了普通代理)
if newLevel > grandparent.Level {
// 新等级高于原上级的上级,不能连接
return nil
}
// 同级不能作为上下级(除了普通代理)
if newLevel == grandparent.Level {
if newLevel == 2 || newLevel == 3 {
// 黄金或钻石同级不能连接
return nil
}
// 普通代理同级可以连接(虽然这种情况不太可能发生)
}
// 检查是否已经存在关系
builder := s.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND child_id = ? AND relation_type = ? AND del_state = ?", grandparent.Id, agentId, 1, globalkey.DelStateNo)
existingRelations, err := s.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return errors.Wrapf(err, "查询现有关系失败")
}
if len(existingRelations) > 0 {
// 关系已存在,不需要重复创建
return nil
}
// 创建新的关系连接到原上级的上级
relation := &model.AgentRelation{
ParentId: grandparent.Id,
ChildId: agentId,
RelationType: 1, // 直接关系
}
if _, err := s.AgentRelationModel.Insert(ctx, session, relation); err != nil {
return errors.Wrapf(err, "创建新关系失败")
}
return nil
}
2025-11-27 13:09:54 +08:00
// updateChildrenTeamLeader 更新所有下级的团队首领
func (s *AgentService) updateChildrenTeamLeader(ctx context.Context, session sqlx.Session, agentId, teamLeaderId int64) error {
// 递归更新所有下级
var updateChildren func(int64) error
updateChildren = func(parentId int64) error {
// 查找直接下级
builder := s.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := s.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return err
}
for _, relation := range relations {
child, err := s.AgentModel.FindOne(ctx, relation.ChildId)
if err != nil {
continue
}
child.TeamLeaderId = sql.NullInt64{Int64: teamLeaderId, Valid: true}
if err := s.AgentModel.UpdateWithVersion(ctx, session, child); err != nil {
return errors.Wrapf(err, "更新下级团队首领失败, childId: %d", child.Id)
}
// 递归更新下级的下级
if err := updateChildren(child.Id); err != nil {
return err
}
}
return nil
}
return updateChildren(agentId)
}
// findTeamLeaderId 查找团队首领ID钻石代理
func (s *AgentService) findTeamLeaderId(ctx context.Context, agentId int64) (int64, error) {
diamondParent, err := s.findDiamondParent(ctx, agentId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return 0, nil
}
return 0, err
}
return diamondParent.Id, nil
}
// giveRebateForUpgrade 发放升级返佣
2025-12-02 19:57:10 +08:00
// 注意:升级返佣信息记录在 agent_upgrade 表中rebate_agent_id 和 rebate_amount不需要在 agent_rebate 表中创建记录
func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Session, parentAgentId, upgradeAgentId int64, amount float64, orderNo string) error {
2025-11-27 13:09:54 +08:00
// 更新钱包余额
wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, parentAgentId)
if err != nil {
return errors.Wrapf(err, "查询钱包失败, agentId: %d", parentAgentId)
}
wallet.Balance += amount
wallet.TotalEarnings += amount
if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil {
return errors.Wrapf(err, "更新钱包失败")
}
return nil
}
// GetUpgradeFee 获取升级费用
2025-12-02 19:57:10 +08:00
func (s *AgentService) GetUpgradeFee(ctx context.Context, fromLevel, toLevel int64) (float64, error) {
2025-11-27 13:09:54 +08:00
if fromLevel == 1 && toLevel == 2 {
2025-12-02 19:57:10 +08:00
// 普通→黄金:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_gold_fee", 199)
2025-11-27 13:09:54 +08:00
} else if toLevel == 3 {
2025-12-02 19:57:10 +08:00
// 升级为钻石:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_fee", 980)
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
return 0, nil
2025-11-27 13:09:54 +08:00
}
// GetUpgradeRebate 获取升级返佣金额
2025-12-02 19:57:10 +08:00
func (s *AgentService) GetUpgradeRebate(ctx context.Context, fromLevel, toLevel int64) (float64, error) {
2025-11-27 13:09:54 +08:00
if fromLevel == 1 && toLevel == 2 {
2025-12-02 19:57:10 +08:00
// 普通→黄金返佣:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_gold_rebate", 139)
2025-11-27 13:09:54 +08:00
} else if toLevel == 3 {
2025-12-02 19:57:10 +08:00
// 升级为钻石返佣:从配置获取
return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_rebate", 680)
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
return 0, nil
2025-11-27 13:09:54 +08:00
}