章节

使用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
}

总结

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

  

使用Go语言开发一个短链接服务:五、添加和获取短链接的更多相关文章

  1. C#开发BIMFACE系列10 服务端API之获取文件下载链接

    系列目录     [已更新最新开发文章,点击查看详细] 通过BIMFACE控制台或者调用服务接口上传文件成功后,默认场景下需要下载该源文件,下载文件一般需要知道文件的下载链接即可.BIMACE平台提供 ...

  2. C#开发BIMFACE系列15 服务端API之获取模型的View token

    系列目录     [已更新最新开发文章,点击查看详细] 在<C#开发BIMFACE系列3 服务端API之获取应用访问凭证AccessToken>中详细介绍了应用程序访问API的令牌凭证.我 ...

  3. C#开发BIMFACE系列3 服务端API之获取应用访问凭证AccessToken

    系列目录     [已更新最新开发文章,点击查看详细] BIMFACE 平台为开发者提供了大量的服务器端 API 与 JavaScript API,用于二次开发 BIM 的相关应用. BIMFACE ...

  4. C#开发BIMFACE系列7 服务端API之获取文件信息列表

    系列目录     [已更新最新开发文章,点击查看详细] 本文详细介绍如何获取BIMFACE平台中所有上传过的文件信息列表. 请求地址:GET https://file.bimface.com/file ...

  5. C#开发BIMFACE系列8 服务端API之获取文件上传状态信息

    系列目录     [已更新最新开发文章,点击查看详细] 在BIMFACE控制台上传文件,上传过程及结束后它会自动告诉你文件的上传状态,目前有三种状态:uploading,success,failure ...

  6. C#开发BIMFACE系列9 服务端API之获取应用支持的文件类型

    系列目录     [已更新最新开发文章,点击查看详细] BIMFACE最核心能力之一是工程文件格式转换.无需安装插件,支持数十种工程文件格式在云端转换,完整保留原始文件信息.开发者将告别原始文件解析烦 ...

  7. C#开发BIMFACE系列19 服务端API之获取模型数据4:获取多个构件的共同属性

    系列目录     [已更新最新开发文章,点击查看详细] 在前几篇博客中介绍了一个三维文件/模型包含多个构建,每个构建又是由多种材质组成,每个构建都有很多属性.不同的构建也有可能包含相同的属性. 上图中 ...

  8. C#开发BIMFACE系列21 服务端API之获取模型数据6:获取单模型的楼层信息

    系列目录     [已更新最新开发文章,点击查看详细] 一个文件/模型中可能包含多个楼层信息,获取楼层信息对于前端页面的动态展示非常有帮助.本篇介绍获取一个文件/模型中可能包含多个楼层信息的详细方法. ...

  9. C#开发BIMFACE系列24 服务端API之获取模型数据9:获取单个房间信息

    系列目录     [已更新最新开发文章,点击查看详细] 大厦建筑模型中,基本上包含多个楼层,每个楼层包含多个房间等信息.在<C#开发BIMFACE系列21 服务端API之获取模型数据6:获取单模 ...

  10. C#开发BIMFACE系列25 服务端API之获取模型数据10:获取楼层对应面积分区列表

    系列目录     [已更新最新开发文章,点击查看详细] 在<C#开发BIMFACE系列22 服务端API之获取模型数据7:获取多个模型的楼层信息>中,返回的楼层信息结果中包含了楼层的具体信 ...

随机推荐

  1. 【分布式】load balance 04-java 从零手写实现负载均衡

    负载均衡系列专题 01-负载均衡基础知识 02-一致性 hash 原理 03-一致性哈希算法 java 实现 04-负载均衡算法 java 实现 本节我们来看一下如何实现一负载均衡框架. 源码 核心接 ...

  2. nginx配置反向代理缓存

    说明 最近运维一个网站里面含有不经常变化的小图片,而每次请求都需要调用file接口获取不太合适.所以就想利用nginx的反向代理缓存来减轻服务接口的请求压力. 工作原理 Nginx反向代理缓存,当客户 ...

  3. 解决maven打包compliation failure程序包不存在

    1.问题说明 spring boot项目,在cmd中使用mvn clean package打包报错如下: 说这个程序包不存在,而实际上在eclipse中查看是能找到的. 2.问题原因 后来看了一下这个 ...

  4. win32 - GetMenuBarInfo的使用

    MSDN文档介绍GetMenuBarInfo是用来检索有关指定菜单栏的信息. 假如有个需求是要找到菜单下拉菜单的矩形大小,该怎么做呢? 最简单的方法就是获取菜单栏的句柄,然后将句柄作为参数传给GetM ...

  5. win32-使用EnumWindows比较两个窗口的Z轴

    通过使用EnumWindows()和枚举窗口来手动确定EnumChildWindows()来直接确定哪个窗口在z轴上比另一个窗口高. struct myEnumInfo { HWND hwnd1; H ...

  6. 从图纸到BIM到数字孪生城市(元宇宙),易如反掌!

    当智能建模平台与虚幻引擎相遇时,它们又能碰撞出怎样的火花呢? 智能建模怎么玩? 以南昌某职业学院项目为例,这个项目总共有16栋楼,我们直接用智能建模平台"bim.zonst.com" ...

  7. maven引入本地jar不能打入部署包的问题解决

    引入的三方依赖 jar 包, scope 为 system 的包 maven 默认是不打包进去的,需要加这个配置 在pom.xml文件中找到spring-boot-maven-plugin插件,添加如 ...

  8. 详细的BoltDB学习记录文档

    最近项目中用到了boltdb这个go开发的key/value 数据库,但是之前并有接触过,所以特意去看了官方,也找了些资料,网上找的资料要不就是官方文档的翻译,要不就是简单的介绍一点,都不是很全,所以 ...

  9. 用linux命令cd 查找想要找的文件

    如果想找文件Computer下的bin文件,在终端输入绝对路径 cd /bin,不能输入 cd /Computer/bin,因为文件目录不对 文件目录可以在文件的终端看到,/bin就是正确的目录 比如 ...

  10. Java 小练习(3) 方法的修改+ 调用

    1 package com.bytezero.exer; 2 3 public class ExerTest 4 { 5 public static void main(String[] args) ...