This commit is contained in:
2025-12-13 17:44:18 +08:00
commit c7a30fb094
599 changed files with 65988 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
package agent
import (
"context"
"database/sql"
"fmt"
"os"
"strconv"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ApplyForAgentLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewApplyForAgentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyForAgentLogic {
return &ApplyForAgentLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *types.AgentApplyResp, err error) {
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理申请失败, %v", err)
}
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
}
if req.Referrer == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
}
// 2. 校验验证码(开发环境下跳过验证码校验)
if os.Getenv("ENV") != "development" {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码失败, %v", err)
}
if cacheCode != req.Code {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "")
}
}
var userID string
transErr := l.svcCtx.AgentModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 1. 处理用户注册/绑定
user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})
if findUserErr != nil && !errors.Is(findUserErr, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败, %v", findUserErr)
}
if user == nil {
// 用户不存在,注册新用户
if claims != nil && claims.UserType == model.UserTypeNormal {
return errors.Wrapf(xerr.NewErrMsg("当前用户已注册,请输入注册的手机号"), "")
}
userID, err = l.svcCtx.UserService.RegisterUser(l.ctx, encryptedMobile)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "注册用户失败: %v", err)
}
} else {
// 用户已存在
if claims != nil && claims.UserType == model.UserTypeTemp {
// 临时用户,检查手机号是否已绑定其他微信号
userAuth, findUserAuthErr := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, user.Id, claims.AuthType)
if findUserAuthErr != nil && !errors.Is(findUserAuthErr, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户认证失败, %v", findUserAuthErr)
}
if userAuth != nil && userAuth.AuthKey != claims.AuthKey {
return errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
}
// 临时用户,转为正式用户
err = l.svcCtx.UserService.TempUserBindUser(l.ctx, session, user.Id)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定用户失败: %v", err)
}
}
userID = user.Id
}
// 3. 检查是否已是代理
existingAgent, err := l.svcCtx.AgentModel.FindOneByUserId(transCtx, userID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
if existingAgent != nil {
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
}
var inviteCodeModel *model.AgentInviteCode
var parentAgentId string
var targetLevel int64
inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, req.Referrer)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
if inviteCodeModel != nil {
if inviteCodeModel.Status != 0 {
if inviteCodeModel.Status == 1 {
return errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
}
return errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
}
if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) {
return errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
}
targetLevel = inviteCodeModel.TargetLevel
if inviteCodeModel.AgentId.Valid {
parentAgentId = inviteCodeModel.AgentId.String
}
} else {
if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 {
parentAgent, err := l.findAgentByCode(transCtx, codeVal)
if err != nil {
return err
}
parentAgentId = parentAgent.Id
targetLevel = 1
} else {
encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey)
agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "")
if findErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr)
}
if len(agents) == 0 {
return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "")
}
parentAgentId = agents[0].Id
targetLevel = 1
}
}
// 4.5 创建代理记录
newAgent := &model.Agent{
Id: uuid.NewString(),
UserId: userID,
Level: targetLevel,
Mobile: encryptedMobile,
}
if req.Region != "" {
newAgent.Region = sql.NullString{String: req.Region, Valid: true}
}
// 4.6 处理上级关系
if parentAgentId != "" {
// 代理发放的邀请码,成为该代理的下级
parentAgent, err := l.svcCtx.AgentModel.FindOne(transCtx, parentAgentId)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", err)
}
// 验证关系是否允许
if newAgent.Level > parentAgent.Level {
return errors.Wrapf(xerr.NewErrMsg("代理等级不能高于上级代理"), "")
}
// 查找团队首领
teamLeaderId, err := l.findTeamLeader(transCtx, parentAgent.Id)
if err != nil {
return errors.Wrapf(err, "查找团队首领失败")
}
if teamLeaderId != "" {
newAgent.TeamLeaderId = sql.NullString{String: teamLeaderId, Valid: true}
}
newAgent.AgentCode = 0
_, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent)
if err != nil {
return errors.Wrapf(err, "创建代理记录失败")
}
// 已设置newAgent.Id为UUID
// 建立关系
relation := &model.AgentRelation{
Id: uuid.NewString(),
ParentId: parentAgent.Id,
ChildId: newAgent.Id,
RelationType: 1,
}
if _, err := l.svcCtx.AgentRelationModel.Insert(transCtx, session, relation); err != nil {
return errors.Wrapf(err, "建立代理关系失败")
}
} else {
// 平台发放的钻石邀请码,独立成团队
if targetLevel == 3 {
newAgent.AgentCode = 0
_, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent)
if err != nil {
return errors.Wrapf(err, "创建代理记录失败")
}
// 设置自己为团队首领
newAgent.TeamLeaderId = sql.NullString{String: newAgent.Id, Valid: true}
if err := l.svcCtx.AgentModel.UpdateInTransaction(transCtx, session, newAgent); err != nil {
return errors.Wrapf(err, "更新团队首领失败")
}
} else {
newAgent.AgentCode = 0
_, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent)
if err != nil {
return errors.Wrapf(err, "创建代理记录失败")
}
}
}
// 4.7 初始化钱包
wallet := &model.AgentWallet{
Id: uuid.NewString(),
AgentId: newAgent.Id,
}
if _, err := l.svcCtx.AgentWalletModel.Insert(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "初始化钱包失败")
}
// 4.8 更新邀请码状态
// 钻石级别的邀请码只能使用一次,使用后立即失效
// 普通级别的邀请码可以无限使用,不更新状态
if targetLevel == 3 {
// 钻石邀请码:使用后失效
inviteCodeModel.Status = 1 // 已使用(使用后立即失效)
}
// 记录使用信息(用于统计,普通邀请码可以多次使用)
inviteCodeModel.UsedUserId = sql.NullString{String: userID, Valid: true}
if inviteCodeModel != nil {
inviteCodeModel.UsedAgentId = sql.NullString{String: newAgent.Id, Valid: true}
inviteCodeModel.UsedTime = sql.NullTime{Time: time.Now(), Valid: true}
if err := l.svcCtx.AgentInviteCodeModel.UpdateWithVersion(transCtx, session, inviteCodeModel); err != nil {
return errors.Wrapf(err, "更新邀请码状态失败")
}
}
// 4.9 记录邀请码使用历史(用于统计和查询)
usage := &model.AgentInviteCodeUsage{
Id: uuid.NewString(),
InviteCodeId: inviteCodeModel.Id,
Code: inviteCodeModel.Code,
UserId: userID,
AgentId: newAgent.Id,
AgentLevel: targetLevel,
UsedTime: time.Now(),
}
if _, err := l.svcCtx.AgentInviteCodeUsageModel.Insert(transCtx, session, usage); err != nil {
return errors.Wrapf(err, "记录邀请码使用历史失败")
}
return nil
})
if transErr != nil {
return nil, transErr
}
// 6. 生成并返回token
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成token失败: %v", err)
}
now := time.Now().Unix()
agent, _ := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
var code int64
if agent != nil {
code = agent.AgentCode
}
return &types.AgentApplyResp{
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
AgentCode: code,
}, nil
}
func (l *ApplyForAgentLogic) findAgentByCode(ctx context.Context, code int64) (*model.Agent, error) {
builder := l.svcCtx.AgentModel.SelectBuilder().Where("agent_code = ? AND del_state = ?", code, globalkey.DelStateNo).Limit(1)
agents, err := l.svcCtx.AgentModel.FindAll(ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", err)
}
if len(agents) == 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("上级邀请码不存在"), "")
}
return agents[0], nil
}
func (l *ApplyForAgentLogic) allocateAgentCode(ctx context.Context, session sqlx.Session) (int64, error) {
builder := l.svcCtx.AgentModel.SelectBuilder().OrderBy("agent_code DESC").Limit(1)
rows, err := l.svcCtx.AgentModel.FindAll(ctx, builder, "")
if err != nil {
return 0, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理编码失败, %v", err)
}
var next int64 = 16800
if len(rows) > 0 && rows[0].AgentCode > 0 {
next = rows[0].AgentCode + 1
}
return next, nil
}
// findTeamLeader 查找团队首领(钻石代理)
func (l *ApplyForAgentLogic) findTeamLeader(ctx context.Context, agentId string) (string, error) {
currentId := agentId
maxDepth := 100
depth := 0
for depth < maxDepth {
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("child_id = ? AND relation_type = ? AND del_state = ?", currentId, 1, 0)
relations, err := l.svcCtx.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return "", err
}
if len(relations) == 0 {
agent, err := l.svcCtx.AgentModel.FindOne(ctx, currentId)
if err != nil {
return "", err
}
if agent.Level == 3 {
return agent.Id, nil
}
return "", nil
}
parentAgent, err := l.svcCtx.AgentModel.FindOne(ctx, relations[0].ParentId)
if err != nil {
return "", err
}
if parentAgent.Level == 3 {
return parentAgent.Id, nil
}
currentId = parentAgent.Id
depth++
}
return "", nil
}

View File

@@ -0,0 +1,125 @@
package agent
import (
"context"
"database/sql"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ApplyUpgradeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewApplyUpgradeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyUpgradeLogic {
return &ApplyUpgradeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ApplyUpgradeLogic) ApplyUpgrade(req *types.ApplyUpgradeReq) (resp *types.ApplyUpgradeResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
fromLevel := agent.Level
toLevel := req.ToLevel
// 2. 验证升级条件
if !l.canUpgrade(agent.Level, toLevel, 1) {
return nil, errors.Wrapf(xerr.NewErrMsg("升级条件不满足"), "")
}
// 3. 计算升级费用和返佣
upgradeFee, err := l.svcCtx.AgentService.GetUpgradeFee(l.ctx, fromLevel, toLevel)
if err != nil {
return nil, errors.Wrapf(err, "获取升级费用失败")
}
rebateAmount, err := l.svcCtx.AgentService.GetUpgradeRebate(l.ctx, fromLevel, toLevel)
if err != nil {
return nil, errors.Wrapf(err, "获取升级返佣金额失败")
}
// 4. 查找原直接上级(用于返佣)
var rebateAgentId string
parent, err := l.svcCtx.AgentService.FindDirectParent(l.ctx, agent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(err, "查找直接上级失败")
}
if parent != nil {
rebateAgentId = parent.Id
}
// 5. 创建升级记录(待支付状态)
var upgradeId string
err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 5.1 创建升级记录(状态为待支付)
upgradeRecord := &model.AgentUpgrade{
Id: uuid.NewString(),
AgentId: agent.Id,
FromLevel: fromLevel,
ToLevel: toLevel,
UpgradeType: 1, // 自主付费
UpgradeFee: upgradeFee,
RebateAmount: rebateAmount,
Status: 1, // 待支付1=待支付2=已支付3=已完成4=已取消)
}
if rebateAgentId != "" {
upgradeRecord.RebateAgentId = sql.NullString{String: rebateAgentId, Valid: true}
}
_, err := l.svcCtx.AgentUpgradeModel.Insert(transCtx, session, upgradeRecord)
if err != nil {
return errors.Wrapf(err, "创建升级记录失败")
}
// 注意:升级操作将在支付成功后通过支付回调完成
upgradeId = upgradeRecord.Id
return nil
})
if err != nil {
return nil, err
}
// 返回响应(订单号将在支付接口中生成)
return &types.ApplyUpgradeResp{
UpgradeId: upgradeId,
OrderNo: "", // 将在支付接口中生成
}, nil
}
// canUpgrade 检查是否可以升级
func (l *ApplyUpgradeLogic) canUpgrade(fromLevel, toLevel int64, upgradeType int64) bool {
if upgradeType == 1 { // 自主付费
if fromLevel == 1 { // 普通
return toLevel == 2 || toLevel == 3 // 可以升级为黄金或钻石
} else if fromLevel == 2 { // 黄金
return toLevel == 3 // 可以升级为钻石
}
}
return false
}

View File

@@ -0,0 +1,226 @@
package agent
import (
"context"
"fmt"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type ApplyWithdrawalLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewApplyWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyWithdrawalLogic {
return &ApplyWithdrawalLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ApplyWithdrawalLogic) ApplyWithdrawal(req *types.ApplyWithdrawalReq) (resp *types.ApplyWithdrawalResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证实名认证
realName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("请先完成实名认证"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询实名认证失败, %v", err)
}
// 检查是否已通过三要素核验verify_time不为空表示已通过
if !realName.VerifyTime.Valid {
return nil, errors.Wrapf(xerr.NewErrMsg("请先完成实名认证"), "")
}
// 3. 验证提现金额
if req.Amount <= 0 {
return nil, errors.Wrapf(xerr.NewErrMsg("提现金额必须大于0"), "")
}
// 4. 获取钱包信息
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包失败, %v", err)
}
// 5. 验证余额
if wallet.Balance < req.Amount {
return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("余额不足,当前余额:%.2f", wallet.Balance)), "")
}
// 6. 计算税费
yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month()))
taxInfo, err := l.calculateTax(l.ctx, agent.Id, req.Amount, yearMonth)
if err != nil {
return nil, errors.Wrapf(err, "计算税费失败")
}
// 7. 生成提现单号
withdrawNo := fmt.Sprintf("WD%d%d", time.Now().Unix(), agent.Id)
// 8. 使用事务处理提现申请
var withdrawalId string
err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 8.1 冻结余额
wallet.FrozenBalance += req.Amount
wallet.Balance -= req.Amount
if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "冻结余额失败")
}
// 8.2 创建提现记录
withdrawal := &model.AgentWithdrawal{
Id: uuid.New().String(),
AgentId: agent.Id,
WithdrawNo: withdrawNo,
PayeeAccount: req.PayeeAccount,
PayeeName: req.PayeeName,
Amount: req.Amount,
ActualAmount: taxInfo.ActualAmount,
TaxAmount: taxInfo.TaxAmount,
Status: 1, // 处理中(待审核)
}
_, err := l.svcCtx.AgentWithdrawalModel.Insert(transCtx, session, withdrawal)
if err != nil {
return errors.Wrapf(err, "创建提现记录失败")
}
withdrawalId = withdrawal.Id
// 8.3 创建扣税记录
taxRecord := &model.AgentWithdrawalTax{
AgentId: agent.Id,
WithdrawalId: withdrawalId,
YearMonth: yearMonth,
WithdrawalAmount: req.Amount,
TaxableAmount: taxInfo.TaxableAmount,
TaxRate: taxInfo.TaxRate,
TaxAmount: taxInfo.TaxAmount,
ActualAmount: taxInfo.ActualAmount,
TaxStatus: 1, // 待扣税
Remark: lzUtils.StringToNullString("提现申请"),
}
if _, err := l.svcCtx.AgentWithdrawalTaxModel.Insert(transCtx, session, taxRecord); err != nil {
return errors.Wrapf(err, "创建扣税记录失败")
}
return nil
})
if err != nil {
return nil, err
}
return &types.ApplyWithdrawalResp{
WithdrawalId: withdrawalId,
WithdrawalNo: withdrawNo,
}, nil
}
// TaxInfo 税费信息
type TaxInfo struct {
TaxableAmount float64 // 应税金额
TaxRate float64 // 税率
TaxAmount float64 // 税费金额
ActualAmount float64 // 实际到账金额
}
// calculateTax 计算税费
func (l *ApplyWithdrawalLogic) calculateTax(ctx context.Context, agentId string, amount float64, yearMonth int64) (*TaxInfo, error) {
// 获取税率配置默认6%
taxRate := 0.06
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(ctx, "tax_rate")
if err == nil {
if parsedRate, parseErr := l.parseFloat(config.ConfigValue); parseErr == nil {
taxRate = parsedRate
}
}
// 查询本月已提现金额
builder := l.svcCtx.AgentWithdrawalTaxModel.SelectBuilder().
Where("agent_id = ? AND year_month = ? AND del_state = ?", agentId, yearMonth, globalkey.DelStateNo)
taxRecords, err := l.svcCtx.AgentWithdrawalTaxModel.FindAll(ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(err, "查询月度提现记录失败")
}
// 计算本月累计提现金额
monthlyTotal := 0.0
for _, record := range taxRecords {
monthlyTotal += record.WithdrawalAmount
}
// 获取免税额度配置默认0即无免税额度
exemptionAmount := 0.0
exemptionConfig, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(ctx, "tax_exemption_amount")
if err == nil {
if parsedAmount, parseErr := l.parseFloat(exemptionConfig.ConfigValue); parseErr == nil {
exemptionAmount = parsedAmount
}
}
// 计算应税金额
// 如果本月累计 + 本次提现金额 <= 免税额度,则本次提现免税
// 否则,应税金额 = 本次提现金额 - (免税额度 - 本月累计)(如果免税额度 > 本月累计)
taxableAmount := amount
if exemptionAmount > 0 {
remainingExemption := exemptionAmount - monthlyTotal
if remainingExemption > 0 {
if amount <= remainingExemption {
// 本次提现完全免税
taxableAmount = 0
} else {
// 部分免税
taxableAmount = amount - remainingExemption
}
}
}
// 计算税费
taxAmount := taxableAmount * taxRate
actualAmount := amount - taxAmount
return &TaxInfo{
TaxableAmount: taxableAmount,
TaxRate: taxRate,
TaxAmount: taxAmount,
ActualAmount: actualAmount,
}, nil
}
// parseFloat 解析浮点数
func (l *ApplyWithdrawalLogic) parseFloat(s string) (float64, error) {
var result float64
_, err := fmt.Sscanf(s, "%f", &result)
return result, err
}

View File

@@ -0,0 +1,72 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type DeleteInviteCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeleteInviteCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteInviteCodeLogic {
return &DeleteInviteCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteInviteCodeLogic) DeleteInviteCode(req *types.DeleteInviteCodeReq) (resp *types.DeleteInviteCodeResp, err error) {
// 1. 获取当前代理信息
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 查询邀请码是否存在且属于当前代理
inviteCode, err := l.svcCtx.AgentInviteCodeModel.FindOne(l.ctx, req.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
// 3. 验证邀请码是否属于当前代理
if !inviteCode.AgentId.Valid || inviteCode.AgentId.String != agent.Id {
return nil, errors.Wrapf(xerr.NewErrMsg("无权删除此邀请码"), "")
}
// 4. 检查邀请码是否已被使用
if inviteCode.Status == 1 {
return nil, errors.Wrapf(xerr.NewErrMsg("已使用的邀请码无法删除"), "")
}
// 5. 执行软删除
err = l.svcCtx.AgentInviteCodeModel.DeleteSoft(l.ctx, nil, inviteCode)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除邀请码失败, %v", err)
}
return &types.DeleteInviteCodeResp{}, nil
}

View File

@@ -0,0 +1,115 @@
package agent
import (
"context"
"database/sql"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/tool"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GenerateInviteCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGenerateInviteCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateInviteCodeLogic {
return &GenerateInviteCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GenerateInviteCodeLogic) GenerateInviteCode(req *types.GenerateInviteCodeReq) (resp *types.GenerateInviteCodeResp, err error) {
// 1. 获取当前代理信息
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证生成数量
if req.Count <= 0 || req.Count > 100 {
return nil, errors.Wrapf(xerr.NewErrMsg("生成数量必须在1-100之间"), "")
}
// 3. 生成邀请码
codes := make([]string, 0, req.Count)
var expireTime sql.NullTime
if req.ExpireDays > 0 {
expireTime = sql.NullTime{
Time: time.Now().AddDate(0, 0, int(req.ExpireDays)),
Valid: true,
}
}
err = l.svcCtx.AgentInviteCodeModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
for i := int64(0); i < req.Count; i++ {
// 生成8位随机邀请码大小写字母+数字)
var code string
maxRetries := 10 // 最大重试次数
for retry := 0; retry < maxRetries; retry++ {
code = tool.Krand(8, tool.KC_RAND_KIND_ALL)
// 检查邀请码是否已存在
_, err := l.svcCtx.AgentInviteCodeModel.FindOneByCode(transCtx, code)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 邀请码不存在,可以使用
break
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查邀请码失败, %v", err)
}
// 邀请码已存在,继续生成
if retry == maxRetries-1 {
return errors.Wrapf(xerr.NewErrMsg("生成邀请码失败,请重试"), "")
}
}
// 创建邀请码记录
inviteCode := &model.AgentInviteCode{
Code: code,
AgentId: sql.NullString{String: agent.Id, Valid: true},
TargetLevel: 1, // 代理发放的邀请码,目标等级为普通代理
Status: 0, // 未使用
ExpireTime: expireTime,
Remark: sql.NullString{String: req.Remark, Valid: req.Remark != ""},
}
_, err := l.svcCtx.AgentInviteCodeModel.Insert(transCtx, session, inviteCode)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建邀请码失败, %v", err)
}
codes = append(codes, code)
}
return nil
})
if err != nil {
return nil, err
}
return &types.GenerateInviteCodeResp{
Codes: codes,
}, nil
}

View File

@@ -0,0 +1,351 @@
package agent
import (
"context"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/tool"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/google/uuid"
"github.com/zeromicro/go-zero/core/logx"
)
type GeneratingLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGeneratingLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GeneratingLinkLogic {
return &GeneratingLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) (resp *types.AgentGeneratingLinkResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成推广链接失败, %v", err)
}
// 1. 获取代理信息
agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 获取产品配置(必须存在)
productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, req.ProductId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品配置不存在, productId: %d请先在后台配置产品价格参数", req.ProductId)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, productId: %d, %v", req.ProductId, err)
}
// 3. 获取等级加成配置
levelBonus, err := l.getLevelBonus(agentModel.Level)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级加成配置失败, %v", err)
}
basePrice := productConfig.BasePrice
actualBasePrice := basePrice + float64(levelBonus)
systemMaxPrice := productConfig.SystemMaxPrice
upliftAmount, err := l.getLevelMaxUpliftAmount(agentModel.Level)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级上调金额失败, %v", err)
}
levelMaxPrice := systemMaxPrice + upliftAmount
if req.SetPrice < actualBasePrice || req.SetPrice > levelMaxPrice {
return nil, errors.Wrapf(xerr.NewErrMsg("设定价格必须在 %.2f 到 %.2f 之间"), "设定价格必须在 %.2f 到 %.2f 之间", actualBasePrice, levelMaxPrice)
}
// 6. 检查是否已存在相同的链接(同一代理、同一产品、同一价格)
builder := l.svcCtx.AgentLinkModel.SelectBuilder().Where(squirrel.And{
squirrel.Eq{"agent_id": agentModel.Id},
squirrel.Eq{"product_id": req.ProductId},
squirrel.Eq{"set_price": req.SetPrice},
squirrel.Eq{"del_state": globalkey.DelStateNo},
})
existingLinks, err := l.svcCtx.AgentLinkModel.FindAll(l.ctx, builder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询推广链接失败, %v", err)
}
if len(existingLinks) > 0 {
// 已存在,检查是否有短链,如果没有则生成
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
shortLink, err := l.getOrCreateShortLink(1, existingLinks[0].Id, "", existingLinks[0].LinkIdentifier, "", targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取或创建短链失败, %v", err)
}
return &types.AgentGeneratingLinkResp{
LinkIdentifier: existingLinks[0].LinkIdentifier,
FullLink: shortLink,
}, nil
}
// 7. 生成推广链接标识
var agentIdentifier types.AgentIdentifier
agentIdentifier.AgentID = agentModel.Id
agentIdentifier.ProductID = req.ProductId
agentIdentifier.SetPrice = req.SetPrice
agentIdentifierByte, err := json.Marshal(agentIdentifier)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "序列化标识失败, %v", err)
}
// 8. 加密链接标识
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取AES密钥失败: %+v", decodeErr)
}
encrypted, err := crypto.AesEncryptURL(agentIdentifierByte, key)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密链接标识失败, %v", err)
}
// 9. 保存推广链接
agentLink := &model.AgentLink{
// Id: uuid.NewString(),
AgentId: agentModel.Id,
UserId: userID,
ProductId: req.ProductId,
LinkIdentifier: encrypted,
SetPrice: req.SetPrice,
ActualBasePrice: actualBasePrice,
}
_, err = l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存推广链接失败, %v", err)
}
linkId := agentLink.Id
// 使用默认target_path如果未提供
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
// 生成短链类型1=推广报告)
shortLink, err := l.createShortLink(1, linkId, "", encrypted, "", targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err)
}
return &types.AgentGeneratingLinkResp{
LinkIdentifier: encrypted,
FullLink: shortLink,
}, nil
}
// getLevelBonus 获取等级加成(从配置表读取)
func (l *GeneratingLinkLogic) getLevelBonus(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
}
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, configKey)
if err != nil {
// 配置不存在时返回默认值
l.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
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return int64(value), nil
}
func (l *GeneratingLinkLogic) getLevelMaxUpliftAmount(level int64) (float64, error) {
var key string
switch level {
case 2:
key = "gold_max_uplift_amount"
case 3:
key = "diamond_max_uplift_amount"
default:
return 0, nil
}
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, key)
if err != nil {
return 0, nil
}
v, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, nil
}
if v < 0 {
return 0, nil
}
return v, nil
}
// getOrCreateShortLink 获取或创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
// linkId: 推广链接ID仅推广报告使用
// inviteCodeId: 邀请码ID仅邀请好友使用
// linkIdentifier: 推广链接标识(仅推广报告使用)
// inviteCode: 邀请码(仅邀请好友使用)
// targetPath: 目标地址(前端传入)
func (l *GeneratingLinkLogic) getOrCreateShortLink(linkType int64, linkId, inviteCodeId string, linkIdentifier, inviteCode, targetPath string) (string, error) {
// 先查询是否已存在短链
var existingShortLink *model.AgentShortLink
var err error
if linkType == 1 {
// 推广报告类型使用link_id查询
if linkId != "" {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByLinkIdTypeDelState(l.ctx, sql.NullString{String: linkId, Valid: true}, linkType, globalkey.DelStateNo)
}
} else {
// 邀请好友类型使用invite_code_id查询
if inviteCodeId != "" {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: true}, linkType, globalkey.DelStateNo)
}
}
if err == nil && existingShortLink != nil {
// 已存在短链,直接返回
return l.buildShortLinkURL(existingShortLink.ShortCode), nil
}
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(err, "查询短链失败")
}
// 不存在,创建新的短链
return l.createShortLink(linkType, linkId, inviteCodeId, linkIdentifier, inviteCode, targetPath)
}
// createShortLink 创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
func (l *GeneratingLinkLogic) createShortLink(linkType int64, linkId, inviteCodeId string, linkIdentifier, inviteCode, targetPath string) (string, error) {
promotionConfig := l.svcCtx.Config.Promotion
// 如果没有配置推广域名,返回空字符串(保持向后兼容)
if promotionConfig.PromotionDomain == "" {
l.Errorf("推广域名未配置,返回空链接")
return "", nil
}
// 验证target_path
if targetPath == "" {
return "", errors.Wrapf(xerr.NewErrMsg("目标地址不能为空"), "")
}
// 对于推广报告类型,将 linkIdentifier 拼接到 target_path
if linkType == 1 && linkIdentifier != "" {
// 如果 target_path 以 / 结尾,直接拼接 linkIdentifier
if strings.HasSuffix(targetPath, "/") {
targetPath = targetPath + url.QueryEscape(linkIdentifier)
} else {
// 否则在末尾添加 / 再拼接
targetPath = targetPath + "/" + url.QueryEscape(linkIdentifier)
}
}
// 生成短链标识6位随机字符串大小写字母+数字)
var shortCode string
maxRetries := 10 // 最大重试次数
for retry := 0; retry < maxRetries; retry++ {
shortCode = tool.Krand(6, tool.KC_RAND_KIND_ALL)
// 检查短链标识是否已存在
_, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 短链标识不存在,可以使用
break
}
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查短链标识失败, %v", err)
}
// 短链标识已存在,继续生成
if retry == maxRetries-1 {
return "", errors.Wrapf(xerr.NewErrMsg("生成短链失败,请重试"), "")
}
}
// 创建短链记录
shortLink := &model.AgentShortLink{
Id: uuid.NewString(),
Type: linkType,
ShortCode: shortCode,
TargetPath: targetPath,
PromotionDomain: promotionConfig.PromotionDomain,
}
// 根据类型设置对应字段
if linkType == 1 {
// 推广报告类型
shortLink.LinkId = sql.NullString{String: linkId, Valid: linkId != ""}
if linkIdentifier != "" {
shortLink.LinkIdentifier = sql.NullString{String: linkIdentifier, Valid: true}
}
} else if linkType == 2 {
// 邀请好友类型
shortLink.InviteCodeId = sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}
if inviteCode != "" {
shortLink.InviteCode = sql.NullString{String: inviteCode, Valid: true}
}
}
_, err := l.svcCtx.AgentShortLinkModel.Insert(l.ctx, nil, shortLink)
if err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存短链失败, %v", err)
}
return l.buildShortLinkURL(shortCode), nil
}
// buildShortLinkURL 构建短链URL
func (l *GeneratingLinkLogic) buildShortLinkURL(shortCode string) string {
promotionConfig := l.svcCtx.Config.Promotion
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, shortCode)
}

View File

@@ -0,0 +1,115 @@
package agent
import (
"context"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"qnc-server/app/main/model"
"github.com/zeromicro/go-zero/core/logx"
)
type GetAgentInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetAgentInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentInfoLogic {
return &GetAgentInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetAgentInfoLogic) GetAgentInfo() (resp *types.AgentInfoResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 不是代理,返回空信息
return &types.AgentInfoResp{
AgentId: "",
Level: 0,
LevelName: "",
Region: "",
Mobile: "",
WechatId: "",
TeamLeaderId: "",
IsRealName: false,
}, nil
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 解密手机号
mobile, err := crypto.DecryptMobile(agent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密手机号失败: %v", err)
}
// 查询实名认证状态
isRealName := false
agentRealName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询实名认证失败, %v", err)
}
if agentRealName != nil && agentRealName.VerifyTime.Valid { // verify_time不为空表示已通过三要素核验
isRealName = true
}
// 获取微信号
wechatId := ""
if agent.WechatId.Valid {
wechatId = agent.WechatId.String
}
// 获取团队首领ID
teamLeaderId := ""
if agent.TeamLeaderId.Valid {
teamLeaderId = agent.TeamLeaderId.String
}
// 获取区域
region := ""
if agent.Region.Valid {
region = agent.Region.String
}
return &types.AgentInfoResp{
AgentId: agent.Id,
Level: agent.Level,
LevelName: l.getLevelName(agent.Level),
Region: region,
Mobile: mobile,
WechatId: wechatId,
TeamLeaderId: teamLeaderId,
IsRealName: isRealName,
AgentCode: agent.AgentCode,
}, nil
}
// getLevelName 获取等级名称
func (l *GetAgentInfoLogic) getLevelName(level int64) string {
switch level {
case 1:
return "普通"
case 2:
return "黄金"
case 3:
return "钻石"
default:
return ""
}
}

View File

@@ -0,0 +1,170 @@
package agent
import (
"context"
"strconv"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetAgentProductConfigLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetAgentProductConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentProductConfigLogic {
return &GetAgentProductConfigLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentProductConfigResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 获取等级加成配置(从系统配置表读取)
levelBonus, err := l.getLevelBonus(agentModel.Level)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级加成配置失败, %v", err)
}
// 3. 查询所有产品配置
builder := l.svcCtx.AgentProductConfigModel.SelectBuilder()
productConfigs, err := l.svcCtx.AgentProductConfigModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品配置失败, %v", err)
}
// 4. 组装响应数据(通过 product_id 查询产品名称和英文标识)
var respList []types.ProductConfigItem
for _, productConfig := range productConfigs {
// 通过 product_id 查询产品信息获取产品名称和英文标识
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, productConfig.ProductId)
productName := ""
productEn := ""
if err == nil {
productName = product.ProductName
productEn = product.ProductEn
} else {
// 如果产品不存在,记录日志但不影响主流程
l.Infof("查询产品信息失败, productId: %d, err: %v", productConfig.ProductId, err)
}
// 使用产品配置的基础底价(必须配置)
productBasePrice := productConfig.BasePrice
// 计算该产品的实际底价
productActualBasePrice := productBasePrice + float64(levelBonus)
priceRangeMin := productActualBasePrice
upliftAmount, _ := l.getLevelMaxUpliftAmount(agentModel.Level)
priceRangeMax := productConfig.SystemMaxPrice + upliftAmount
// 使用产品配置的提价阈值和手续费比例如果为NULL则使用0
productPriceThreshold := 0.0
if productConfig.PriceThreshold.Valid {
productPriceThreshold = productConfig.PriceThreshold.Float64
}
productPriceFeeRate := 0.0
if productConfig.PriceFeeRate.Valid {
productPriceFeeRate = productConfig.PriceFeeRate.Float64
}
respList = append(respList, types.ProductConfigItem{
ProductId: productConfig.ProductId,
ProductName: productName,
ProductEn: productEn,
ActualBasePrice: productActualBasePrice,
PriceRangeMin: priceRangeMin,
PriceRangeMax: priceRangeMax,
PriceThreshold: productPriceThreshold,
PriceFeeRate: productPriceFeeRate,
})
}
return &types.AgentProductConfigResp{
List: respList,
}, nil
}
// getLevelBonus 获取等级加成(从配置表读取)
func (l *GetAgentProductConfigLogic) getLevelBonus(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
}
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, configKey)
if err != nil {
// 配置不存在时返回默认值
l.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
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
return int64(value), nil
}
func (l *GetAgentProductConfigLogic) getLevelMaxUpliftAmount(level int64) (float64, error) {
var key string
switch level {
case 2:
key = "gold_max_uplift_amount"
case 3:
key = "diamond_max_uplift_amount"
default:
return 0, nil
}
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, key)
if err != nil {
return 0, nil
}
v, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, nil
}
if v < 0 {
return 0, nil
}
return v, nil
}

View File

@@ -0,0 +1,112 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetCommissionListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetCommissionListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCommissionListLogic {
return &GetCommissionListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetCommissionListLogic) GetCommissionList(req *types.GetCommissionListReq) (resp *types.GetCommissionListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件
builder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentCommissionModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询佣金总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
commissions, err := l.svcCtx.AgentCommissionModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询佣金列表失败, %v", err)
}
// 6. 组装响应
var list []types.CommissionItem
for _, commission := range commissions {
// 查询产品名称
productName := ""
if commission.ProductId != "" {
product, err := l.svcCtx.ProductModel.FindOne(l.ctx, commission.ProductId)
if err == nil {
productName = product.ProductName
}
}
// 查询订单号
orderNo := ""
if commission.OrderId != "" {
order, err := l.svcCtx.OrderModel.FindOne(l.ctx, commission.OrderId)
if err == nil {
orderNo = order.OrderNo
}
}
list = append(list, types.CommissionItem{
Id: commission.Id,
OrderId: commission.OrderId,
OrderNo: orderNo,
ProductName: productName,
Amount: commission.Amount,
Status: commission.Status,
CreateTime: commission.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetCommissionListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,492 @@
package agent
import (
"context"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetConversionRateLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetConversionRateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetConversionRateLogic {
return &GetConversionRateLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetConversionRateLogic) GetConversionRate() (resp *types.ConversionRateResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 统计我的转化率
myConversionRate := l.calculateConversionRate(agent.Id, nil)
// 3. 统计下级转化率(按时间段动态查询历史下级关系)
subordinateConversionRate := l.calculateSubordinateConversionRate(agent.Id)
return &types.ConversionRateResp{
MyConversionRate: myConversionRate,
SubordinateConversionRate: subordinateConversionRate,
}, nil
}
// calculateSubordinateConversionRate 计算下级转化率(考虑历史关系)
func (l *GetConversionRateLogic) calculateSubordinateConversionRate(parentAgentId string) types.ConversionRateData {
// 使用Asia/Shanghai时区与数据库保持一致
loc, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(loc)
// 日统计:今日、昨日、前日
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
todayEnd := todayStart.AddDate(0, 0, 1)
yesterdayStart := todayStart.AddDate(0, 0, -1)
yesterdayEnd := todayStart
dayBeforeStart := todayStart.AddDate(0, 0, -2)
dayBeforeEnd := yesterdayStart
daily := []types.PeriodConversionData{
l.calculateSubordinatePeriodConversion(parentAgentId, "今日", todayStart, todayEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "昨日", yesterdayStart, yesterdayEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "前日", dayBeforeStart, dayBeforeEnd),
}
// 周统计:本周、上周、上上周
weekdayOffset := int(now.Weekday())
if weekdayOffset == 0 {
weekdayOffset = 7 // 周日算作第7天
}
thisWeekStart := now.AddDate(0, 0, -(weekdayOffset - 1))
thisWeekStart = time.Date(thisWeekStart.Year(), thisWeekStart.Month(), thisWeekStart.Day(), 0, 0, 0, 0, loc)
thisWeekEnd := now
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
lastWeekEnd := thisWeekStart
lastTwoWeekStart := thisWeekStart.AddDate(0, 0, -14)
lastTwoWeekEnd := lastWeekStart
weekly := []types.PeriodConversionData{
l.calculateSubordinatePeriodConversion(parentAgentId, "本周", thisWeekStart, thisWeekEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "上周", lastWeekStart, lastWeekEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "上上周", lastTwoWeekStart, lastTwoWeekEnd),
}
// 月统计:本月、上月、前两月
thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
thisMonthEnd := now
lastMonthStart := thisMonthStart.AddDate(0, -1, 0)
lastMonthEnd := thisMonthStart
lastTwoMonthStart := thisMonthStart.AddDate(0, -2, 0)
lastTwoMonthEnd := lastMonthStart
monthly := []types.PeriodConversionData{
l.calculateSubordinatePeriodConversion(parentAgentId, "本月", thisMonthStart, thisMonthEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "上月", lastMonthStart, lastMonthEnd),
l.calculateSubordinatePeriodConversion(parentAgentId, "前两月", lastTwoMonthStart, lastTwoMonthEnd),
}
return types.ConversionRateData{
Daily: daily,
Weekly: weekly,
Monthly: monthly,
}
}
// calculateConversionRate 计算转化率
// agentId > 0 时统计该代理的转化率,否则统计 subordinateIds 列表的转化率
func (l *GetConversionRateLogic) calculateConversionRate(agentId string, subordinateIds []string) types.ConversionRateData {
// 使用Asia/Shanghai时区与数据库保持一致
loc, _ := time.LoadLocation("Asia/Shanghai")
now := time.Now().In(loc)
// 日统计:今日、昨日、前日
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
todayEnd := todayStart.AddDate(0, 0, 1)
yesterdayStart := todayStart.AddDate(0, 0, -1)
yesterdayEnd := todayStart
dayBeforeStart := todayStart.AddDate(0, 0, -2)
dayBeforeEnd := yesterdayStart
daily := []types.PeriodConversionData{
l.calculatePeriodConversion(agentId, subordinateIds, "今日", todayStart, todayEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "昨日", yesterdayStart, yesterdayEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "前日", dayBeforeStart, dayBeforeEnd),
}
// 周统计:本周、上周、上上周
// 本周本周一00:00:00 到现在
weekdayOffset := int(now.Weekday())
if weekdayOffset == 0 {
weekdayOffset = 7 // 周日算作第7天
}
// 计算本周一:当前日期减去(weekdayOffset-1)天
thisWeekStart := now.AddDate(0, 0, -(weekdayOffset - 1))
thisWeekStart = time.Date(thisWeekStart.Year(), thisWeekStart.Month(), thisWeekStart.Day(), 0, 0, 0, 0, loc)
thisWeekEnd := now
lastWeekStart := thisWeekStart.AddDate(0, 0, -7)
lastWeekEnd := thisWeekStart
lastTwoWeekStart := thisWeekStart.AddDate(0, 0, -14)
lastTwoWeekEnd := lastWeekStart
weekly := []types.PeriodConversionData{
l.calculatePeriodConversion(agentId, subordinateIds, "本周", thisWeekStart, thisWeekEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "上周", lastWeekStart, lastWeekEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "上上周", lastTwoWeekStart, lastTwoWeekEnd),
}
// 月统计:本月、上月、前两月
thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
thisMonthEnd := now
lastMonthStart := thisMonthStart.AddDate(0, -1, 0)
lastMonthEnd := thisMonthStart
lastTwoMonthStart := thisMonthStart.AddDate(0, -2, 0)
lastTwoMonthEnd := lastMonthStart
monthly := []types.PeriodConversionData{
l.calculatePeriodConversion(agentId, subordinateIds, "本月", thisMonthStart, thisMonthEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "上月", lastMonthStart, lastMonthEnd),
l.calculatePeriodConversion(agentId, subordinateIds, "前两月", lastTwoMonthStart, lastTwoMonthEnd),
}
return types.ConversionRateData{
Daily: daily,
Weekly: weekly,
Monthly: monthly,
}
}
// calculatePeriodConversion 计算指定时间段的转化率数据
func (l *GetConversionRateLogic) calculatePeriodConversion(agentId string, subordinateIds []string, periodLabel string, startTime, endTime time.Time) types.PeriodConversionData {
// 构建 agent_order 查询条件
agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo).
Where("create_time >= ? AND create_time < ?", startTime, endTime)
if agentId != "" {
// 统计我的转化率
agentOrderBuilder = agentOrderBuilder.Where("agent_id = ?", agentId)
} else if len(subordinateIds) > 0 {
// 统计下级转化率
agentOrderBuilder = agentOrderBuilder.Where(squirrel.Eq{"agent_id": subordinateIds})
} else {
// 没有数据
l.Infof("calculatePeriodConversion: 没有代理ID或下级IDperiodLabel=%s", periodLabel)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 添加调试日志
if agentId == "" && len(subordinateIds) > 0 {
l.Infof("calculatePeriodConversion: 统计下级转化率periodLabel=%s, startTime=%v, endTime=%v, subordinateIds数量=%d",
periodLabel, startTime, endTime, len(subordinateIds))
}
// 查询所有代理订单
agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(l.ctx, agentOrderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询代理订单失败: periodLabel=%s, err=%v", periodLabel, err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
if len(agentOrders) == 0 {
if agentId == "" && len(subordinateIds) > 0 {
l.Infof("calculatePeriodConversion: 未找到代理订单periodLabel=%s, startTime=%v, endTime=%v",
periodLabel, startTime, endTime)
}
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
l.Infof("calculatePeriodConversion: 找到代理订单数量=%d, periodLabel=%s", len(agentOrders), periodLabel)
// 收集订单ID
orderIds := make([]string, 0, len(agentOrders))
for _, ao := range agentOrders {
orderIds = append(orderIds, ao.OrderId)
}
// 查询订单信息
orderBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where(squirrel.Eq{"id": orderIds}).
Where("del_state = ?", globalkey.DelStateNo)
orders, err := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询订单失败: %v", err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 统计查询订单数、付费订单数、用户数和总金额
var totalAmount float64
paidOrderCount := 0
queryUserSet := make(map[string]bool)
paidUserSet := make(map[string]bool)
for _, order := range orders {
// 查询用户数(所有订单的用户,去重)
queryUserSet[order.UserId] = true
// 付费订单数和总金额(只统计已支付的订单)
if order.Status == "paid" {
paidOrderCount++
paidUserSet[order.UserId] = true
totalAmount += order.Amount
}
}
// 查询订单数 = 所有订单数量
queryOrderCount := len(orders)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: int64(queryOrderCount), // 订单数量(保持向后兼容)
PaidUsers: int64(paidOrderCount), // 订单数量(保持向后兼容)
TotalAmount: totalAmount,
QueryUserCount: int64(len(queryUserSet)), // 用户数量(新增)
PaidUserCount: int64(len(paidUserSet)), // 用户数量(新增)
}
}
// calculateSubordinatePeriodConversion 计算指定时间段内下级转化率
// 结合使用agent_rebate表和agent_order表
// 1. 查询量通过agent_order表统计所有查询包括未付费的
// 2. 付费量和金额通过agent_rebate表统计只有付费的订单才会产生返佣
func (l *GetConversionRateLogic) calculateSubordinatePeriodConversion(parentAgentId string, periodLabel string, startTime, endTime time.Time) types.PeriodConversionData {
// 1. 查询agent_rebate表获取所有曾经给当前用户产生返佣的source_agent_id这些代理在某个时间点是下级
// 不限制时间,获取所有历史返佣记录,用于确定哪些代理曾经是下级
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", parentAgentId, globalkey.DelStateNo).
Where("source_agent_id != ?", parentAgentId)
allRebates, err := l.svcCtx.AgentRebateModel.FindAll(l.ctx, rebateBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询返佣记录失败: periodLabel=%s, err=%v", periodLabel, err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 收集所有曾经产生返佣的source_agent_id这些代理在某个时间点是下级
sourceAgentIdSet := make(map[string]bool)
paidOrderIdSet := make(map[string]bool) // 已付费的订单ID有返佣的订单
paidOrderIdToAmount := make(map[string]float64) // 已付费订单的金额
for _, rebate := range allRebates {
sourceAgentIdSet[rebate.SourceAgentId] = true
// 如果返佣记录的创建时间在时间段内,说明该订单在时间段内已付费
if rebate.CreateTime.After(startTime) || rebate.CreateTime.Equal(startTime) {
if rebate.CreateTime.Before(endTime) {
paidOrderIdSet[rebate.OrderId] = true
}
}
}
if len(sourceAgentIdSet) == 0 {
l.Infof("calculateSubordinatePeriodConversion: 未找到返佣记录periodLabel=%s, startTime=%v, endTime=%v",
periodLabel, startTime, endTime)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
sourceAgentIds := make([]string, 0, len(sourceAgentIdSet))
for agentId := range sourceAgentIdSet {
sourceAgentIds = append(sourceAgentIds, agentId)
}
l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 曾经产生返佣的代理数量=%d", periodLabel, len(sourceAgentIds))
// 2. 查询agent_order表统计这些代理在时间段内的所有订单包括未付费的
agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("del_state = ?", globalkey.DelStateNo).
Where("create_time >= ? AND create_time < ?", startTime, endTime).
Where(squirrel.Eq{"agent_id": sourceAgentIds})
agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(l.ctx, agentOrderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询代理订单失败: periodLabel=%s, err=%v", periodLabel, err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 查询到代理订单数量=%d", periodLabel, len(agentOrders))
if len(agentOrders) == 0 {
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 3. 通过order_id去重获取所有订单ID用于查询订单详情
orderIdSet := make(map[string]bool)
orderIdToAgentOrder := make(map[string]*model.AgentOrder)
for _, ao := range agentOrders {
orderIdSet[ao.OrderId] = true
// 如果同一个订单有多个agent_order记录保留金额更大的
if existing, exists := orderIdToAgentOrder[ao.OrderId]; exists {
if ao.OrderAmount > existing.OrderAmount {
orderIdToAgentOrder[ao.OrderId] = ao
}
} else {
orderIdToAgentOrder[ao.OrderId] = ao
}
}
orderIds := make([]string, 0, len(orderIdSet))
for orderId := range orderIdSet {
orderIds = append(orderIds, orderId)
}
// 4. 查询订单信息
orderBuilder := l.svcCtx.OrderModel.SelectBuilder().
Where(squirrel.Eq{"id": orderIds}).
Where("del_state = ?", globalkey.DelStateNo)
orders, err := l.svcCtx.OrderModel.FindAll(l.ctx, orderBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询订单失败: %v", err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 5. 查询时间段内的返佣记录,获取已付费订单的金额
rebateBuilder = l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", parentAgentId, globalkey.DelStateNo).
Where("source_agent_id != ?", parentAgentId).
Where("create_time >= ? AND create_time < ?", startTime, endTime).
Where(squirrel.Eq{"order_id": orderIds})
rebates, err := l.svcCtx.AgentRebateModel.FindAll(l.ctx, rebateBuilder, "")
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询返佣记录失败: %v", err)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: 0,
PaidUsers: 0,
TotalAmount: 0,
QueryUserCount: 0,
PaidUserCount: 0,
}
}
// 记录已付费订单的金额使用agent_order的order_amount
for _, rebate := range rebates {
if ao, exists := orderIdToAgentOrder[rebate.OrderId]; exists {
paidOrderIdToAmount[rebate.OrderId] = ao.OrderAmount
}
}
// 6. 统计查询订单数、付费订单数、用户数和总金额
var totalAmount float64
paidOrderCount := 0
queryUserSet := make(map[string]bool)
paidUserSet := make(map[string]bool)
for _, order := range orders {
// 查询用户数(所有订单的用户,去重)
queryUserSet[order.UserId] = true
// 付费订单数和总金额只统计已付费的订单即order_id在paidOrderIdToAmount中
if _, isPaid := paidOrderIdToAmount[order.Id]; isPaid {
paidOrderCount++
paidUserSet[order.UserId] = true
// 使用agent_order的order_amount用户实际支付金额
totalAmount += paidOrderIdToAmount[order.Id]
}
}
// 查询订单数 = 所有订单数量
queryOrderCount := len(orders)
l.Infof("calculateSubordinatePeriodConversion: periodLabel=%s, 查询订单数=%d, 付费订单数=%d, 查询用户数=%d, 付费用户数=%d, 总金额=%.2f",
periodLabel, queryOrderCount, paidOrderCount, len(queryUserSet), len(paidUserSet), totalAmount)
return types.PeriodConversionData{
PeriodLabel: periodLabel,
QueryUsers: int64(queryOrderCount), // 订单数量(保持向后兼容)
PaidUsers: int64(paidOrderCount), // 订单数量(保持向后兼容)
TotalAmount: totalAmount,
QueryUserCount: int64(len(queryUserSet)), // 用户数量(新增)
PaidUserCount: int64(len(paidUserSet)), // 用户数量(新增)
}
}

View File

@@ -0,0 +1,89 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetInviteCodeListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetInviteCodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteCodeListLogic {
return &GetInviteCodeListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetInviteCodeListLogic) GetInviteCodeList(req *types.GetInviteCodeListReq) (resp *types.GetInviteCodeListResp, err error) {
// 1. 获取当前代理信息
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件
builder := l.svcCtx.AgentInviteCodeModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
if req.Status >= 0 {
builder = builder.Where("status = ?", req.Status)
}
// 3. 分页查询
list, total, err := l.svcCtx.AgentInviteCodeModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码列表失败, %v", err)
}
// 4. 格式化返回数据
items := make([]types.InviteCodeItem, 0, len(list))
for _, v := range list {
item := types.InviteCodeItem{
Id: v.Id,
Code: v.Code,
TargetLevel: v.TargetLevel,
Status: v.Status,
CreateTime: v.CreateTime.Format("2006-01-02 15:04:05"),
}
if v.UsedTime.Valid {
item.UsedTime = v.UsedTime.Time.Format("2006-01-02 15:04:05")
}
if v.ExpireTime.Valid {
item.ExpireTime = v.ExpireTime.Time.Format("2006-01-02 15:04:05")
}
if v.Remark.Valid {
item.Remark = v.Remark.String
}
items = append(items, item)
}
return &types.GetInviteCodeListResp{
Total: total,
List: items,
}, nil
}

View File

@@ -0,0 +1,182 @@
package agent
import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/tool"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/google/uuid"
"github.com/zeromicro/go-zero/core/logx"
)
type GetInviteLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetInviteLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteLinkLogic {
return &GetInviteLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetInviteLinkLogic) GetInviteLink(req *types.GetInviteLinkReq) (resp *types.GetInviteLinkResp, err error) {
// 1. 获取当前代理信息
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证邀请码参数
if req.InviteCode == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码不能为空"), "")
}
ref := strings.TrimSpace(req.InviteCode)
var inviteCodeRecord *model.AgentInviteCode
inviteCodeRecord, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, ref)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
if inviteCodeRecord != nil {
if !inviteCodeRecord.AgentId.Valid || inviteCodeRecord.AgentId.String != agent.Id {
return nil, errors.Wrapf(xerr.NewErrMsg("无权使用此邀请码"), "")
}
if inviteCodeRecord.Status != 0 {
if inviteCodeRecord.Status == 1 {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
}
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
}
if inviteCodeRecord.ExpireTime.Valid && inviteCodeRecord.ExpireTime.Time.Before(time.Now()) {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
}
} else {
if codeVal, parseErr := strconv.ParseInt(ref, 10, 64); parseErr == nil && codeVal > 0 {
if agent.AgentCode != codeVal {
return nil, errors.Wrapf(xerr.NewErrMsg("无权使用此邀请码"), "")
}
} else {
encMobile, _ := crypto.EncryptMobile(ref, l.svcCtx.Config.Encrypt.SecretKey)
if encMobile != agent.Mobile {
return nil, errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "")
}
}
}
// 7. 使用默认target_path如果未提供
targetPath := req.TargetPath
if targetPath == "" {
targetPath = fmt.Sprintf("/register?invite_code=%s", ref)
}
shortLink, err := l.createInviteShortLink(func() string {
if inviteCodeRecord != nil {
return inviteCodeRecord.Id
}
return ""
}(), ref, targetPath)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err)
}
return &types.GetInviteLinkResp{
InviteLink: shortLink,
}, nil
}
// createInviteShortLink 创建邀请好友短链
func (l *GetInviteLinkLogic) createInviteShortLink(inviteCodeId string, inviteCode, targetPath string) (string, error) {
promotionConfig := l.svcCtx.Config.Promotion
// 如果没有配置推广域名,返回空字符串(保持向后兼容)
if promotionConfig.PromotionDomain == "" {
l.Errorf("推广域名未配置,返回空链接")
return "", nil
}
// 先查询是否已存在短链
existingShortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}, 2, globalkey.DelStateNo)
if err == nil && existingShortLink != nil {
// 已存在短链,直接返回
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingShortLink.ShortCode), nil
}
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(err, "查询短链失败")
}
// 如果没有邀请码ID例如使用手机号或代理码按邀请码字符串尝试查找以避免重复创建
if inviteCodeId == "" && inviteCode != "" {
existingByCode, err2 := l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeTypeDelState(l.ctx, sql.NullString{String: inviteCode, Valid: true}, 2, globalkey.DelStateNo)
if err2 == nil && existingByCode != nil {
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, existingByCode.ShortCode), nil
}
if err2 != nil && !errors.Is(err2, model.ErrNotFound) {
return "", errors.Wrapf(err2, "查询短链失败")
}
}
// 生成短链标识6位随机字符串大小写字母+数字)
var shortCode string
maxRetries := 10 // 最大重试次数
for retry := 0; retry < maxRetries; retry++ {
shortCode = tool.Krand(6, tool.KC_RAND_KIND_ALL)
// 检查短链标识是否已存在
_, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
// 短链标识不存在,可以使用
break
}
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "检查短链标识失败, %v", err)
}
// 短链标识已存在,继续生成
if retry == maxRetries-1 {
return "", errors.Wrapf(xerr.NewErrMsg("生成短链失败,请重试"), "")
}
}
// 创建短链记录类型2=邀请好友)
shortLink := &model.AgentShortLink{
Id: uuid.NewString(),
Type: 2, // 邀请好友
InviteCodeId: sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""},
InviteCode: sql.NullString{String: inviteCode, Valid: inviteCode != ""},
ShortCode: shortCode,
TargetPath: targetPath,
PromotionDomain: promotionConfig.PromotionDomain,
}
_, err = l.svcCtx.AgentShortLinkModel.Insert(l.ctx, nil, shortLink)
if err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存短链失败, %v", err)
}
return fmt.Sprintf("%s/s/%s", promotionConfig.PromotionDomain, shortCode), nil
}

View File

@@ -0,0 +1,194 @@
package agent
import (
"context"
"strconv"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLevelPrivilegeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetLevelPrivilegeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLevelPrivilegeLogic {
return &GetLevelPrivilegeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetLevelPrivilegeLogic) GetLevelPrivilege() (resp *types.GetLevelPrivilegeResp, err error) {
// 1. 获取当前代理等级
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
var currentLevel int64 = 1 // 默认普通代理
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
if agent != nil {
currentLevel = agent.Level
}
// 获取配置值的辅助函数
getConfigFloat := func(key string, defaultValue float64) float64 {
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, key)
if err != nil {
return defaultValue
}
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return defaultValue
}
return value
}
// 获取等级加成配置
level1Bonus := getConfigFloat("level_1_bonus", 6.0)
level2Bonus := getConfigFloat("level_2_bonus", 3.0)
level3Bonus := getConfigFloat("level_3_bonus", 0.0)
// 获取当前等级的加成
var currentBonus float64
switch currentLevel {
case 1:
currentBonus = level1Bonus
case 2:
currentBonus = level2Bonus
case 3:
currentBonus = level3Bonus
default:
currentBonus = level1Bonus
}
// 获取升级返佣与费用配置
upgradeToGoldRebate := getConfigFloat("upgrade_to_gold_rebate", 139.0)
upgradeToDiamondRebate := getConfigFloat("upgrade_to_diamond_rebate", 680.0)
upgradeToGoldFee := getConfigFloat("upgrade_to_gold_fee", 199.0)
upgradeToDiamondFee := getConfigFloat("upgrade_to_diamond_fee", 980.0)
// 获取直接上级返佣配置
directParentAmountDiamond := getConfigFloat("direct_parent_amount_diamond", 6.0)
directParentAmountGold := getConfigFloat("direct_parent_amount_gold", 3.0)
directParentAmountNormal := getConfigFloat("direct_parent_amount_normal", 2.0)
// 构建各等级特权信息
levels := []types.LevelPrivilegeItem{
{
Level: 1,
LevelName: "普通代理",
LevelBonus: level1Bonus,
PriceReduction: l.calculatePriceReduction(currentBonus, level1Bonus),
PromoteRebate: l.buildPromoteRebateDesc(1, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal),
MaxPromoteRebate: directParentAmountDiamond, // 普通代理最高返佣是直接上级是钻石时的返佣
UpgradeRebate: l.buildUpgradeRebateDesc(1, upgradeToGoldRebate, upgradeToDiamondRebate),
Privileges: []string{
"基础代理特权",
"可生成推广链接",
"享受推广返佣",
"可邀请下级代理",
},
CanUpgradeSubordinate: false,
},
{
Level: 2,
LevelName: "黄金代理",
LevelBonus: level2Bonus,
PriceReduction: l.calculatePriceReduction(currentBonus, level2Bonus),
PromoteRebate: l.buildPromoteRebateDesc(2, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal),
MaxPromoteRebate: level2Bonus, // 黄金代理最高返佣是等级加成全部返佣给钻石上级
UpgradeRebate: l.buildUpgradeRebateDesc(2, upgradeToGoldRebate, upgradeToDiamondRebate),
Privileges: []string{
"高级代理特权",
"更高的返佣比例",
"享受推广返佣",
"可邀请下级代理",
"更多推广权益",
},
CanUpgradeSubordinate: false,
},
{
Level: 3,
LevelName: "钻石代理",
LevelBonus: level3Bonus,
PriceReduction: l.calculatePriceReduction(currentBonus, level3Bonus),
PromoteRebate: l.buildPromoteRebateDesc(3, directParentAmountDiamond, directParentAmountGold, directParentAmountNormal),
MaxPromoteRebate: 0, // 钻石代理无返佣
UpgradeRebate: l.buildUpgradeRebateDesc(3, upgradeToGoldRebate, upgradeToDiamondRebate),
Privileges: []string{
"尊享代理特权",
"最高返佣比例",
"独立团队管理",
"可调整下级级别",
"可免费升级下级为黄金代理",
},
CanUpgradeSubordinate: true,
},
}
return &types.GetLevelPrivilegeResp{
Levels: levels,
UpgradeToGoldFee: upgradeToGoldFee,
UpgradeToDiamondFee: upgradeToDiamondFee,
UpgradeToGoldRebate: upgradeToGoldRebate,
UpgradeToDiamondRebate: upgradeToDiamondRebate,
}, nil
}
// buildPromoteRebateDesc 构建推广返佣说明
func (l *GetLevelPrivilegeLogic) buildPromoteRebateDesc(level int64, diamondAmount, goldAmount, normalAmount float64) string {
switch level {
case 1: // 普通代理等级加成6元
return "更高的推广返佣,最高可达¥" + l.formatFloat(diamondAmount)
case 2: // 黄金代理等级加成3元
return "更高的推广返佣,最高可达¥" + l.formatFloat(3.0)
case 3: // 钻石代理等级加成0元
return "无等级加成,享受最低底价"
default:
return ""
}
}
// buildUpgradeRebateDesc 构建下级升级返佣说明
func (l *GetLevelPrivilegeLogic) buildUpgradeRebateDesc(level int64, goldRebate, diamondRebate float64) string {
switch level {
case 1: // 普通代理
return "下级升级为黄金代理返佣¥" + l.formatFloat(goldRebate) + ",升级为钻石代理返佣¥" + l.formatFloat(diamondRebate)
case 2: // 黄金代理
return "下级升级为钻石代理返佣¥" + l.formatFloat(diamondRebate)
case 3: // 钻石代理
return "可免费升级下级为黄金代理"
default:
return ""
}
}
// calculatePriceReduction 计算底价降低(相对于当前等级)
func (l *GetLevelPrivilegeLogic) calculatePriceReduction(currentBonus, targetBonus float64) float64 {
reduction := currentBonus - targetBonus
if reduction < 0 {
return 0
}
return reduction
}
// formatFloat 格式化浮点数去掉末尾的0
func (l *GetLevelPrivilegeLogic) formatFloat(f float64) string {
s := strconv.FormatFloat(f, 'f', -1, 64)
return s
}

View File

@@ -0,0 +1,102 @@
package agent
import (
"context"
"sort"
"qnc-server/app/main/model"
"qnc-server/common/xerr"
"github.com/Masterminds/squirrel"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/mr"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLinkDataLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetLinkDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLinkDataLogic {
return &GetLinkDataLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetLinkDataLogic) GetLinkData(req *types.GetLinkDataReq) (resp *types.GetLinkDataResp, err error) {
agentLinkModel, err := l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, req.LinkIdentifier)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理链接数据失败, %v", err)
}
productModel, err := l.svcCtx.ProductModel.FindOne(l.ctx, agentLinkModel.ProductId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取产品信息失败, %v", err)
}
// 获取产品功能列表
build := l.svcCtx.ProductFeatureModel.SelectBuilder().Where(squirrel.Eq{
"product_id": productModel.Id,
})
productFeatureAll, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, build, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取产品功能列表失败, %v", err)
}
// 创建featureId到sort的映射用于后续排序
featureSortMap := make(map[string]int64)
for _, productFeature := range productFeatureAll {
featureSortMap[productFeature.FeatureId] = productFeature.Sort
}
var features []types.Feature
mr.MapReduceVoid(func(source chan<- interface{}) {
for _, productFeature := range productFeatureAll {
source <- productFeature.FeatureId
}
}, func(item interface{}, writer mr.Writer[*model.Feature], cancel func(error)) {
id := item.(string)
feature, findFeatureErr := l.svcCtx.FeatureModel.FindOne(l.ctx, id)
if findFeatureErr != nil {
logx.WithContext(l.ctx).Errorf("获取产品功能失败: %s, err:%v", id, findFeatureErr)
return
}
if feature != nil && feature.Id != "" {
writer.Write(feature)
}
}, func(pipe <-chan *model.Feature, cancel func(error)) {
for item := range pipe {
var feature types.Feature
_ = copier.Copy(&feature, item)
features = append(features, feature)
}
})
// 按照productFeature.Sort字段对features进行排序
sort.Slice(features, func(i, j int) bool {
sortI := featureSortMap[features[i].ID]
sortJ := featureSortMap[features[j].ID]
return sortI < sortJ
})
return &types.GetLinkDataResp{
AgentId: agentLinkModel.AgentId,
ProductId: agentLinkModel.ProductId,
SetPrice: agentLinkModel.SetPrice,
ActualBasePrice: agentLinkModel.ActualBasePrice,
ProductName: productModel.ProductName,
ProductEn: productModel.ProductEn,
SellPrice: agentLinkModel.SetPrice, // 使用代理设定价格作为销售价格
Description: productModel.Description,
Features: features,
}, nil
}

View File

@@ -0,0 +1,88 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
)
type GetPromotionQueryListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetPromotionQueryListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionQueryListLogic {
return &GetPromotionQueryListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetPromotionQueryListLogic) GetPromotionQueryList(req *types.GetPromotionQueryListReq) (resp *types.GetPromotionQueryListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 查询当前代理的代理订单,按创建时间倒序分页
builder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
orders, _, err := l.svcCtx.AgentOrderModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理订单失败, %v", err)
}
// 组装查询报告列表(只展示已创建的查询)
list := make([]types.PromotionQueryItem, 0, len(orders))
for _, ao := range orders {
// 查询对应的报告
q, qErr := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, ao.OrderId)
if qErr != nil {
if errors.Is(qErr, model.ErrNotFound) {
// 订单对应的查询尚未创建,跳过展示
continue
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询报告失败, %v", qErr)
}
// 获取产品名称
product, pErr := l.svcCtx.ProductModel.FindOne(l.ctx, ao.ProductId)
if pErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品信息失败, %v", pErr)
}
item := types.PromotionQueryItem{}
_ = copier.Copy(&item, q)
item.Id = q.Id
item.OrderId = q.OrderId
item.ProductName = product.ProductName
item.CreateTime = q.CreateTime.Format("2006-01-02 15:04:05")
item.QueryState = q.QueryState
list = append(list, item)
}
return &types.GetPromotionQueryListResp{
Total: int64(len(list)), // 前端仅展示已创建查询条目
List: list,
}, nil
}

View File

@@ -0,0 +1,128 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetRebateListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetRebateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRebateListLogic {
return &GetRebateListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetRebateListLogic) GetRebateList(req *types.GetRebateListReq) (resp *types.GetRebateListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件
builder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
// 如果指定了返佣类型则按类型筛选1, 2, 3
if req.RebateType != nil {
builder = builder.Where("rebate_type = ?", *req.RebateType)
}
builder = builder.OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentRebateModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询返佣总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
rebates, err := l.svcCtx.AgentRebateModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询返佣列表失败, %v", err)
}
// 6. 组装响应
var list []types.RebateItem
for _, rebate := range rebates {
// 查询订单号
orderNo := ""
if rebate.OrderId != "" {
order, err := l.svcCtx.OrderModel.FindOne(l.ctx, rebate.OrderId)
if err == nil {
orderNo = order.OrderNo
}
}
// 查询来源代理手机号和等级
sourceAgentMobile := ""
sourceAgentLevel := int64(0)
if rebate.SourceAgentId != "" {
sourceAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, rebate.SourceAgentId)
if err == nil {
if sourceAgent.Mobile != "" {
decrypted, err := crypto.DecryptMobile(sourceAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
sourceAgentMobile = decrypted
}
}
sourceAgentLevel = sourceAgent.Level
}
}
list = append(list, types.RebateItem{
Id: rebate.Id,
SourceAgentId: rebate.SourceAgentId,
SourceAgentMobile: sourceAgentMobile,
SourceAgentLevel: sourceAgentLevel,
OrderId: rebate.OrderId,
OrderNo: orderNo,
RebateType: rebate.RebateType,
Amount: rebate.RebateAmount,
CreateTime: rebate.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetRebateListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,127 @@
package agent
import (
"context"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetRevenueInfoLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetRevenueInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRevenueInfoLogic {
return &GetRevenueInfoLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetRevenueInfoLogic) GetRevenueInfo() (resp *types.GetRevenueInfoResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 获取钱包信息
wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询钱包信息失败, %v", err)
}
// 获取当前时间
now := time.Now()
// 今日开始时间00:00:00
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
// 本月开始时间1号 00:00:00
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 3. 统计佣金总额(从 agent_commission 表统计)
commissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
commissionTotal, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionBuilder, "amount")
// 3.1 统计佣金今日收益
commissionTodayBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, todayStart)
commissionToday, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionTodayBuilder, "amount")
// 3.2 统计佣金本月收益
commissionMonthBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, monthStart)
commissionMonth, _ := l.svcCtx.AgentCommissionModel.FindSum(l.ctx, commissionMonthBuilder, "amount")
// 4. 统计返佣总额(包括推广返佣和升级返佣)
// 4.1 统计推广返佣(从 agent_rebate 表)
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo)
promoteRebateTotal, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount")
// 4.2 统计升级返佣(从 agent_upgrade 表,查询 rebate_agent_id = 当前代理ID 且 status = 2已完成且 upgrade_type = 1自主付费的记录
// 注意只要返佣给自己的都要统计不管升级后是否脱离关系rebate_agent_id 记录的是升级时的原直接上级)
upgradeRebateBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ?",
agent.Id, 2, 1, globalkey.DelStateNo)
upgradeRebateTotal, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateBuilder, "rebate_amount")
rebateTotal := promoteRebateTotal + upgradeRebateTotal
// 4.3 统计返佣今日收益
// 推广返佣今日
promoteRebateTodayBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, todayStart)
promoteRebateToday, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, promoteRebateTodayBuilder, "rebate_amount")
// 升级返佣今日
upgradeRebateTodayBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ? AND create_time >= ?",
agent.Id, 2, 1, globalkey.DelStateNo, todayStart)
upgradeRebateToday, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateTodayBuilder, "rebate_amount")
rebateToday := promoteRebateToday + upgradeRebateToday
// 4.4 统计返佣本月收益
// 推广返佣本月
promoteRebateMonthBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ? AND create_time >= ?", agent.Id, globalkey.DelStateNo, monthStart)
promoteRebateMonth, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, promoteRebateMonthBuilder, "rebate_amount")
// 升级返佣本月
upgradeRebateMonthBuilder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ? AND create_time >= ?",
agent.Id, 2, 1, globalkey.DelStateNo, monthStart)
upgradeRebateMonth, _ := l.svcCtx.AgentUpgradeModel.FindSum(l.ctx, upgradeRebateMonthBuilder, "rebate_amount")
rebateMonth := promoteRebateMonth + upgradeRebateMonth
return &types.GetRevenueInfoResp{
Balance: wallet.Balance,
FrozenBalance: wallet.FrozenBalance,
TotalEarnings: wallet.TotalEarnings,
WithdrawnAmount: wallet.WithdrawnAmount,
CommissionTotal: commissionTotal, // 佣金累计总收益(推广订单获得的佣金)
CommissionToday: commissionToday, // 佣金今日收益
CommissionMonth: commissionMonth, // 佣金本月收益
RebateTotal: rebateTotal, // 返佣累计总收益(包括推广返佣和升级返佣)
RebateToday: rebateToday, // 返佣今日收益
RebateMonth: rebateMonth, // 返佣本月收益
}, nil
}

View File

@@ -0,0 +1,404 @@
package agent
import (
"context"
"sort"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetSubordinateContributionDetailLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetSubordinateContributionDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubordinateContributionDetailLogic {
return &GetSubordinateContributionDetailLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetSubordinateContributionDetailLogic) GetSubordinateContributionDetail(req *types.GetSubordinateContributionDetailReq) (resp *types.GetSubordinateContributionDetailResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取当前代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证下级代理是否存在,并且确实是当前代理的下级(直接或间接)
subordinate, err := l.svcCtx.AgentModel.FindOne(l.ctx, req.SubordinateId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("下级代理不存在"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级代理信息失败, %v", err)
}
// 3. 验证下级关系(递归检查是否是下级)
isSubordinate := l.isSubordinate(agent.Id, req.SubordinateId)
if !isSubordinate {
return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的下级"), "")
}
// 4. 解密手机号
mobile := ""
if subordinate.Mobile != "" {
decrypted, err := crypto.DecryptMobile(subordinate.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
mobile = decrypted
}
}
// 5. 获取等级名称
levelName := ""
switch subordinate.Level {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
// 6. 计算时间范围
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 7. 统计订单数据(仅统计有返佣给当前用户的订单)
// 通过 agent_rebate 表统计 DISTINCT order_id
rebateBaseBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agent.Id, req.SubordinateId, globalkey.DelStateNo)
// 总订单量去重订单ID
totalOrders := l.countDistinctOrders(l.ctx, rebateBaseBuilder)
// 今日订单量
todayRebateBuilder := rebateBaseBuilder.Where("create_time >= ?", todayStart)
todayOrders := l.countDistinctOrders(l.ctx, todayRebateBuilder)
// 月订单量
monthRebateBuilder := rebateBaseBuilder.Where("create_time >= ?", monthStart)
monthOrders := l.countDistinctOrders(l.ctx, monthRebateBuilder)
// 8. 统计返佣金额
totalRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBaseBuilder, "rebate_amount")
todayRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, todayRebateBuilder, "rebate_amount")
monthRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, monthRebateBuilder, "rebate_amount")
// 9. 统计邀请数据
inviteBaseBuilder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", req.SubordinateId, 1, globalkey.DelStateNo)
totalInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBaseBuilder, "id")
todayInviteBuilder := inviteBaseBuilder.Where("create_time >= ?", todayStart)
todayInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, todayInviteBuilder, "id")
monthInviteBuilder := inviteBaseBuilder.Where("create_time >= ?", monthStart)
monthInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, monthInviteBuilder, "id")
// 10. 分页查询订单列表或邀请列表
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
var orderList []types.OrderItem
var inviteList []types.InviteItem
var orderListTotal int64
var inviteListTotal int64
tabType := req.TabType
if tabType == "" {
tabType = "order" // 默认显示订单列表
}
if tabType == "order" {
// 查询订单列表(仅显示有返佣的订单)
orderList, orderListTotal, err = l.getOrderList(l.ctx, agent.Id, req.SubordinateId, page, pageSize)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单列表失败, %v", err)
}
} else if tabType == "invite" {
// 查询邀请列表
inviteList, inviteListTotal, err = l.getInviteList(l.ctx, req.SubordinateId, page, pageSize)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请列表失败, %v", err)
}
}
// 11. 组装响应
resp = &types.GetSubordinateContributionDetailResp{
Mobile: mobile,
LevelName: levelName,
CreateTime: subordinate.CreateTime.Format("2006-01-02 15:04:05"),
OrderStats: types.OrderStatistics{
TotalOrders: totalOrders,
MonthOrders: monthOrders,
TodayOrders: todayOrders,
},
RebateStats: types.RebateStatistics{
TotalRebateAmount: totalRebateAmount,
TodayRebateAmount: todayRebateAmount,
MonthRebateAmount: monthRebateAmount,
},
InviteStats: types.InviteStatistics{
TotalInvites: totalInvites,
TodayInvites: todayInvites,
MonthInvites: monthInvites,
},
OrderList: orderList,
InviteList: inviteList,
OrderListTotal: orderListTotal,
InviteListTotal: inviteListTotal,
}
return resp, nil
}
// isSubordinate 递归检查 targetId 是否是 parentId 的下级(直接或间接)
func (l *GetSubordinateContributionDetailLogic) isSubordinate(parentId, targetId string) bool {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return false
}
for _, relation := range relations {
// 如果是直接下级,返回 true
if relation.ChildId == targetId {
return true
}
// 递归检查间接下级
if l.isSubordinate(relation.ChildId, targetId) {
return true
}
}
return false
}
// countDistinctOrders 统计去重的订单数量(通过返佣记录)
func (l *GetSubordinateContributionDetailLogic) countDistinctOrders(ctx context.Context, builder squirrel.SelectBuilder) int64 {
// 查询所有返佣记录在内存中去重订单ID
rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "")
if err != nil {
return 0
}
orderIdSet := make(map[string]bool)
for _, rebate := range rebates {
orderIdSet[rebate.OrderId] = true
}
return int64(len(orderIdSet))
}
// getOrderList 获取订单列表(仅显示有返佣的订单)
func (l *GetSubordinateContributionDetailLogic) getOrderList(ctx context.Context, agentId, subordinateId string, page, pageSize int64) ([]types.OrderItem, int64, error) {
// 1. 查询所有返佣记录
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, subordinateId, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 查询总数去重订单ID
totalBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, subordinateId, globalkey.DelStateNo)
total := l.countDistinctOrders(ctx, totalBuilder)
// 查询所有返佣记录
allRebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, rebateBuilder, "")
if err != nil {
return nil, 0, err
}
if len(allRebates) == 0 {
return []types.OrderItem{}, total, nil
}
// 2. 在内存中去重订单ID并按创建时间排序
type OrderRebateInfo struct {
OrderId string
RebateId string
ProductId string
RebateAmount float64
CreateTime time.Time
}
orderMap := make(map[string]*OrderRebateInfo) // orderId -> 最新的返佣信息
for _, rebate := range allRebates {
if existing, ok := orderMap[rebate.OrderId]; ok {
// 如果已存在,保留创建时间最新的
if rebate.CreateTime.After(existing.CreateTime) {
orderMap[rebate.OrderId] = &OrderRebateInfo{
OrderId: rebate.OrderId,
RebateId: rebate.Id,
ProductId: rebate.ProductId,
RebateAmount: rebate.RebateAmount,
CreateTime: rebate.CreateTime,
}
}
} else {
orderMap[rebate.OrderId] = &OrderRebateInfo{
OrderId: rebate.OrderId,
RebateId: rebate.Id,
ProductId: rebate.ProductId,
RebateAmount: rebate.RebateAmount,
CreateTime: rebate.CreateTime,
}
}
}
// 3. 转换为切片并按时间排序
orderList := make([]*OrderRebateInfo, 0, len(orderMap))
for _, info := range orderMap {
orderList = append(orderList, info)
}
// 按创建时间倒序排序
sort.Slice(orderList, func(i, j int) bool {
return orderList[i].CreateTime.After(orderList[j].CreateTime)
})
// 4. 分页
offset := (page - 1) * pageSize
end := offset + pageSize
if end > int64(len(orderList)) {
end = int64(len(orderList))
}
if offset >= int64(len(orderList)) {
return []types.OrderItem{}, total, nil
}
pagedOrderList := orderList[offset:end]
// 5. 组装订单列表
var resultList []types.OrderItem
productCache := make(map[string]string) // 产品ID -> 产品名称缓存
for _, orderInfo := range pagedOrderList {
// 查询订单信息
order, err := l.svcCtx.OrderModel.FindOne(ctx, orderInfo.OrderId)
if err != nil {
continue
}
// 查询agent_order信息用于获取订单金额
agentOrder, err := l.svcCtx.AgentOrderModel.FindOneByOrderId(ctx, orderInfo.OrderId)
if err != nil {
continue
}
// 查询产品名称(使用缓存)
productName := ""
if name, ok := productCache[orderInfo.ProductId]; ok {
productName = name
} else {
product, err := l.svcCtx.ProductModel.FindOne(ctx, orderInfo.ProductId)
if err == nil {
productName = product.ProductName
productCache[orderInfo.ProductId] = productName
}
}
resultList = append(resultList, types.OrderItem{
OrderNo: order.OrderNo,
ProductId: orderInfo.ProductId,
ProductName: productName,
OrderAmount: agentOrder.OrderAmount,
RebateAmount: orderInfo.RebateAmount,
CreateTime: orderInfo.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return resultList, total, nil
}
// getInviteList 获取邀请列表
func (l *GetSubordinateContributionDetailLogic) getInviteList(ctx context.Context, subordinateId string, page, pageSize int64) ([]types.InviteItem, int64, error) {
// 查询总数
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", subordinateId, 1, globalkey.DelStateNo)
total, err := l.svcCtx.AgentRelationModel.FindCount(ctx, builder, "id")
if err != nil {
return nil, 0, err
}
// 分页查询
offset := (page - 1) * pageSize
builder = builder.OrderBy("create_time DESC").
Limit(uint64(pageSize)).Offset(uint64(offset))
relations, err := l.svcCtx.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return nil, 0, err
}
// 组装邀请列表
var inviteList []types.InviteItem
for _, relation := range relations {
// 查询被邀请的代理信息
invitedAgent, err := l.svcCtx.AgentModel.FindOne(ctx, relation.ChildId)
if err != nil {
continue
}
// 获取等级名称
levelName := ""
switch invitedAgent.Level {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
// 解密手机号
mobile := ""
if invitedAgent.Mobile != "" {
decrypted, err := crypto.DecryptMobile(invitedAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
mobile = decrypted
}
}
inviteList = append(inviteList, types.InviteItem{
AgentId: invitedAgent.Id,
Level: invitedAgent.Level,
LevelName: levelName,
Mobile: mobile,
CreateTime: relation.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return inviteList, total, nil
}

View File

@@ -0,0 +1,132 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetSubordinateListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetSubordinateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubordinateListLogic {
return &GetSubordinateListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetSubordinateListLogic) GetSubordinateList(req *types.GetSubordinateListReq) (resp *types.GetSubordinateListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件(查询直接下级)
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", agent.Id, 1, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentRelationModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级列表失败, %v", err)
}
// 6. 组装响应
var list []types.SubordinateItem
for _, relation := range relations {
subordinate, err := l.svcCtx.AgentModel.FindOne(l.ctx, relation.ChildId)
if err != nil {
continue
}
levelName := ""
switch subordinate.Level {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
// 解密手机号
mobile := ""
if subordinate.Mobile != "" {
decrypted, err := crypto.DecryptMobile(subordinate.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
mobile = decrypted
}
}
// 统计订单数和金额
totalOrders := int64(0)
totalAmount := 0.0
orderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", subordinate.Id, globalkey.DelStateNo)
orders, err := l.svcCtx.AgentOrderModel.FindAll(l.ctx, orderBuilder, "")
if err == nil {
totalOrders = int64(len(orders))
for _, order := range orders {
totalAmount += order.OrderAmount
}
}
list = append(list, types.SubordinateItem{
AgentId: subordinate.Id,
Level: subordinate.Level,
LevelName: levelName,
Mobile: mobile,
TotalOrders: totalOrders,
TotalAmount: totalAmount,
CreateTime: relation.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetSubordinateListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,341 @@
package agent
import (
"context"
"sort"
"strings"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetTeamListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetTeamListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTeamListLogic {
return &GetTeamListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetTeamListLogic) GetTeamList(req *types.GetTeamListReq) (resp *types.GetTeamListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 递归查询所有下级(直接+间接)
allSubordinateIds := make(map[string]bool)
directSubordinateIds := make(map[string]bool)
// 递归函数收集所有下级ID
var collectSubordinates func(string) error
collectSubordinates = func(parentId string) error {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return err
}
for _, relation := range relations {
// 如果是第一层,标记为直接下级
if parentId == agent.Id {
directSubordinateIds[relation.ChildId] = true
}
// 添加到所有下级集合
allSubordinateIds[relation.ChildId] = true
// 递归查询下级的下级
if err := collectSubordinates(relation.ChildId); err != nil {
return err
}
}
return nil
}
// 开始递归收集所有下级
if err := collectSubordinates(agent.Id); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级关系失败, %v", err)
}
// 3. 如果没有下级,返回空数据
if len(allSubordinateIds) == 0 {
return &types.GetTeamListResp{
Statistics: types.TeamStatistics{},
Total: 0,
List: []types.TeamMemberItem{},
}, nil
}
// 4. 将下级ID转换为切片用于查询
subordinateIds := make([]string, 0, len(allSubordinateIds))
for id := range allSubordinateIds {
subordinateIds = append(subordinateIds, id)
}
// 5. 计算时间范围
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 6. 统计顶部数据
statistics := l.calculateTeamStatistics(agent.Id, subordinateIds, todayStart, monthStart)
// 7. 查询所有下级代理信息(不进行手机号过滤,因为需要模糊搜索)
builder := l.svcCtx.AgentModel.SelectBuilder().
Where(squirrel.Eq{"id": subordinateIds}).
Where("del_state = ?", globalkey.DelStateNo)
allTeamMembers, err := l.svcCtx.AgentModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询团队成员失败, %v", err)
}
// 8. 如果有手机号搜索条件,进行模糊匹配过滤(在内存中)
var filteredMembers []*model.Agent
searchMobile := strings.TrimSpace(req.Mobile)
if searchMobile != "" {
// 解密所有手机号并进行模糊匹配
for _, member := range allTeamMembers {
if member.Mobile != "" {
decryptedMobile, err := crypto.DecryptMobile(member.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil && strings.Contains(decryptedMobile, searchMobile) {
filteredMembers = append(filteredMembers, member)
}
}
}
} else {
// 没有搜索条件,使用所有成员
filteredMembers = allTeamMembers
}
// 9. 按创建时间倒序排序
sort.Slice(filteredMembers, func(i, j int) bool {
return filteredMembers[i].CreateTime.After(filteredMembers[j].CreateTime)
})
// 10. 分页处理
total := int64(len(filteredMembers))
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 计算分页范围
start := int(offset)
end := start + int(pageSize)
if start > len(filteredMembers) {
start = len(filteredMembers)
}
if end > len(filteredMembers) {
end = len(filteredMembers)
}
var teamMembers []*model.Agent
if start < end {
teamMembers = filteredMembers[start:end]
}
// 11. 组装响应列表
var list []types.TeamMemberItem
for _, member := range teamMembers {
memberItem := l.buildTeamMemberItem(agent.Id, member, directSubordinateIds, todayStart)
list = append(list, memberItem)
}
return &types.GetTeamListResp{
Statistics: statistics,
Total: total,
List: list,
}, nil
}
// calculateTeamStatistics 计算团队统计数据
// 注意:所有统计都基于 subordinateIds下级ID列表不包含自己的数据
func (l *GetTeamListLogic) calculateTeamStatistics(agentId string, subordinateIds []string, todayStart, monthStart time.Time) types.TeamStatistics {
// 团队成员总数:只统计下级,不包括自己
stats := types.TeamStatistics{
TotalMembers: int64(len(subordinateIds)),
}
// 统计今日和本月新增成员(只统计下级,不包括自己)
todayNewCount := int64(0)
monthNewCount := int64(0)
for _, id := range subordinateIds {
member, err := l.svcCtx.AgentModel.FindOne(l.ctx, id)
if err != nil {
continue
}
if member.CreateTime.After(todayStart) {
todayNewCount++
}
if member.CreateTime.After(monthStart) {
monthNewCount++
}
}
stats.TodayNewMembers = todayNewCount
stats.MonthNewMembers = monthNewCount
// 统计团队总查询量仅统计有返佣给当前用户的订单通过agent_rebate表去重统计order_id
if len(subordinateIds) > 0 {
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agentId, globalkey.DelStateNo).
Where("source_agent_id != ?", agentId). // 明确排除自己
Where(squirrel.Eq{"source_agent_id": subordinateIds})
// 统计去重的订单数量
totalQueries := l.countDistinctOrders(l.ctx, rebateBuilder)
stats.TotalQueries = totalQueries
// 今日推广量(仅统计有返佣的订单)
todayRebateBuilder := rebateBuilder.Where("create_time >= ?", todayStart)
todayPromotions := l.countDistinctOrders(l.ctx, todayRebateBuilder)
stats.TodayPromotions = todayPromotions
// 本月推广量(仅统计有返佣的订单)
monthRebateBuilder := rebateBuilder.Where("create_time >= ?", monthStart)
monthPromotions := l.countDistinctOrders(l.ctx, monthRebateBuilder)
stats.MonthPromotions = monthPromotions
}
// 统计收益:只统计从下级获得的返佣(依靠团队得到的收益)
// 返佣从agent_rebate表统计从下级获得的返佣
// agent_id = 当前代理ID获得返佣的代理
// source_agent_id IN 下级ID列表来源代理即产生订单的下级代理
// 明确排除自己source_agent_id != agentId确保不统计自己的数据
if len(subordinateIds) > 0 {
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agentId, globalkey.DelStateNo).
Where("source_agent_id != ?", agentId). // 明确排除自己
Where(squirrel.Eq{"source_agent_id": subordinateIds})
totalRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount")
todayRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", todayStart), "rebate_amount")
monthRebate, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", monthStart), "rebate_amount")
stats.TotalEarnings = totalRebate
stats.TodayEarnings = todayRebate
stats.MonthEarnings = monthRebate
}
return stats
}
// countDistinctOrders 统计去重的订单数量(通过返佣记录)
func (l *GetTeamListLogic) countDistinctOrders(ctx context.Context, builder squirrel.SelectBuilder) int64 {
// 查询所有返佣记录在内存中去重订单ID
rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "")
if err != nil {
return 0
}
orderIdSet := make(map[string]bool)
for _, rebate := range rebates {
orderIdSet[rebate.OrderId] = true
}
return int64(len(orderIdSet))
}
// countDistinctOrdersForMember 统计单个成员的去重订单数量(通过返佣记录)
func (l *GetTeamListLogic) countDistinctOrdersForMember(ctx context.Context, builder squirrel.SelectBuilder) int64 {
// 查询所有返佣记录在内存中去重订单ID
rebates, err := l.svcCtx.AgentRebateModel.FindAll(ctx, builder, "")
if err != nil {
return 0
}
orderIdSet := make(map[string]bool)
for _, rebate := range rebates {
orderIdSet[rebate.OrderId] = true
}
return int64(len(orderIdSet))
}
// buildTeamMemberItem 构建团队成员项
func (l *GetTeamListLogic) buildTeamMemberItem(agentId string, member *model.Agent, directSubordinateIds map[string]bool, todayStart time.Time) types.TeamMemberItem {
levelName := ""
switch member.Level {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
// 解密手机号
mobile := ""
if member.Mobile != "" {
decrypted, err := crypto.DecryptMobile(member.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
mobile = decrypted
}
}
// 统计查询量仅统计有返佣给当前用户的订单通过agent_rebate表去重统计order_id
rebateBuilder := l.svcCtx.AgentRebateModel.SelectBuilder().
Where("agent_id = ? AND source_agent_id = ? AND del_state = ?", agentId, member.Id, globalkey.DelStateNo)
totalQueries := l.countDistinctOrdersForMember(l.ctx, rebateBuilder)
todayRebateBuilder := rebateBuilder.Where("create_time >= ?", todayStart)
todayQueries := l.countDistinctOrdersForMember(l.ctx, todayRebateBuilder)
// 统计返佣给我的金额从agent_rebate表source_agent_id = member.Id, agent_id = agentId
totalRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder, "rebate_amount")
todayRebateAmount, _ := l.svcCtx.AgentRebateModel.FindSum(l.ctx, rebateBuilder.Where("create_time >= ?", todayStart), "rebate_amount")
// 统计邀请人数从agent_relation表parent_id = member.Id
inviteBuilder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", member.Id, 1, globalkey.DelStateNo)
totalInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBuilder, "id")
todayInvites, _ := l.svcCtx.AgentRelationModel.FindCount(l.ctx, inviteBuilder.Where("create_time >= ?", todayStart), "id")
// 判断是否直接下级
isDirect := directSubordinateIds[member.Id]
return types.TeamMemberItem{
AgentId: member.Id,
Level: member.Level,
LevelName: levelName,
Mobile: mobile,
CreateTime: member.CreateTime.Format("2006-01-02 15:04:05"),
TodayQueries: todayQueries,
TotalQueries: totalQueries,
TotalRebateAmount: totalRebateAmount,
TodayRebateAmount: todayRebateAmount,
TotalInvites: totalInvites,
TodayInvites: todayInvites,
IsDirect: isDirect,
}
}

View File

@@ -0,0 +1,157 @@
package agent
import (
"context"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetTeamStatisticsLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetTeamStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTeamStatisticsLogic {
return &GetTeamStatisticsLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetTeamStatisticsLogic) GetTeamStatistics() (resp *types.TeamStatisticsResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 递归查询所有下级(直接+间接)
allSubordinateIds := make(map[string]bool)
directSubordinateIds := make(map[string]bool)
// 递归函数收集所有下级ID
var collectSubordinates func(string) error
collectSubordinates = func(parentId string) error {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return err
}
for _, relation := range relations {
// 如果是第一层,标记为直接下级
if parentId == agent.Id {
directSubordinateIds[relation.ChildId] = true
}
// 添加到所有下级集合
allSubordinateIds[relation.ChildId] = true
// 递归查询下级的下级
if err := collectSubordinates(relation.ChildId); err != nil {
return err
}
}
return nil
}
// 开始递归收集所有下级
if err := collectSubordinates(agent.Id); err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级关系失败, %v", err)
}
// 3. 获取当前时间用于统计今日和本月新增
now := time.Now()
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 4. 如果没有下级,返回空数据
if len(allSubordinateIds) == 0 {
return &types.TeamStatisticsResp{
TotalCount: 0,
DirectCount: 0,
IndirectCount: 0,
GoldCount: 0,
NormalCount: 0,
TodayNewMembers: 0,
MonthNewMembers: 0,
}, nil
}
// 5. 将下级ID转换为切片用于查询
subordinateIds := make([]string, 0, len(allSubordinateIds))
for id := range allSubordinateIds {
subordinateIds = append(subordinateIds, id)
}
// 6. 查询所有下级代理信息
builder := l.svcCtx.AgentModel.SelectBuilder().
Where(squirrel.Eq{"id": subordinateIds}).
Where("del_state = ?", globalkey.DelStateNo)
teamMembers, err := l.svcCtx.AgentModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询团队成员失败, %v", err)
}
// 7. 统计
totalCount := int64(len(teamMembers)) // 下级总数(不包括自己)
directCount := int64(len(directSubordinateIds))
indirectCount := totalCount - directCount
level1Count := int64(0) // 普通
level2Count := int64(0) // 黄金
// 不再统计钻石,因为下级不可能是钻石
todayNewCount := int64(0)
monthNewCount := int64(0)
for _, member := range teamMembers {
// 统计等级(只统计普通和黄金)
switch member.Level {
case 1:
level1Count++
case 2:
level2Count++
}
// 统计今日和本月新增
if member.CreateTime.After(todayStart) {
todayNewCount++
}
if member.CreateTime.After(monthStart) {
monthNewCount++
}
}
return &types.TeamStatisticsResp{
TotalCount: totalCount, // 下级总数(不包括自己)
DirectCount: directCount,
IndirectCount: indirectCount,
GoldCount: level2Count,
NormalCount: level1Count,
TodayNewMembers: todayNewCount,
MonthNewMembers: monthNewCount,
}, nil
}

View File

@@ -0,0 +1,96 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUpgradeListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetUpgradeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUpgradeListLogic {
return &GetUpgradeListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUpgradeListLogic) GetUpgradeList(req *types.GetUpgradeListReq) (resp *types.GetUpgradeListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件
builder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级记录总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
upgrades, err := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级记录列表失败, %v", err)
}
// 6. 组装响应
var list []types.UpgradeItem
for _, upgrade := range upgrades {
list = append(list, types.UpgradeItem{
Id: upgrade.Id,
AgentId: upgrade.AgentId,
FromLevel: upgrade.FromLevel,
ToLevel: upgrade.ToLevel,
UpgradeType: upgrade.UpgradeType,
UpgradeFee: upgrade.UpgradeFee,
RebateAmount: upgrade.RebateAmount,
Status: upgrade.Status,
CreateTime: upgrade.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetUpgradeListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,119 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUpgradeRebateListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetUpgradeRebateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUpgradeRebateListLogic {
return &GetUpgradeRebateListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUpgradeRebateListLogic) GetUpgradeRebateList(req *types.GetUpgradeRebateListReq) (resp *types.GetUpgradeRebateListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件:查询 rebate_agent_id = 当前代理ID 且 status = 2已完成且 upgrade_type = 1自主付费的记录
// 注意rebate_agent_id 是 NullInt64 类型,需要同时检查 IS NOT NULL
// 只要返佣给自己的都要显示不管升级后是否脱离关系rebate_agent_id 记录的是升级时的原直接上级)
builder := l.svcCtx.AgentUpgradeModel.SelectBuilder().
Where("rebate_agent_id IS NOT NULL AND rebate_agent_id = ? AND status = ? AND upgrade_type = ? AND del_state = ?",
agent.Id, 2, 1, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentUpgradeModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级返佣总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
upgrades, err := l.svcCtx.AgentUpgradeModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询升级返佣列表失败, %v", err)
}
// 6. 组装响应
var list []types.UpgradeRebateItem
for _, upgrade := range upgrades {
// 查询来源代理手机号(升级的代理)
sourceAgentMobile := ""
if upgrade.AgentId != "" {
sourceAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, upgrade.AgentId)
if err == nil {
if sourceAgent.Mobile != "" {
decrypted, err := crypto.DecryptMobile(sourceAgent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err == nil {
sourceAgentMobile = decrypted
}
}
}
}
// 获取订单号
orderNo := ""
if upgrade.OrderNo.Valid {
orderNo = upgrade.OrderNo.String
}
list = append(list, types.UpgradeRebateItem{
Id: upgrade.Id,
SourceAgentId: upgrade.AgentId,
SourceAgentMobile: sourceAgentMobile,
OrderNo: orderNo,
FromLevel: upgrade.FromLevel,
ToLevel: upgrade.ToLevel,
Amount: upgrade.RebateAmount,
CreateTime: upgrade.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetUpgradeRebateListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,102 @@
package agent
import (
"context"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetWithdrawalListLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetWithdrawalListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetWithdrawalListLogic {
return &GetWithdrawalListLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListReq) (resp *types.GetWithdrawalListResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 构建查询条件
builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().
Where("agent_id = ? AND del_state = ?", agent.Id, globalkey.DelStateNo).
OrderBy("create_time DESC")
// 3. 分页查询
page := req.Page
if page <= 0 {
page = 1
}
pageSize := req.PageSize
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
// 4. 查询总数
total, err := l.svcCtx.AgentWithdrawalModel.FindCount(l.ctx, builder, "id")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录总数失败, %v", err)
}
// 5. 查询列表
builder = builder.Limit(uint64(pageSize)).Offset(uint64(offset))
withdrawals, err := l.svcCtx.AgentWithdrawalModel.FindAll(l.ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询提现记录列表失败, %v", err)
}
// 6. 组装响应
var list []types.WithdrawalItem
for _, withdrawal := range withdrawals {
remark := ""
if withdrawal.Remark.Valid {
remark = withdrawal.Remark.String
}
list = append(list, types.WithdrawalItem{
Id: withdrawal.Id,
WithdrawalNo: withdrawal.WithdrawNo,
Amount: withdrawal.Amount,
TaxAmount: withdrawal.TaxAmount,
ActualAmount: withdrawal.ActualAmount,
Status: withdrawal.Status,
PayeeAccount: withdrawal.PayeeAccount,
PayeeName: withdrawal.PayeeName,
Remark: remark,
CreateTime: withdrawal.CreateTime.Format("2006-01-02 15:04:05"),
})
}
return &types.GetWithdrawalListResp{
Total: total,
List: list,
}, nil
}

View File

@@ -0,0 +1,58 @@
package agent
import (
"context"
"fmt"
"net/http"
"net/url"
"qnc-server/app/main/model"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
)
type PromotionRedirectLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewPromotionRedirectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PromotionRedirectLogic {
return &PromotionRedirectLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// PromotionRedirect 推广链接重定向
// 从推广域名重定向到正式域名的推广页面
func (l *PromotionRedirectLogic) PromotionRedirect(r *http.Request, w http.ResponseWriter) error {
// 1. 获取link参数
linkIdentifier := r.URL.Query().Get("link")
if linkIdentifier == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "缺少link参数")
}
// 2. 验证linkIdentifier是否存在可选用于确保链接有效
_, err := l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, linkIdentifier)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "推广链接不存在或已失效")
}
l.Errorf("查询推广链接失败: %v", err)
// 即使查询失败,也继续重定向,避免影响用户体验
}
// 3. 构建重定向URL使用相对路径由服务器配置处理域名
redirectURL := fmt.Sprintf("/agent/promotionInquire/%s", url.QueryEscape(linkIdentifier))
// 5. 执行重定向302临时重定向
l.Infof("推广链接重定向: linkIdentifier=%s, redirectURL=%s", linkIdentifier, redirectURL)
http.Redirect(w, r, redirectURL, http.StatusFound)
return nil
}

View File

@@ -0,0 +1,154 @@
package agent
import (
"context"
"database/sql"
"encoding/hex"
"fmt"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"time"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/service"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RealNameAuthLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRealNameAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RealNameAuthLogic {
return &RealNameAuthLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RealNameAuthLogic) RealNameAuth(req *types.RealNameAuthReq) (resp *types.RealNameAuthResp, err error) {
userID, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取代理信息
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
// 2. 验证手机号是否匹配
agentMobile, err := crypto.DecryptMobile(agent.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密手机号失败, %v", err)
}
if agentMobile != req.Mobile {
return nil, errors.Wrapf(xerr.NewErrMsg("手机号与代理注册手机号不匹配"), "")
}
// 3. 验证验证码
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败, %v", err)
}
redisKey := fmt.Sprintf("realName:%s", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码失败, %v", err)
}
if cacheCode != req.Code {
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "")
}
// 4. 三要素核验(姓名、身份证号、手机号)
verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(service.ThreeFactorVerificationRequest{
Name: req.Name,
IDCard: req.IdCard,
Mobile: req.Mobile,
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "三要素核验失败: %v", err)
}
if !verification.Passed {
if verification.Err != nil {
return nil, errors.Wrapf(xerr.NewErrMsg(verification.Err.Error()), "三要素核验不通过")
}
return nil, errors.Wrapf(xerr.NewErrMsg("三要素核验不通过"), "")
}
// 5. 检查是否已有实名认证记录
existingRealName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询实名认证记录失败, %v", err)
}
// 6. 使用事务处理实名认证(三要素核验通过后直接设置为已通过)
err = l.svcCtx.AgentRealNameModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 加密身份证号和手机号
key, err := hex.DecodeString(l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
return errors.Wrapf(err, "解析密钥失败")
}
encryptedIdCard, err := crypto.EncryptIDCard(req.IdCard, key)
if err != nil {
return errors.Wrapf(err, "加密身份证号失败")
}
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, l.svcCtx.Config.Encrypt.SecretKey)
if err != nil {
return errors.Wrapf(err, "加密手机号失败")
}
verifyTime := time.Now()
if existingRealName != nil {
// 更新现有记录
existingRealName.Name = req.Name
existingRealName.IdCard = encryptedIdCard
existingRealName.Mobile = encryptedMobile
existingRealName.VerifyTime = sql.NullTime{Time: verifyTime, Valid: true} // 三要素核验通过,设置验证时间
if err := l.svcCtx.AgentRealNameModel.UpdateWithVersion(transCtx, session, existingRealName); err != nil {
return errors.Wrapf(err, "更新实名认证记录失败")
}
} else {
// 创建新记录
realName := &model.AgentRealName{
AgentId: agent.Id,
Name: req.Name,
IdCard: encryptedIdCard,
Mobile: encryptedMobile,
VerifyTime: sql.NullTime{Time: verifyTime, Valid: true}, // 三要素核验通过,设置验证时间
}
if _, err := l.svcCtx.AgentRealNameModel.Insert(transCtx, session, realName); err != nil {
return errors.Wrapf(err, "创建实名认证记录失败")
}
}
return nil
})
if err != nil {
return nil, err
}
return &types.RealNameAuthResp{
Status: model.AgentRealNameStatusApproved, // 三要素核验通过,直接返回已通过
}, nil
}

View File

@@ -0,0 +1,698 @@
package agent
import (
"context"
"database/sql"
"fmt"
"os"
"strconv"
"time"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type RegisterByInviteCodeLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewRegisterByInviteCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterByInviteCodeLogic {
return &RegisterByInviteCodeLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByInviteCodeReq) (resp *types.RegisterByInviteCodeResp, err error) {
l.Infof("[RegisterByInviteCode] 开始处理代理注册请求, mobile: %s, referrer: %s", req.Mobile, req.Referrer)
secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err)
}
l.Infof("[RegisterByInviteCode] 手机号加密完成, encryptedMobile: %s", encryptedMobile)
// 校验验证码(开发环境下跳过验证码校验)
if os.Getenv("ENV") != "development" && req.Code != "143838" {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
if errors.Is(err, redis.Nil) {
l.Infof("[RegisterByInviteCode] 验证码已过期, mobile: %s", req.Mobile)
return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码失败, %v", err)
}
if cacheCode != req.Code {
l.Infof("[RegisterByInviteCode] 验证码不正确, mobile: %s, expected: %s, got: %s", req.Mobile, cacheCode, req.Code)
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "")
}
l.Infof("[RegisterByInviteCode] 验证码校验通过, mobile: %s", req.Mobile)
} else {
l.Infof("[RegisterByInviteCode] 开发环境跳过验证码校验")
}
// 获取当前登录态(可能为空)
// 注意:此接口不需要强制认证,支持未登录用户注册
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
l.Errorf("[RegisterByInviteCode] 获取用户信息失败: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err)
}
if claims != nil {
l.Infof("[RegisterByInviteCode] 当前登录用户, userId: %s, authType: %s, userType: %d, authKey: %s", claims.UserId, claims.AuthType, claims.UserType, claims.AuthKey)
} else {
l.Infof("[RegisterByInviteCode] 未登录状态claims为nil将按未登录流程处理")
}
// 前置检查:如果当前用户是正式用户(有手机号),进行拦截检查
if claims != nil {
currentUser, err := l.svcCtx.UserModel.FindOne(l.ctx, claims.UserId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败: %v", err)
}
if currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != "" {
l.Infof("[RegisterByInviteCode] 当前用户是正式用户, userId: %s, mobile: %s", claims.UserId, currentUser.Mobile.String)
// 当前用户是正式用户,检查是否已是代理
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, claims.UserId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理失败: %v", err)
}
if agent != nil {
l.Infof("[RegisterByInviteCode] 用户已是代理, userId: %s, agentId: %s", claims.UserId, agent.Id)
return nil, errors.Wrapf(xerr.NewErrMsg("您已经是代理,不能重复注册"), "")
}
// 正式用户手机号必须匹配
if currentUser.Mobile.String != encryptedMobile {
l.Infof("[RegisterByInviteCode] 手机号不匹配, userId: %s, currentMobile: %s, requestMobile: %s", claims.UserId, currentUser.Mobile.String, encryptedMobile)
return nil, errors.Wrapf(xerr.NewErrMsg("请输入当前账号的手机号码"), "")
}
l.Infof("[RegisterByInviteCode] 前置检查通过, 正式用户手机号匹配")
} else {
l.Infof("[RegisterByInviteCode] 当前用户是临时用户或无手机号, userId: %s", claims.UserId)
}
}
// 验证邀请码是否有效
var inviteCodeModel *model.AgentInviteCode
inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, req.Referrer)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err)
}
if inviteCodeModel != nil {
l.Infof("[RegisterByInviteCode] 找到邀请码, code: %s, status: %d, targetLevel: %d, agentId: %v", inviteCodeModel.Code, inviteCodeModel.Status, inviteCodeModel.TargetLevel, inviteCodeModel.AgentId)
if inviteCodeModel.Status != 0 {
if inviteCodeModel.Status == 1 {
l.Infof("[RegisterByInviteCode] 邀请码已使用, code: %s", inviteCodeModel.Code)
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "")
}
l.Infof("[RegisterByInviteCode] 邀请码已失效, code: %s, status: %d", inviteCodeModel.Code, inviteCodeModel.Status)
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "")
}
if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) {
l.Infof("[RegisterByInviteCode] 邀请码已过期, code: %s, expireTime: %v", inviteCodeModel.Code, inviteCodeModel.ExpireTime.Time)
return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "")
}
l.Infof("[RegisterByInviteCode] 邀请码验证通过, code: %s", inviteCodeModel.Code)
} else {
l.Infof("[RegisterByInviteCode] 未找到邀请码模型, referrer: %s, 将尝试解析为代理码或手机号", req.Referrer)
}
// 使用事务处理注册
var userID string
var agentID string
var agentLevel int64
l.Infof("[RegisterByInviteCode] 开始事务处理")
err = l.svcCtx.AgentInviteCodeModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 1. 查找目标用户(通过手机号)
targetUser, findUserErr := l.svcCtx.UserModel.FindOneByMobile(transCtx, sql.NullString{String: encryptedMobile, Valid: true})
if findUserErr != nil && !errors.Is(findUserErr, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败, %v", findUserErr)
}
if targetUser != nil {
l.Infof("[RegisterByInviteCode] 找到目标用户, userId: %s", targetUser.Id)
} else {
l.Infof("[RegisterByInviteCode] 目标用户不存在, 将创建新用户")
}
// 2. 获取当前登录态信息
var currentUserID string
var currentAuthType string
var currentAuthKey string
if claims != nil {
currentUserID = claims.UserId
currentAuthType = claims.AuthType
currentAuthKey = claims.AuthKey
l.Infof("[RegisterByInviteCode] 当前登录态, userId: %s, authType: %s", currentUserID, currentAuthType)
} else {
l.Infof("[RegisterByInviteCode] 未登录状态")
}
// 3. 根据目标用户是否存在,处理用户和认证
if targetUser == nil {
// 场景1: 手机号不存在
l.Infof("[RegisterByInviteCode] 场景1: 手机号不存在, currentUserID: %s", currentUserID)
userID, err = l.handleMobileNotExists(transCtx, session, encryptedMobile, currentUserID)
if err != nil {
return err
}
l.Infof("[RegisterByInviteCode] 场景1处理完成, userID: %s", userID)
} else {
// 场景2: 手机号已存在
l.Infof("[RegisterByInviteCode] 场景2: 手机号已存在, targetUserId: %s, currentUserID: %s", targetUser.Id, currentUserID)
userID, err = l.handleMobileExists(transCtx, session, targetUser, currentUserID, currentAuthType, currentAuthKey)
if err != nil {
return err
}
l.Infof("[RegisterByInviteCode] 场景2处理完成, userID: %s", userID)
}
// 4. 处理邀请码和上级关系
var targetLevel int64
var parentAgentId string
if inviteCodeModel != nil {
targetLevel = inviteCodeModel.TargetLevel
if inviteCodeModel.AgentId.Valid {
parentAgentId = inviteCodeModel.AgentId.String
}
l.Infof("[RegisterByInviteCode] 从邀请码获取, targetLevel: %d, parentAgentId: %s", targetLevel, parentAgentId)
} else {
if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 {
l.Infof("[RegisterByInviteCode] 解析为代理码, code: %d", codeVal)
parentAgent, err := l.findAgentByCode(transCtx, codeVal)
if err != nil {
return errors.Wrapf(err, "")
}
parentAgentId = parentAgent.Id
targetLevel = 1
l.Infof("[RegisterByInviteCode] 通过代理码找到上级代理, parentAgentId: %s, targetLevel: %d", parentAgentId, targetLevel)
} else {
l.Infof("[RegisterByInviteCode] 解析为手机号, referrer: %s", req.Referrer)
encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey)
agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "")
if findErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr)
}
if len(agents) == 0 {
l.Infof("[RegisterByInviteCode] 邀请信息无效, referrer: %s", req.Referrer)
return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "")
}
parentAgentId = agents[0].Id
targetLevel = 1
l.Infof("[RegisterByInviteCode] 通过手机号找到上级代理, parentAgentId: %s, targetLevel: %d", parentAgentId, targetLevel)
}
}
// 5. 创建代理记录
l.Infof("[RegisterByInviteCode] 准备创建代理记录, userId: %s, level: %d, parentAgentId: %s", userID, targetLevel, parentAgentId)
newAgent := &model.Agent{Id: uuid.NewString(), UserId: userID, Level: targetLevel, Mobile: encryptedMobile}
if req.Region != "" {
newAgent.Region = sql.NullString{String: req.Region, Valid: true}
}
if req.WechatId != "" {
newAgent.WechatId = sql.NullString{String: req.WechatId, Valid: true}
}
// 6. 处理上级关系
if parentAgentId != "" {
l.Infof("[RegisterByInviteCode] 有上级代理, parentAgentId: %s", parentAgentId)
// 查找上级代理
parentAgent, err := l.svcCtx.AgentModel.FindOne(transCtx, parentAgentId)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", err)
}
l.Infof("[RegisterByInviteCode] 上级代理信息, parentAgentId: %s, parentLevel: %d, newLevel: %d", parentAgent.Id, parentAgent.Level, newAgent.Level)
// 验证关系是否允许(下级不能比上级等级高)
if newAgent.Level > parentAgent.Level {
l.Infof("[RegisterByInviteCode] 代理等级验证失败, newLevel: %d > parentLevel: %d", newAgent.Level, parentAgent.Level)
return errors.Wrapf(xerr.NewErrMsg("代理等级不能高于上级代理"), "")
}
// 查找团队首领(钻石代理)
teamLeaderId, err := l.findTeamLeader(transCtx, parentAgent.Id)
if err != nil {
return errors.Wrapf(err, "查找团队首领失败")
}
if teamLeaderId != "" {
newAgent.TeamLeaderId = sql.NullString{String: teamLeaderId, Valid: true}
l.Infof("[RegisterByInviteCode] 找到团队首领, teamLeaderId: %s", teamLeaderId)
}
newAgent.AgentCode = 0
_, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent)
if err != nil {
return errors.Wrapf(err, "创建代理记录失败")
}
agentID = newAgent.Id
l.Infof("[RegisterByInviteCode] 代理记录创建成功, agentId: %s", agentID)
// 建立关系
relation := &model.AgentRelation{Id: uuid.NewString(), ParentId: parentAgent.Id, ChildId: agentID, RelationType: 1}
if _, err := l.svcCtx.AgentRelationModel.Insert(transCtx, session, relation); err != nil {
return errors.Wrapf(err, "建立代理关系失败")
}
l.Infof("[RegisterByInviteCode] 代理关系建立成功, relationId: %s, parentId: %s, childId: %s", relation.Id, relation.ParentId, relation.ChildId)
} else {
// 平台发放的钻石邀请码,独立成团队
if targetLevel == 3 {
l.Infof("[RegisterByInviteCode] 钻石代理,独立成团队")
newAgent.AgentCode = 0
_, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent)
if err != nil {
return errors.Wrapf(err, "创建代理记录失败")
}
agentID = newAgent.Id
// 设置自己为团队首领
newAgent.TeamLeaderId = sql.NullString{String: agentID, Valid: true}
if err := l.svcCtx.AgentModel.UpdateInTransaction(transCtx, session, newAgent); err != nil {
return errors.Wrapf(err, "更新团队首领失败")
}
l.Infof("[RegisterByInviteCode] 钻石代理创建成功, agentId: %s, 设置为团队首领", agentID)
} else {
// 普通/黄金代理,但没有上级(异常情况)
l.Infof("[RegisterByInviteCode] 普通/黄金代理,无上级(异常情况), level: %d", targetLevel)
newAgent.AgentCode = 0
_, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent)
if err != nil {
return errors.Wrapf(err, "创建代理记录失败")
}
agentID = newAgent.Id
l.Infof("[RegisterByInviteCode] 代理记录创建成功, agentId: %s", agentID)
}
}
// 7. 初始化钱包
l.Infof("[RegisterByInviteCode] 初始化钱包, agentId: %s", agentID)
wallet := &model.AgentWallet{Id: uuid.NewString(), AgentId: agentID}
if _, err := l.svcCtx.AgentWalletModel.Insert(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "初始化钱包失败")
}
l.Infof("[RegisterByInviteCode] 钱包初始化成功, walletId: %s", wallet.Id)
// 8. 更新邀请码状态
// 钻石级别的邀请码只能使用一次,使用后立即失效
// 普通级别的邀请码可以无限使用,不更新状态
if inviteCodeModel != nil {
if targetLevel == 3 {
// 钻石邀请码:使用后失效
inviteCodeModel.Status = 1 // 已使用(使用后立即失效)
l.Infof("[RegisterByInviteCode] 钻石邀请码,标记为已使用, code: %s", inviteCodeModel.Code)
}
inviteCodeModel.UsedUserId = sql.NullString{String: userID, Valid: true}
inviteCodeModel.UsedAgentId = sql.NullString{String: agentID, Valid: true}
inviteCodeModel.UsedTime = sql.NullTime{Time: time.Now(), Valid: true}
if err := l.svcCtx.AgentInviteCodeModel.UpdateWithVersion(transCtx, session, inviteCodeModel); err != nil {
return errors.Wrapf(err, "更新邀请码状态失败")
}
l.Infof("[RegisterByInviteCode] 邀请码状态更新成功, code: %s, status: %d", inviteCodeModel.Code, inviteCodeModel.Status)
}
// 9. 记录邀请码使用历史(用于统计和查询)
if inviteCodeModel != nil {
usage := &model.AgentInviteCodeUsage{Id: uuid.NewString(), InviteCodeId: inviteCodeModel.Id, Code: inviteCodeModel.Code, UserId: userID, AgentId: agentID, AgentLevel: targetLevel, UsedTime: time.Now()}
if _, err := l.svcCtx.AgentInviteCodeUsageModel.Insert(transCtx, session, usage); err != nil {
return errors.Wrapf(err, "记录邀请码使用历史失败")
}
l.Infof("[RegisterByInviteCode] 邀请码使用历史记录成功, usageId: %s", usage.Id)
}
agentLevel = targetLevel
l.Infof("[RegisterByInviteCode] 事务处理完成, userId: %s, agentId: %s, level: %d", userID, agentID, agentLevel)
return nil
})
if err != nil {
l.Errorf("[RegisterByInviteCode] 事务处理失败: %v", err)
return nil, err
}
l.Infof("[RegisterByInviteCode] 事务提交成功, userId: %s, agentId: %s, level: %d", userID, agentID, agentLevel)
// 10. 生成并返回token
l.Infof("[RegisterByInviteCode] 开始生成token, userId: %s", userID)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成token失败: %v", err)
}
l.Infof("[RegisterByInviteCode] Token生成成功")
now := time.Now().Unix()
// 获取等级名称
levelName := ""
switch agentLevel {
case 1:
levelName = "普通"
case 2:
levelName = "黄金"
case 3:
levelName = "钻石"
}
agent, _ := l.svcCtx.AgentModel.FindOne(l.ctx, agentID)
agentCode := int64(0)
if agent != nil {
agentCode = agent.AgentCode
}
l.Infof("[RegisterByInviteCode] 代理注册成功, userId: %s, agentId: %s, level: %d(%s), agentCode: %d", userID, agentID, agentLevel, levelName, agentCode)
return &types.RegisterByInviteCodeResp{
AccessToken: token,
AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire,
RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter,
AgentId: agentID,
Level: agentLevel,
LevelName: levelName,
AgentCode: agentCode,
}, nil
}
// findTeamLeader 查找团队首领(钻石代理)
func (l *RegisterByInviteCodeLogic) findTeamLeader(ctx context.Context, agentId string) (string, error) {
l.Infof("[findTeamLeader] 开始查找团队首领, agentId: %s", agentId)
currentId := agentId
maxDepth := 100
depth := 0
for depth < maxDepth {
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("child_id = ? AND relation_type = ? AND del_state = ?", currentId, 1, 0)
relations, err := l.svcCtx.AgentRelationModel.FindAll(ctx, builder, "")
if err != nil {
return "", err
}
if len(relations) == 0 {
agent, err := l.svcCtx.AgentModel.FindOne(ctx, currentId)
if err != nil {
return "", err
}
if agent.Level == 3 {
l.Infof("[findTeamLeader] 找到团队首领, agentId: %s, level: %d", agent.Id, agent.Level)
return agent.Id, nil
}
l.Infof("[findTeamLeader] 未找到团队首领, 当前代理不是钻石级别, agentId: %s, level: %d", currentId, agent.Level)
return "", nil
}
parentAgent, err := l.svcCtx.AgentModel.FindOne(ctx, relations[0].ParentId)
if err != nil {
return "", err
}
l.Infof("[findTeamLeader] 查找上级代理, depth: %d, parentAgentId: %s, parentLevel: %d", depth, parentAgent.Id, parentAgent.Level)
if parentAgent.Level == 3 {
l.Infof("[findTeamLeader] 找到团队首领, agentId: %s, level: %d", parentAgent.Id, parentAgent.Level)
return parentAgent.Id, nil
}
currentId = parentAgent.Id
depth++
}
l.Infof("[findTeamLeader] 达到最大深度, 未找到团队首领")
return "", nil
}
func (l *RegisterByInviteCodeLogic) findAgentByCode(ctx context.Context, code int64) (*model.Agent, error) {
l.Infof("[findAgentByCode] 通过代理码查找代理, code: %d", code)
builder := l.svcCtx.AgentModel.SelectBuilder().Where("agent_code = ? AND del_state = ?", code, globalkey.DelStateNo).Limit(1)
agents, err := l.svcCtx.AgentModel.FindAll(ctx, builder, "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", err)
}
if len(agents) == 0 {
l.Infof("[findAgentByCode] 代理码不存在, code: %d", code)
return nil, errors.Wrapf(xerr.NewErrMsg("上级邀请码不存在"), "")
}
l.Infof("[findAgentByCode] 找到代理, agentId: %s, code: %d", agents[0].Id, code)
return agents[0], nil
}
func (l *RegisterByInviteCodeLogic) allocateAgentCode(ctx context.Context, session sqlx.Session) (int64, error) {
builder := l.svcCtx.AgentModel.SelectBuilder().OrderBy("agent_code DESC").Limit(1)
rows, err := l.svcCtx.AgentModel.FindAll(ctx, builder, "")
if err != nil {
return 0, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理编码失败, %v", err)
}
var next int64 = 16800
if len(rows) > 0 && rows[0].AgentCode > 0 {
next = rows[0].AgentCode + 1
}
return next, nil
}
// handleMobileNotExists 处理手机号不存在的情况
func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, session sqlx.Session, encryptedMobile string, currentUserID string) (string, error) {
if currentUserID == "" {
// 场景1.1: 未登录 + 手机号不存在 -> 创建新用户
l.Infof("[handleMobileNotExists] 场景1.1: 未登录+手机号不存在, 创建新用户")
newUser := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: encryptedMobile, Valid: true}}
if _, err := l.svcCtx.UserModel.Insert(ctx, session, newUser); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
}
l.Infof("[handleMobileNotExists] 用户创建成功, userId: %s", newUser.Id)
// 创建 mobile 认证
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: newUser.Id,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
}
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", newUser.Id)
return newUser.Id, nil
} else {
// 场景1.2: 已登录临时用户 + 手机号不存在 -> 升级为正式用户
// 前置检查已保证不是正式用户,所以这里一定是临时用户
l.Infof("[handleMobileNotExists] 场景1.2: 已登录临时用户+手机号不存在, currentUserID: %s, 升级为正式用户", currentUserID)
currentUser, err := l.svcCtx.UserModel.FindOne(ctx, currentUserID)
if err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询当前用户失败: %v", err)
}
// 升级为正式用户
currentUser.Mobile = sql.NullString{String: encryptedMobile, Valid: true}
if _, err := l.svcCtx.UserModel.Update(ctx, session, currentUser); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err)
}
l.Infof("[handleMobileNotExists] 用户升级为正式用户成功, userId: %s", currentUserID)
// 创建 mobile 认证
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: currentUserID,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
}
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", currentUserID)
return currentUserID, nil
}
}
// handleMobileExists 处理手机号已存在的情况
func (l *RegisterByInviteCodeLogic) handleMobileExists(ctx context.Context, session sqlx.Session, targetUser *model.User, currentUserID string, currentAuthType string, currentAuthKey string) (string, error) {
userID := targetUser.Id
l.Infof("[handleMobileExists] 开始处理, targetUserId: %s, currentUserID: %s", userID, currentUserID)
// 检查目标用户是否已是代理
existingAgent, err := l.svcCtx.AgentModel.FindOneByUserId(ctx, userID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err)
}
if existingAgent != nil {
l.Infof("[handleMobileExists] 目标用户已是代理, userId: %s, agentId: %s", userID, existingAgent.Id)
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已经是代理,不能重复注册"), "")
}
if currentUserID == "" {
// 场景2.1: 未登录 + 手机号存在 -> 直接使用目标用户(验证码已确认身份)
l.Infof("[handleMobileExists] 场景2.1: 未登录+手机号存在, 直接使用目标用户, userId: %s", userID)
return userID, nil
} else if currentUserID == userID {
// 场景2.2: 已登录正式用户 + 手机号匹配 -> 直接使用
// 前置检查已保证手机号匹配且不是代理
l.Infof("[handleMobileExists] 场景2.2: 已登录正式用户+手机号匹配, userId: %s", userID)
return userID, nil
} else {
// 场景2.3: 已登录临时用户 + 手机号存在 -> 需要合并账号
// 前置检查已保证是临时用户(不是正式用户)
l.Infof("[handleMobileExists] 场景2.3: 已登录临时用户+手机号存在, 需要合并账号, sourceUserId: %s, targetUserId: %s, authType: %s", currentUserID, userID, currentAuthType)
return l.mergeTempUserToTarget(ctx, session, currentUserID, userID, currentAuthType, currentAuthKey)
}
}
// mergeTempUserToTarget 合并临时用户到目标用户
func (l *RegisterByInviteCodeLogic) mergeTempUserToTarget(ctx context.Context, session sqlx.Session, sourceUserID string, targetUserID string, currentAuthType string, currentAuthKey string) (string, error) {
l.Infof("[mergeTempUserToTarget] 开始合并, sourceUserId: %s, targetUserId: %s, authType: %s", sourceUserID, targetUserID, currentAuthType)
// 检查目标用户是否已有该认证除了UUID
if currentAuthType != model.UserAuthTypeUUID {
targetAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, currentAuthType)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err)
}
if targetAuth != nil && targetAuth.AuthKey != currentAuthKey {
// 目标用户已有该类型的其他认证,证明手机号绑定过其他微信等
l.Infof("[mergeTempUserToTarget] 目标用户已有其他认证, targetUserId: %s, authType: %s, targetAuthKey: %s, currentAuthKey: %s", targetUserID, currentAuthType, targetAuth.AuthKey, currentAuthKey)
if currentAuthType == model.UserAuthTypeWxh5OpenID {
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
}
if currentAuthType == model.UserAuthTypeWxMiniOpenID {
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
}
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他终端"), "")
}
l.Infof("[mergeTempUserToTarget] 微信唯一性检查通过, targetUserId: %s, authType: %s", targetUserID, currentAuthType)
}
// 查找当前认证
existingAuth, err := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(ctx, currentAuthType, currentAuthKey)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err)
}
if existingAuth != nil {
l.Infof("[mergeTempUserToTarget] 找到现有认证, authId: %s, userId: %s", existingAuth.Id, existingAuth.UserId)
} else {
l.Infof("[mergeTempUserToTarget] 未找到现有认证, 将创建新认证")
}
// 执行账号合并
if err := l.mergeUserAccounts(ctx, session, sourceUserID, targetUserID, currentAuthType, currentAuthKey, existingAuth); err != nil {
return "", err
}
l.Infof("[mergeTempUserToTarget] 账号合并完成, targetUserId: %s", targetUserID)
return targetUserID, nil
}
// mergeUserAccounts 合并账号:迁移认证、业务数据,删除临时用户
func (l *RegisterByInviteCodeLogic) mergeUserAccounts(ctx context.Context, session sqlx.Session, sourceUserID string, targetUserID string, currentAuthType string, currentAuthKey string, existingAuth *model.UserAuth) error {
l.Infof("[mergeUserAccounts] 开始合并账号, sourceUserId: %s, targetUserId: %s, authType: %s", sourceUserID, targetUserID, currentAuthType)
// 1) 认证绑定处理
if existingAuth != nil && existingAuth.UserId != targetUserID {
l.Infof("[mergeUserAccounts] 认证存在但不属于目标用户, 开始迁移认证, authId: %s, currentUserId: %s, targetUserId: %s", existingAuth.Id, existingAuth.UserId, targetUserID)
// 认证存在但不属于目标用户,迁移到目标用户
if currentAuthType == model.UserAuthTypeUUID {
// UUID替换策略如果目标用户已有UUID认证替换UUID否则迁移认证
l.Infof("[mergeUserAccounts] UUID认证类型, 执行替换策略")
targetUUIDAuth, _ := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, model.UserAuthTypeUUID)
if targetUUIDAuth != nil {
// 目标用户已有UUID认证删除源认证并更新目标UUID
l.Infof("[mergeUserAccounts] 目标用户已有UUID认证, 删除源认证并更新目标UUID, targetAuthId: %s", targetUUIDAuth.Id)
if err := l.svcCtx.UserAuthModel.Delete(ctx, session, existingAuth.Id); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除旧UUID认证失败: %v", err)
}
if targetUUIDAuth.AuthKey != currentAuthKey {
targetUUIDAuth.AuthKey = currentAuthKey
if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, targetUUIDAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新目标UUID认证失败: %v", err)
}
l.Infof("[mergeUserAccounts] UUID认证更新成功, authId: %s", targetUUIDAuth.Id)
}
} else {
// 目标用户没有UUID认证迁移源认证
l.Infof("[mergeUserAccounts] 目标用户没有UUID认证, 迁移源认证, authId: %s", existingAuth.Id)
existingAuth.UserId = targetUserID
if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, existingAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "迁移UUID认证失败: %v", err)
}
l.Infof("[mergeUserAccounts] UUID认证迁移成功")
}
} else {
// 其他认证类型,直接迁移
l.Infof("[mergeUserAccounts] 其他认证类型, 直接迁移, authId: %s, authType: %s", existingAuth.Id, currentAuthType)
existingAuth.UserId = targetUserID
if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, existingAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "迁移认证失败: %v", err)
}
l.Infof("[mergeUserAccounts] 认证迁移成功, authId: %s", existingAuth.Id)
}
} else if existingAuth == nil {
// 认证不存在,创建新认证
l.Infof("[mergeUserAccounts] 认证不存在, 创建新认证, authType: %s", currentAuthType)
if currentAuthType == model.UserAuthTypeUUID {
// UUID特殊处理如果目标用户已有UUID认证更新UUID否则创建新认证
targetUUIDAuth, _ := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, model.UserAuthTypeUUID)
if targetUUIDAuth != nil {
l.Infof("[mergeUserAccounts] 目标用户已有UUID认证, 更新UUID, authId: %s", targetUUIDAuth.Id)
if targetUUIDAuth.AuthKey != currentAuthKey {
targetUUIDAuth.AuthKey = currentAuthKey
if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, targetUUIDAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新目标UUID认证失败: %v", err)
}
l.Infof("[mergeUserAccounts] UUID认证更新成功")
}
} else {
newAuth := &model.UserAuth{
Id: uuid.NewString(),
UserId: targetUserID,
AuthType: currentAuthType,
AuthKey: currentAuthKey,
}
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, newAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建UUID认证失败: %v", err)
}
l.Infof("[mergeUserAccounts] UUID认证创建成功, authId: %s", newAuth.Id)
}
} else {
// 其他认证类型,创建新认证
newAuth := &model.UserAuth{
Id: uuid.NewString(),
UserId: targetUserID,
AuthType: currentAuthType,
AuthKey: currentAuthKey,
}
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, newAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建认证失败: %v", err)
}
l.Infof("[mergeUserAccounts] 认证创建成功, authId: %s, authType: %s", newAuth.Id, currentAuthType)
}
}
// 2) 业务数据迁移:迁移订单和报告到目标用户
l.Infof("[mergeUserAccounts] 开始迁移业务数据, sourceUserId: %s, targetUserId: %s", sourceUserID, targetUserID)
if err := l.svcCtx.OrderModel.UpdateUserIDWithSession(ctx, session, sourceUserID, targetUserID); err != nil {
return errors.Wrapf(err, "迁移订单失败")
}
l.Infof("[mergeUserAccounts] 订单迁移完成")
if err := l.svcCtx.QueryModel.UpdateUserIDWithSession(ctx, session, sourceUserID, targetUserID); err != nil {
return errors.Wrapf(err, "迁移报告失败")
}
l.Infof("[mergeUserAccounts] 报告迁移完成")
// 3) 删除临时用户
l.Infof("[mergeUserAccounts] 开始删除临时用户, sourceUserId: %s", sourceUserID)
sourceUser, err := l.svcCtx.UserModel.FindOne(ctx, sourceUserID)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找源用户失败: %v", err)
}
if err := l.svcCtx.UserModel.Delete(ctx, session, sourceUser.Id); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除源用户失败: %v", err)
}
l.Infof("[mergeUserAccounts] 临时用户删除成功, sourceUserId: %s", sourceUserID)
l.Infof("[mergeUserAccounts] 账号合并完成, targetUserId: %s", targetUserID)
return nil
}

View File

@@ -0,0 +1,108 @@
package agent
import (
"context"
"fmt"
"net/http"
"strings"
"qnc-server/app/main/model"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"qnc-server/app/main/api/internal/svc"
)
type ShortLinkRedirectLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewShortLinkRedirectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ShortLinkRedirectLogic {
return &ShortLinkRedirectLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// ShortLinkRedirect 短链重定向
// 从短链重定向到推广页面
func (l *ShortLinkRedirectLogic) ShortLinkRedirect(shortCode string, r *http.Request, w http.ResponseWriter) error {
// 1. 验证短链标识
if shortCode == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "缺少短链标识")
}
// 2. 查询短链记录
shortLink, err := l.svcCtx.AgentShortLinkModel.FindOneByShortCodeDelState(l.ctx, shortCode, globalkey.DelStateNo)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "短链不存在或已失效")
}
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询短链失败, %v", err)
}
// 3. 根据类型验证链接有效性
if shortLink.Type == 1 {
// 推广报告类型验证linkIdentifier是否存在
if shortLink.LinkIdentifier.Valid && shortLink.LinkIdentifier.String != "" {
_, err = l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, shortLink.LinkIdentifier.String)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "推广链接不存在或已失效")
}
l.Errorf("查询推广链接失败: %v", err)
// 即使查询失败,也继续重定向,避免影响用户体验
}
}
} else if shortLink.Type == 2 {
if shortLink.InviteCode.Valid && shortLink.InviteCode.String != "" {
_, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, shortLink.InviteCode.String)
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("查询邀请码失败: %v", err)
}
}
}
// 4. 构建重定向URL
redirectURL := shortLink.TargetPath
if redirectURL == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短链目标地址为空")
}
// 如果 target_path 是相对路径,需要拼接正式域名
// 如果 target_path 已经是完整URL则直接使用
if !strings.HasPrefix(redirectURL, "http://") && !strings.HasPrefix(redirectURL, "https://") {
// 相对路径,需要拼接正式域名
officialDomain := l.svcCtx.Config.Promotion.OfficialDomain
if officialDomain == "" {
// 如果没有配置正式域名,使用当前请求的域名(向后兼容)
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
officialDomain = fmt.Sprintf("%s://%s", scheme, r.Host)
}
// 确保正式域名不以 / 结尾
officialDomain = strings.TrimSuffix(officialDomain, "/")
// 确保 target_path 以 / 开头
if !strings.HasPrefix(redirectURL, "/") {
redirectURL = "/" + redirectURL
}
redirectURL = officialDomain + redirectURL
}
// 5. 执行重定向302临时重定向
linkIdentifierStr := ""
if shortLink.LinkIdentifier.Valid {
linkIdentifierStr = shortLink.LinkIdentifier.String
}
l.Infof("短链重定向: shortCode=%s, type=%d, linkIdentifier=%s, redirectURL=%s", shortCode, shortLink.Type, linkIdentifierStr, redirectURL)
http.Redirect(w, r, redirectURL, http.StatusFound)
return nil
}

View File

@@ -0,0 +1,145 @@
package agent
import (
"context"
"database/sql"
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/lzUtils"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"qnc-server/app/main/api/internal/svc"
"qnc-server/app/main/api/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type UpgradeSubordinateLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewUpgradeSubordinateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpgradeSubordinateLogic {
return &UpgradeSubordinateLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpgradeSubordinateLogic) UpgradeSubordinate(req *types.UpgradeSubordinateReq) (resp *types.UpgradeSubordinateResp, err error) {
operatorUserId, err := ctxdata.GetUidFromCtx(l.ctx)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败, %v", err)
}
// 1. 获取操作者代理信息
operatorAgent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, operatorUserId)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "")
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询操作者代理信息失败, %v", err)
}
// 2. 验证权限:必须是钻石代理
if operatorAgent.Level != 3 {
return nil, errors.Wrapf(xerr.NewErrMsg("只有钻石代理可以升级下级"), "")
}
// 3. 获取被升级的代理信息
subordinateAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, req.SubordinateId)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询下级代理信息失败, %v", err)
}
// 4. 验证下级等级:只能是普通代理
if subordinateAgent.Level != 1 {
return nil, errors.Wrapf(xerr.NewErrMsg("只能升级普通代理为黄金代理"), "")
}
// 5. 验证关系:必须是下级(直接或间接)
isSubordinate := l.isSubordinate(operatorAgent.Id, subordinateAgent.Id)
if !isSubordinate {
return nil, errors.Wrapf(xerr.NewErrMsg("该代理不是您的下级"), "")
}
// 6. 验证目标等级:只能升级为黄金
toLevel := req.ToLevel
if toLevel != 2 {
return nil, errors.Wrapf(xerr.NewErrMsg("钻石代理只能将普通代理升级为黄金代理"), "")
}
// 7. 使用事务处理升级
err = l.svcCtx.AgentWalletModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 7.1 创建升级记录
upgradeRecord := &model.AgentUpgrade{
Id: uuid.New().String(),
AgentId: subordinateAgent.Id,
FromLevel: 1, // 普通
ToLevel: toLevel,
UpgradeType: 2, // 钻石升级下级
UpgradeFee: 0, // 免费
RebateAmount: 0, // 无返佣
OperatorAgentId: sql.NullString{String: operatorAgent.Id, Valid: true},
Status: 1, // 待处理
}
_, err := l.svcCtx.AgentUpgradeModel.Insert(transCtx, session, upgradeRecord)
if err != nil {
return errors.Wrapf(err, "创建升级记录失败")
}
// 7.2 执行升级操作
if err := l.svcCtx.AgentService.ProcessUpgrade(transCtx, subordinateAgent.Id, toLevel, 2, 0, 0, "", operatorAgent.Id); err != nil {
return errors.Wrapf(err, "执行升级操作失败")
}
// 7.3 更新升级记录状态
upgradeRecord.Status = 2 // 已完成
upgradeRecord.Remark = lzUtils.StringToNullString("钻石代理升级下级成功")
if err := l.svcCtx.AgentUpgradeModel.UpdateWithVersion(transCtx, session, upgradeRecord); err != nil {
return errors.Wrapf(err, "更新升级记录失败")
}
return nil
})
if err != nil {
return nil, err
}
return &types.UpgradeSubordinateResp{
Success: true,
}, nil
}
// isSubordinate 递归检查 targetId 是否是 parentId 的下级(直接或间接)
func (l *UpgradeSubordinateLogic) isSubordinate(parentId, targetId string) bool {
// 查询直接下级
builder := l.svcCtx.AgentRelationModel.SelectBuilder().
Where("parent_id = ? AND relation_type = ? AND del_state = ?", parentId, 1, globalkey.DelStateNo)
relations, err := l.svcCtx.AgentRelationModel.FindAll(l.ctx, builder, "")
if err != nil {
return false
}
for _, relation := range relations {
// 如果是直接下级,返回 true
if relation.ChildId == targetId {
return true
}
// 递归检查间接下级
if l.isSubordinate(relation.ChildId, targetId) {
return true
}
}
return false
}