Files
ycc-proxy-server/app/main/api/internal/logic/agent/generatinglinklogic.go
2025-12-02 19:57:10 +08:00

327 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package agent
import (
"context"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
"ycc-server/common/tool"
"ycc-server/common/xerr"
"ycc-server/pkg/lzkit/crypto"
"github.com/Masterminds/squirrel"
"github.com/pkg/errors"
"ycc-server/app/main/api/internal/svc"
"ycc-server/app/main/api/internal/types"
"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)
}
// 4. 计算实际底价(产品基础底价 + 等级加成)
basePrice := productConfig.BasePrice
actualBasePrice := basePrice + float64(levelBonus)
systemMaxPrice := productConfig.SystemMaxPrice
// 5. 验证设定价格范围
if req.SetPrice < actualBasePrice || req.SetPrice > systemMaxPrice {
return nil, errors.Wrapf(xerr.NewErrMsg("设定价格必须在 %.2f 到 %.2f 之间"), "设定价格必须在 %.2f 到 %.2f 之间", actualBasePrice, systemMaxPrice)
}
// 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, 0, 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{
AgentId: agentModel.Id,
UserId: userID,
ProductId: req.ProductId,
LinkIdentifier: encrypted,
SetPrice: req.SetPrice,
ActualBasePrice: actualBasePrice,
}
result, err := l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存推广链接失败, %v", err)
}
// 获取插入的ID
linkId, err := result.LastInsertId()
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广链接ID失败, %v", err)
}
// 使用默认target_path如果未提供
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
// 生成短链类型1=推广报告)
shortLink, err := l.createShortLink(1, linkId, 0, 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
}
// getOrCreateShortLink 获取或创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
// linkId: 推广链接ID仅推广报告使用
// inviteCodeId: 邀请码ID仅邀请好友使用
// linkIdentifier: 推广链接标识(仅推广报告使用)
// inviteCode: 邀请码(仅邀请好友使用)
// targetPath: 目标地址(前端传入)
func (l *GeneratingLinkLogic) getOrCreateShortLink(linkType int64, linkId, inviteCodeId int64, linkIdentifier, inviteCode, targetPath string) (string, error) {
// 先查询是否已存在短链
var existingShortLink *model.AgentShortLink
var err error
if linkType == 1 {
// 推广报告类型使用link_id查询
if linkId > 0 {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByLinkIdTypeDelState(l.ctx, sql.NullInt64{Int64: linkId, Valid: true}, linkType, globalkey.DelStateNo)
}
} else {
// 邀请好友类型使用invite_code_id查询
if inviteCodeId > 0 {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullInt64{Int64: 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 int64, 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{
Type: linkType,
ShortCode: shortCode,
TargetPath: targetPath,
PromotionDomain: promotionConfig.PromotionDomain,
}
// 根据类型设置对应字段
if linkType == 1 {
// 推广报告类型
shortLink.LinkId = sql.NullInt64{Int64: linkId, Valid: linkId > 0}
if linkIdentifier != "" {
shortLink.LinkIdentifier = sql.NullString{String: linkIdentifier, Valid: true}
}
} else if linkType == 2 {
// 邀请好友类型
shortLink.InviteCodeId = sql.NullInt64{Int64: inviteCodeId, Valid: inviteCodeId > 0}
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)
}