Files
ycc-proxy-server/app/main/api/internal/logic/agent/generatinglinklogic.go

352 lines
12 KiB
Go
Raw Normal View History

2025-11-27 13:09:54 +08:00
package agent
import (
"context"
2025-12-02 19:57:10 +08:00
"database/sql"
2025-11-27 13:09:54 +08:00
"encoding/hex"
"encoding/json"
2025-12-02 19:57:10 +08:00
"fmt"
"net/url"
2025-11-27 13:09:54 +08:00
"strconv"
2025-12-02 19:57:10 +08:00
"strings"
2025-11-27 13:09:54 +08:00
"ycc-server/app/main/model"
"ycc-server/common/ctxdata"
"ycc-server/common/globalkey"
2025-12-02 19:57:10 +08:00
"ycc-server/common/tool"
2025-11-27 13:09:54 +08:00
"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"
2025-12-09 18:55:28 +08:00
"github.com/google/uuid"
2025-11-27 13:09:54 +08:00
"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)
}
2025-12-02 19:57:10 +08:00
// 2. 获取产品配置(必须存在)
productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, req.ProductId)
2025-11-27 13:09:54 +08:00
if err != nil {
2025-12-02 19:57:10 +08:00
if errors.Is(err, model.ErrNotFound) {
return 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)
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
// 3. 获取等级加成配置
levelBonus, err := l.getLevelBonus(agentModel.Level)
2025-11-27 13:09:54 +08:00
if err != nil {
2025-12-02 19:57:10 +08:00
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取等级加成配置失败, %v", err)
2025-11-27 13:09:54 +08:00
}
2025-12-09 18:55:28 +08:00
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)
}
2025-11-27 13:09:54 +08:00
// 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 {
2025-12-02 19:57:10 +08:00
// 已存在,检查是否有短链,如果没有则生成
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
2025-12-09 18:55:28 +08:00
shortLink, err := l.getOrCreateShortLink(1, existingLinks[0].Id, "", existingLinks[0].LinkIdentifier, "", targetPath)
2025-12-02 19:57:10 +08:00
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取或创建短链失败, %v", err)
}
2025-11-27 13:09:54 +08:00
return &types.AgentGeneratingLinkResp{
LinkIdentifier: existingLinks[0].LinkIdentifier,
2025-12-02 19:57:10 +08:00
FullLink: shortLink,
2025-11-27 13:09:54 +08:00
}, 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{
2025-12-09 18:55:28 +08:00
// Id: uuid.NewString(),
2025-11-27 13:09:54 +08:00
AgentId: agentModel.Id,
UserId: userID,
ProductId: req.ProductId,
LinkIdentifier: encrypted,
SetPrice: req.SetPrice,
ActualBasePrice: actualBasePrice,
}
2025-12-09 18:55:28 +08:00
_, err = l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, agentLink)
2025-11-27 13:09:54 +08:00
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "保存推广链接失败, %v", err)
}
2025-12-09 18:55:28 +08:00
linkId := agentLink.Id
2025-12-02 19:57:10 +08:00
// 使用默认target_path如果未提供
targetPath := req.TargetPath
if targetPath == "" {
targetPath = "/agent/promotionInquire/"
}
// 生成短链类型1=推广报告)
2025-12-09 18:55:28 +08:00
shortLink, err := l.createShortLink(1, linkId, "", encrypted, "", targetPath)
2025-12-02 19:57:10 +08:00
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成短链失败, %v", err)
}
2025-11-27 13:09:54 +08:00
return &types.AgentGeneratingLinkResp{
LinkIdentifier: encrypted,
2025-12-02 19:57:10 +08:00
FullLink: shortLink,
2025-11-27 13:09:54 +08:00
}, nil
}
2025-12-02 19:57:10 +08:00
// getLevelBonus 获取等级加成(从配置表读取)
func (l *GeneratingLinkLogic) getLevelBonus(level int64) (int64, error) {
var configKey string
2025-11-27 13:09:54 +08:00
switch level {
case 1: // 普通
2025-12-02 19:57:10 +08:00
configKey = "level_1_bonus"
2025-11-27 13:09:54 +08:00
case 2: // 黄金
2025-12-02 19:57:10 +08:00
configKey = "level_2_bonus"
2025-11-27 13:09:54 +08:00
case 3: // 钻石
2025-12-02 19:57:10 +08:00
configKey = "level_3_bonus"
2025-11-27 13:09:54 +08:00
default:
2025-12-02 19:57:10 +08:00
return 0, nil
2025-11-27 13:09:54 +08:00
}
config, err := l.svcCtx.AgentConfigModel.FindOneByConfigKey(l.ctx, configKey)
if err != nil {
2025-12-02 19:57:10 +08:00
// 配置不存在时返回默认值
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
2025-11-27 13:09:54 +08:00
}
2025-12-02 19:57:10 +08:00
2025-11-27 13:09:54 +08:00
value, err := strconv.ParseFloat(config.ConfigValue, 64)
if err != nil {
return 0, errors.Wrapf(err, "解析配置值失败, key: %s, value: %s", configKey, config.ConfigValue)
}
2025-12-02 19:57:10 +08:00
return int64(value), nil
}
2025-12-09 18:55:28 +08:00
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
}
2025-12-02 19:57:10 +08:00
// getOrCreateShortLink 获取或创建短链
// type: 1=推广报告(promotion), 2=邀请好友(invite)
// linkId: 推广链接ID仅推广报告使用
// inviteCodeId: 邀请码ID仅邀请好友使用
// linkIdentifier: 推广链接标识(仅推广报告使用)
// inviteCode: 邀请码(仅邀请好友使用)
// targetPath: 目标地址(前端传入)
2025-12-09 18:55:28 +08:00
func (l *GeneratingLinkLogic) getOrCreateShortLink(linkType int64, linkId, inviteCodeId string, linkIdentifier, inviteCode, targetPath string) (string, error) {
2025-12-02 19:57:10 +08:00
// 先查询是否已存在短链
var existingShortLink *model.AgentShortLink
var err error
if linkType == 1 {
// 推广报告类型使用link_id查询
2025-12-09 18:55:28 +08:00
if linkId != "" {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByLinkIdTypeDelState(l.ctx, sql.NullString{String: linkId, Valid: true}, linkType, globalkey.DelStateNo)
2025-12-02 19:57:10 +08:00
}
} else {
// 邀请好友类型使用invite_code_id查询
2025-12-09 18:55:28 +08:00
if inviteCodeId != "" {
existingShortLink, err = l.svcCtx.AgentShortLinkModel.FindOneByInviteCodeIdTypeDelState(l.ctx, sql.NullString{String: inviteCodeId, Valid: true}, linkType, globalkey.DelStateNo)
2025-12-02 19:57:10 +08:00
}
}
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)
2025-12-09 18:55:28 +08:00
func (l *GeneratingLinkLogic) createShortLink(linkType int64, linkId, inviteCodeId string, linkIdentifier, inviteCode, targetPath string) (string, error) {
2025-12-02 19:57:10 +08:00
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{
2025-12-09 18:55:28 +08:00
Id: uuid.NewString(),
2025-12-02 19:57:10 +08:00
Type: linkType,
ShortCode: shortCode,
TargetPath: targetPath,
PromotionDomain: promotionConfig.PromotionDomain,
}
// 根据类型设置对应字段
if linkType == 1 {
// 推广报告类型
2025-12-09 18:55:28 +08:00
shortLink.LinkId = sql.NullString{String: linkId, Valid: linkId != ""}
2025-12-02 19:57:10 +08:00
if linkIdentifier != "" {
shortLink.LinkIdentifier = sql.NullString{String: linkIdentifier, Valid: true}
}
} else if linkType == 2 {
// 邀请好友类型
2025-12-09 18:55:28 +08:00
shortLink.InviteCodeId = sql.NullString{String: inviteCodeId, Valid: inviteCodeId != ""}
2025-12-02 19:57:10 +08:00
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)
2025-11-27 13:09:54 +08:00
}