327 lines
11 KiB
Go
327 lines
11 KiB
Go
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)
|
||
}
|