首页 > 其他分享 >使用Go语言开发一个短链接服务:五、添加和获取短链接

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

时间:2024-03-26 12:12:33浏览次数:23  
标签:code return nil err 添加 link Go 链接

章节

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

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

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

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

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

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

  源码:https://gitee.com/alxps/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
}

 

总结

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

  

标签:code,return,nil,err,添加,link,Go,链接
From: https://www.cnblogs.com/ALXPS/p/18094095

相关文章

  • 使用Go语言开发一个短链接服务:六、链接跳转
    章节 使用Go语言开发一个短链接服务:一、基本原理 使用Go语言开发一个短链接服务:二、架构设计 使用Go语言开发一个短链接服务:三、项目目录结构设计 使用Go语言开发一个短链接服务:四、生成code算法 使用Go语言开发一个短链接服务:五、添加和获取短链接 使用Go语言开......
  • 使用Go语言开发一个短链接服务:三、项目目录结构设计
    章节 使用Go语言开发一个短链接服务:一、基本原理 使用Go语言开发一个短链接服务:二、架构设计 使用Go语言开发一个短链接服务:三、项目目录结构设计 使用Go语言开发一个短链接服务:四、生成code算法 使用Go语言开发一个短链接服务:五、添加和获取短链接 使用Go语言开......
  • golang模板库之fasttemplate
    简介fasttemplate是一个比较简单、易用的小型模板库。fasttemplate的作者valyala另外还开源了不少优秀的库,如大名鼎鼎的fasthttp,前面介绍的bytebufferpool,还有一个重量级的模板库quicktemplate。quicktemplate比标准库中的text/template和html/template要灵活和易用很多,后面会专......
  • Go的可变参数函数
    可变函数是指可以接收可变数量的参数的函数。在Golang中,可以传递与函数签名中引用的类型相同的不同数量的参数。在声明可变函数时,最后一个参数的类型前会有一个省略号"...",这表明该函数可以用任意数量的该类型参数来调用,可以是0个、1个或者多个。这种类型的函数在不知道传递给......
  • Django之权限管理
    一,引入1.为什么要有权限?2.为什么要开发权限的组件?3.在web开发中,什么是权限?4.表结构的设计权限表IDURL1/user_list/2/customer_list/用户表IDUSER_NAME1root2root2角色/用户组表ID组1销售2开发用户与角色的......
  • CPPB 表中的TXN_CATEGORY
    cst_pac_period_balanceTXN_CATEGORY1    期初    2    成本更新:新成本或百分比变动    3    自有成本事务处理    4    非返工完成    5    成本更新:值变动    6    返工发放    7    返工完成    8    组......
  • google搜索如何修改搜索结果地区
    本文写于2024年03月25日,阅读时请注意时效。解决使用谷歌搜索引擎的时候经常会因为IP地址位置导致搜索结果的语言出现偏差,尤其是使用汉语进行搜索时常常会导致搜索结果中出现很多日语等语言结果造成干扰的问题。设置位置在:首页→右下角Settings→Searchsettings→左上角......
  • c语言中的goto语句,goto语句的使用
    在c语言中,goto语句与分支语句if,switch不同,也和循环语句while,for,do...while不同,goto语句被称为无条件转移语句,也被称为转向语句,其实和break,return语句是同一个类型。goto语句的使用一般都需要一个again进行配合,当使用goto语句时,程序会转跳回again处重新运行again后的程序。got......
  • 获取Book里所有sheet的名字,且带上超链接
     应用背景:        当一个excel有很多sheet的时候,来回切换sheet会比较复杂,所以我希望excel的第一页有目录,可以随着sheet的增加,减少,改名而随时可以去更新,还希望有超链接可以直接跳到该sheet。可以采用以下代码OptionExplicitSubList_Hyperlink()Dimk!,shk=0......
  • 【YOLOv5改进系列(4)】高效涨点----添加可变形卷积DCNv2
    可变形卷积......