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/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) }