使用Go语言开发一个短链接服务:五、添加和获取短链接

章节

使用Go语言开发一个短链接服务:一、基本原理  

使用Go语言开发一个短链接服务:二、架构设计

使用Go语言开发一个短链接服务:三、项目目录结构设计

使用Go语言开发一个短链接服务:四、生成code算法

使用Go语言开发一个短链接服务:五、添加和获取短链接

使用Go语言开发一个短链接服务:六、链接跳转

  Gitee https://gitee.com/alxps/short_link

  Github https://github.com/1911860538/short_link

 

  上一篇说明了短链接code的生成算法,这一篇讲述怎么添加和获取短链接。

   本篇涉及的代码看这里https://gitee.com/alxps/short_link/tree/master/app/server/service

 

添加短链接

  简单来说就是登录用户,发来长链接,我们生成短链接code,并保存这条数据。分四个步骤:

    1、校验long_url合法性

    2、检查该用户是不是已经为long_url生成短链接

    3、生成短链接code

    4、保存到数据库

  步骤1,检验url合法性包括两项,是否为一个合法的http或https url,以及http请求url是否能正常响应。至于http请求url,我们优先使用head请求,如果head请求返回405,则再使用get请求。因为head请求相比get请求更清凉,只传回响应头,也就是资源的“元信息”,但有可能部分服务器不支持head请求。

  步骤2,很简单,到数据库查询user_id和long_url的数据是否存在。

  步骤3,生成code,看上一篇, 使用Go语言开发一个短链接服务:四、生成code算法

  步骤4,将数据保存到数据库,如果code在数据库已存在,则重新生成code,递归,直到code不重复。

  上代码,由于添加短链接的handler只是负责http入参和出参的处理,代码不贴,直接看service

app/server/service/add_link.go

package service

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"hash/fnv"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"time"
	"unsafe"

	"github.com/1911860538/short_link/app/component"
	"github.com/1911860538/short_link/config"
)

type AddLinkSvc struct {
	Database component.DatabaseItf
}

type AddLinkParams struct {
	UserId   string
	LongUrl  string
	Deadline time.Time
}

type AddLinkRes struct {
	StatusCode int
	Msg        string

	Code string
}

const (
	msgUrlInvalid = "不是一个合法的http或https链接"
	msgUrlRespErr = "链接请求未能正常响应"
)

var (
	confLongUrlConnTimeout = time.Duration(config.Conf.Core.LongUrlConnTimeout) * time.Second
	confExpiredKeepHours   = time.Duration(config.Conf.Core.ExpiredKeepDays*24) * time.Hour
)

func (s *AddLinkSvc) Do(ctx context.Context, params AddLinkParams) (AddLinkRes, error) {
	// 检查url合法性
	u, err := url.ParseRequestURI(params.LongUrl)
	if err != nil {
		return s.badRequest(msgUrlInvalid)
	}
	if u.Scheme != "http" && u.Scheme != "https" {
		return s.badRequest(msgUrlInvalid)
	}
	client := http.Client{
		Timeout: confLongUrlConnTimeout,
	}
	headResp, err := client.Head(u.String())
	if err != nil {
		return s.badRequest(msgUrlRespErr)
	}
	if headResp.Body != nil {
		defer headResp.Body.Close()
	}
	respOk := headResp.StatusCode == http.StatusOK
	// 使用GET,部分服务器不支持HEAD请求
	if !respOk && headResp.StatusCode == http.StatusMethodNotAllowed {
		getResp, err := client.Get(u.String())
		if err != nil {
			return s.badRequest(msgUrlRespErr)
		}
		if getResp.Body != nil {
			defer getResp.Body.Close()
		}
		respOk = getResp.StatusCode == http.StatusOK
	}
	if !respOk {
		return s.badRequest(msgUrlRespErr)
	}

	// 检查这个userId是不是已经生成了此longUrl的code
	filter := map[string]any{
		"user_id":  params.UserId,
		"long_url": params.LongUrl,
	}
	oldLink, err := s.Database.Get(ctx, filter)
	if err != nil {
		return s.internalErr(err)
	}
	if oldLink != nil && !oldLink.Expired() {
		return s.codeConflicted(oldLink.Code)
	}

	// 生成longUrl对应的code
	code, err := GenCode(params.UserId, params.LongUrl, "")
	if err != nil {
		return s.internalErr(err)
	}

	// 保存到数据库,这里要注意可能和数据库code冲突
	var ttlTime time.Time
	if params.Deadline.IsZero() {
		ttlTime = time.Time{}
	} else {
		ttlTime = params.Deadline.Add(confExpiredKeepHours)
	}
	link := &component.Link{
		UserId:    params.UserId,
		Code:      code,
		Salt:      "",
		LongUrl:   params.LongUrl,
		Deadline:  params.Deadline,
		TtlTime:   ttlTime,
		CreatedAt: time.Now().UTC(),
		UpdatedAt: time.Now().UTC(),
	}

	if err := s.trySaveLink(ctx, link); err != nil {
		return s.internalErr(err)
	}

	return s.ok(link.Code)
}

func (s *AddLinkSvc) ok(code string) (AddLinkRes, error) {
	return AddLinkRes{
		StatusCode: http.StatusCreated,
		Code:       code,
	}, nil
}

func (s *AddLinkSvc) badRequest(errMsg string) (AddLinkRes, error) {
	return AddLinkRes{
		StatusCode: http.StatusBadRequest,
		Msg:        errMsg,
	}, nil
}

func (s *AddLinkSvc) codeConflicted(code string) (AddLinkRes, error) {
	return AddLinkRes{
		StatusCode: http.StatusConflict,
		Msg:        fmt.Sprintf("你已对该链接已生成了对应的短链接,短链接code为:%s", code),
	}, nil
}

func (s *AddLinkSvc) internalErr(err error) (AddLinkRes, error) {
	return AddLinkRes{
		StatusCode: http.StatusInternalServerError,
	}, err
}

func (s *AddLinkSvc) trySaveLink(ctx context.Context, link *component.Link) error {
	_, existed, err := s.Database.Create(ctx, link)
	if err != nil {
		return err
	}
	if !existed {
		return nil
	}

	nowTimestampStr := strconv.FormatInt(time.Now().UnixMilli(), 10)
	link.Salt = nowTimestampStr
	link.Code, err = GenCode(link.UserId, link.Code, nowTimestampStr)
	if err != nil {
		return err
	}

	return s.trySaveLink(ctx, link)
}

const letters = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

// GenCode
/*
下面一通计算,
和随机生成字母数字code的区别是,
尽量保证同样的userId+longUrl每次生成的code一样,
如果userId+longUrl生成了数据库已有的code,
则加上当前时间戳字符串作为盐salt,
递归,直到生成的code数据库中没有
*/
func GenCode(userId string, longUlr string, salt string) (string, error) {
	// 首先对userId+longUrl+salt md5 主要为了防止longUrl包含汉字等字符串
	hasher := md5.New()
	if _, err := io.WriteString(hasher, userId+longUlr+salt); err != nil {
		return "", err
	}
	hashStr := hex.EncodeToString(hasher.Sum(nil))

	stepLen := len(hashStr) / confCodeLen
	remain := len(hashStr) % confCodeLen
	if remain > 0 {
		stepLen += 1
	}
	lettersLen := uint32(len(letters))
	b := make([]byte, confCodeLen)

	for i := 0; i < confCodeLen; i++ {
		// 根据要生成的code长度,切分md5字符串
		var piece string
		if remain > 0 && i == confCodeLen-1 {
			piece = hashStr[i*stepLen : i*stepLen+remain]
		} else {
			piece = hashStr[i*stepLen : i*stepLen+stepLen]
		}

		// 为切片元素生成对应的整形数值
		h := fnv.New32a()
		pieceBytes := unsafe.Slice(unsafe.StringData(piece), len(piece))
		if _, err := h.Write(pieceBytes); err != nil {
			return "", err
		}
		pieceHash32 := h.Sum32()

		// 切片字符的整形,取len(letters)余数,并取letters索引为该余数的letter
		letterIdx := pieceHash32 % lettersLen
		b[i] = letters[letterIdx]
	}

	return unsafe.String(unsafe.SliceData(b), len(b)), nil
}

 

获取短链接

  用户登录情况下,输入code或者长链接url,获取链接信息(code/long_url/deadline)。直接上代码

app/server/service/get_link.go

package service

import (
	"context"
	"net/http"
	"time"

	"github.com/1911860538/short_link/app/component"
)

type GetLinkSvc struct {
	Database component.DatabaseItf
}

type GetLinkParams struct {
	UserId  string
	Code    string
	LongUrl string
}

type GetLinkRes struct {
	StatusCode int
	Msg        string

	Code     string
	LongUrl  string
	Deadline time.Time
}

func (s *GetLinkSvc) Do(ctx context.Context, params GetLinkParams) (GetLinkRes, error) {
	filter := make(map[string]any)
	if params.UserId != "" {
		filter["user_id"] = params.UserId
	}
	if params.Code != "" {
		filter["code"] = params.Code
	}
	if params.LongUrl != "" {
		filter["long_url"] = params.LongUrl
	}

	link, err := s.Database.Get(ctx, filter)
	if err != nil {
		return GetLinkRes{
			StatusCode: http.StatusInternalServerError,
		}, err
	}

	if link == nil {
		return GetLinkRes{
			StatusCode: http.StatusNotFound,
			Msg:        "数据不存在",
		}, nil
	}

	return GetLinkRes{
		StatusCode: http.StatusOK,
		Code:       link.Code,
		LongUrl:    link.LongUrl,
		Deadline:   link.Deadline,
	}, nil
}

 

总结

  下一篇,服务核心逻辑,短链接跳转到长链接,敬请期待~

  

posted @ 2024-03-26 12:05  ALXPS  阅读(124)  评论(0编辑  收藏  举报