package service import ( "context" "database/sql" "fmt" "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/google/uuid" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/sqlx" ) // AgentService 新代理系统服务 type AgentService struct { 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 AgentProductConfigModel model.AgentProductConfigModel AgentRealNameModel model.AgentRealNameModel AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel AgentFreezeTaskModel model.AgentFreezeTaskModel // 冻结任务模型(需要先运行SQL并生成model) } // 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, agentFreezeTaskModel model.AgentFreezeTaskModel, // 冻结任务模型(需要先运行SQL并生成model) ) *AgentService { return &AgentService{ config: c, OrderModel: orderModel, AgentModel: agentModel, AgentWalletModel: agentWalletModel, AgentRelationModel: agentRelationModel, AgentLinkModel: agentLinkModel, AgentOrderModel: agentOrderModel, AgentCommissionModel: agentCommissionModel, AgentRebateModel: agentRebateModel, AgentUpgradeModel: agentUpgradeModel, AgentWithdrawalModel: agentWithdrawalModel, AgentConfigModel: agentConfigModel, AgentProductConfigModel: agentProductConfigModel, AgentRealNameModel: agentRealNameModel, AgentWithdrawalTaxModel: agentWithdrawalTaxModel, AgentFreezeTaskModel: agentFreezeTaskModel, } } // 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) } // 4. 获取产品配置(必须存在) productConfig, err := s.AgentProductConfigModel.FindOneByProductId(ctx, order.ProductId) if err != nil { if errors.Is(err, model.ErrNotFound) { return errors.Wrapf(err, "产品配置不存在, productId: %d,请先在后台配置产品价格参数", order.ProductId) } return errors.Wrapf(err, "查询产品配置失败, productId: %d", order.ProductId) } // 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 actualBasePrice := basePrice + float64(levelBonus) // 5.3 计算提价成本(使用产品配置) priceThreshold := 0.0 priceFeeRate := 0.0 if productConfig.PriceThreshold.Valid { priceThreshold = productConfig.PriceThreshold.Float64 } if productConfig.PriceFeeRate.Valid { priceFeeRate = productConfig.PriceFeeRate.Float64 } priceCost := s.calculatePriceCost(agentOrder.SetPrice, priceThreshold, priceFeeRate) // 5.4 计算代理收益 agentProfit := agentOrder.SetPrice - actualBasePrice - priceCost // 5.5 更新代理订单记录 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, "更新代理订单失败") } // 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, "发放代理佣金失败") } // 5.7 分配等级加成返佣给上级链 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 }) return err } // getLevelBonus 获取等级加成(从配置表读取) func (s *AgentService) getLevelBonus(ctx context.Context, level int64) (int64, error) { var configKey string switch level { case 1: // 普通 configKey = "level_1_bonus" case 2: // 黄金 configKey = "level_2_bonus" case 3: // 钻石 configKey = "level_3_bonus" default: 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 } return int64(bonus), nil } // calculatePriceCost 计算提价成本 func (s *AgentService) calculatePriceCost(setPrice, priceThreshold, priceFeeRate float64) float64 { if setPrice <= priceThreshold { return 0 } return (setPrice - priceThreshold) * priceFeeRate } // giveAgentCommission 发放代理佣金 // orderPrice: 订单单价,用于判断是否需要冻结 // 返回:freezeTaskId(如果有冻结任务),error func (s *AgentService) giveAgentCommission(ctx context.Context, session sqlx.Session, agentId, orderId, productId string, amount float64, orderPrice float64) (string, error) { // 1. 创建佣金记录 commission := &model.AgentCommission{ Id: uuid.NewString(), AgentId: agentId, OrderId: orderId, ProductId: productId, Amount: amount, Status: 1, // 已发放 } _, err := s.AgentCommissionModel.Insert(ctx, session, commission) if err != nil { return "", errors.Wrapf(err, "创建佣金记录失败") } commissionId := commission.Id // 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) } // 2.2 判断订单单价是否达到冻结阈值 freezeAmount := 0.0 var freezeTaskId string = "" if orderPrice >= freezeThreshold { // 2.3 获取冻结比例配置(默认10%) freezeRatio, err := s.getConfigFloat(ctx, "commission_freeze_ratio") if err != nil { // 配置不存在时使用默认值0.1(10%) 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{ Id: uuid.NewString(), 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)), } _, err = s.AgentFreezeTaskModel.Insert(ctx, session, freezeTask) if err != nil { return "", errors.Wrapf(err, "创建冻结任务失败") } freezeTaskId = freezeTask.Id } } // 3. 更新钱包余额 wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, agentId) if err != nil { return "", errors.Wrapf(err, "查询钱包失败, agentId: %s", agentId) } // 实际到账金额 = 佣金金额 - 冻结金额 actualAmount := amount - freezeAmount wallet.Balance += actualAmount wallet.FrozenBalance += freezeAmount wallet.TotalEarnings += amount // 累计收益包含冻结部分 if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil { return "", errors.Wrapf(err, "更新钱包失败") } return freezeTaskId, nil } // distributeLevelBonus 分配等级加成返佣给上级链 func (s *AgentService) distributeLevelBonus(ctx context.Context, session sqlx.Session, agent *model.Agent, orderId, productId string, 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 } // 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 会自动跳过中间的所有普通代理, // // 直接向上查找到第一个钻石或黄金代理 func (s *AgentService) distributeNormalAgentBonus(ctx context.Context, session sqlx.Session, agent *model.Agent, orderId, productId string, 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 } // 2. 根据直接上级等级分配 switch parent.Level { 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元 } // 计算给直接上级黄金代理的返佣金额(不能超过总等级加成金额) goldAmount := goldRebateAmount // 默认3元 if goldAmount > amount { // 如果配置金额大于等级加成总额,则只给总额(防止配置错误导致负数) goldAmount = amount } // 发放返佣给直接上级黄金代理 // 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元归平台 } return nil 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元 } // 计算给直接上级的返佣金额(不能超过总等级加成金额) 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, "给直接上级返佣失败") } } // ========== 步骤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. 最后归平台(如果钻石和黄金都不存在) if diamondParent != nil { // ========== 情况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 } else if goldParent != nil { // ========== 情况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元 } // 计算给黄金上级的返佣金额 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, "给黄金上级返佣失败") } } // 超出最大限额的部分归平台(不需要记录) // 例如:剩余4元,给黄金3元,剩余1元归平台 return nil } // ========== 情况C:既没有钻石上级,也没有黄金上级 ========== // 场景示例: // - 普通(推广人) -> 普通(直接上级) -> 普通 -> 普通...(整个链路都是普通代理) // - 普通(推广人) -> 普通(直接上级) -> 无上级(已经是最顶层) // 剩余金额全部归平台(不需要记录) return nil default: // 未知等级,全部归平台 return nil } } // 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 } // giveRebate 发放返佣 func (s *AgentService) giveRebate(ctx context.Context, session sqlx.Session, agentId, sourceAgentId, orderId, productId string, amount float64, levelBonus int64, rebateType int64) error { // 1. 创建返佣记录 rebate := &model.AgentRebate{ Id: uuid.NewString(), 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 string) (*model.Agent, error) { return s.findDirectParent(ctx, agentId) } // findDirectParent 查找直接上级 func (s *AgentService) findDirectParent(ctx context.Context, agentId string) (*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 向上查找钻石上级 // // 功能说明:从指定代理开始,向上逐级查找第一个钻石代理(Level == 3) // // 查找规则: // - 自动跳过所有普通代理(Level == 1)和黄金代理(Level == 2) // - 只返回第一个找到的钻石代理 // - 如果没有找到钻石代理,返回 ErrNotFound // // 示例场景: // - 普通 -> 普通 -> 钻石:会找到钻石(跳过中间的普通代理) // - 普通 -> 黄金 -> 钻石:会找到钻石(跳过黄金代理) // - 普通 -> 普通 -> 黄金:返回 ErrNotFound(没有钻石) func (s *AgentService) findDiamondParent(ctx context.Context, agentId string) (*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 向上查找黄金上级 // // 功能说明:从指定代理开始,向上逐级查找第一个黄金代理(Level == 2) // // 查找规则: // - 自动跳过所有普通代理(Level == 1) // - 如果先遇到钻石代理(Level == 3),不会跳过,但会继续查找黄金代理 // 注意:在实际使用中,应该先调用 findDiamondParent,如果没找到钻石再调用此方法 // - 只返回第一个找到的黄金代理 // - 如果没有找到黄金代理,返回 ErrNotFound // // 示例场景: // - 普通 -> 普通 -> 黄金:会找到黄金(跳过中间的普通代理) // - 普通 -> 黄金:会找到黄金 // - 普通 -> 普通 -> 钻石:返回 ErrNotFound(跳过钻石,继续查找黄金,但找不到) func (s *AgentService) findGoldParent(ctx context.Context, agentId string) (*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 string, toLevel int64, upgradeType int64, upgradeFee, rebateAmount float64, orderNo string, operatorAgentId string) 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 { // 返佣给原直接上级 if err := s.giveRebateForUpgrade(transCtx, session, parent.Id, agentId, rebateAmount, orderNo); err != nil { 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 { // 脱离前先获取原直接上级及其上级的信息(用于后续重新连接) oldParent, oldParentErr := s.findDirectParent(transCtx, agentId) var grandparentId string = "" if oldParentErr == nil && oldParent != nil { // 查找原上级的上级 grandparent, grandparentErr := s.findDirectParent(transCtx, oldParent.Id) if grandparentErr == nil && grandparent != nil { grandparentId = grandparent.Id } } // 脱离直接上级关系 if err := s.detachFromParent(transCtx, session, agentId); err != nil { return errors.Wrapf(err, "脱离直接上级关系失败") } // 脱离后,尝试连接到原上级的上级 if grandparentId != "" { if err := s.reconnectToGrandparent(transCtx, session, agentId, toLevel, grandparentId); err != nil { return errors.Wrapf(err, "重新连接上级关系失败") } } } // 5. 如果升级为钻石,独立成新团队 if toLevel == 3 { agent.TeamLeaderId = sql.NullString{String: 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 != "" { agent.TeamLeaderId = sql.NullString{String: 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 { if newLevel == 2 || newLevel == 3 { // 黄金或钻石同级需要脱离 return true, nil } // 普通代理同级(newLevel == 1 && parent.Level == 1)不需要脱离 } return false, nil } // detachFromParent 脱离直接上级关系 func (s *AgentService) detachFromParent(ctx context.Context, session sqlx.Session, agentId string) 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 } // reconnectToGrandparent 重新连接到原上级的上级(如果存在且符合条件) func (s *AgentService) reconnectToGrandparent(ctx context.Context, session sqlx.Session, agentId string, newLevel int64, grandparentId string) 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{ Id: uuid.NewString(), ParentId: grandparent.Id, ChildId: agentId, RelationType: 1, // 直接关系 } if _, err := s.AgentRelationModel.Insert(ctx, session, relation); err != nil { return errors.Wrapf(err, "创建新关系失败") } return nil } // updateChildrenTeamLeader 更新所有下级的团队首领 func (s *AgentService) updateChildrenTeamLeader(ctx context.Context, session sqlx.Session, agentId, teamLeaderId string) error { // 递归更新所有下级 var updateChildren func(string) error updateChildren = func(parentId string) 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.NullString{String: teamLeaderId, Valid: true} if err := s.AgentModel.UpdateWithVersion(ctx, session, child); err != nil { return errors.Wrapf(err, "更新下级团队首领失败, childId: %s", 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 string) (string, error) { diamondParent, err := s.findDiamondParent(ctx, agentId) if err != nil { if errors.Is(err, model.ErrNotFound) { return "", nil } return "", err } return diamondParent.Id, nil } // giveRebateForUpgrade 发放升级返佣 // 注意:升级返佣信息记录在 agent_upgrade 表中(rebate_agent_id 和 rebate_amount),不需要在 agent_rebate 表中创建记录 func (s *AgentService) giveRebateForUpgrade(ctx context.Context, session sqlx.Session, parentAgentId, upgradeAgentId string, amount float64, orderNo string) error { // 更新钱包余额 wallet, err := s.AgentWalletModel.FindOneByAgentId(ctx, parentAgentId) if err != nil { return errors.Wrapf(err, "查询钱包失败, agentId: %s", parentAgentId) } wallet.Balance += amount wallet.TotalEarnings += amount if err := s.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil { return errors.Wrapf(err, "更新钱包失败") } return nil } // GetUpgradeFee 获取升级费用 func (s *AgentService) GetUpgradeFee(ctx context.Context, fromLevel, toLevel int64) (float64, error) { if fromLevel == 1 && toLevel == 2 { // 普通→黄金:从配置获取 return s.getRebateConfigFloat(ctx, "upgrade_to_gold_fee", 199) } else if toLevel == 3 { // 升级为钻石:从配置获取 return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_fee", 980) } return 0, nil } // GetUpgradeRebate 获取升级返佣金额 func (s *AgentService) GetUpgradeRebate(ctx context.Context, fromLevel, toLevel int64) (float64, error) { if fromLevel == 1 && toLevel == 2 { // 普通→黄金返佣:从配置获取 return s.getRebateConfigFloat(ctx, "upgrade_to_gold_rebate", 139) } else if toLevel == 3 { // 升级为钻石返佣:从配置获取 return s.getRebateConfigFloat(ctx, "upgrade_to_diamond_rebate", 680) } return 0, nil }