章节

使用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. IIS配置跨域

    在IIS里找到HTTP响应标头 添加如下两个标头 Access-Control-Allow-Headers:Content-Type, api_key, Authorization Access-Co ...

  2. NVME(学习杂谈)—Asynchronous Event

    Asynchronous Event Request Host Software Recommendations 当一个异步事件请求完成(提供Event Type,Event Information, ...

  3. 【Unity3D】刚体组件Rigidbody

    1 前言 ​ 刚体(Rigidbody)是运动学(Kinematic)中的一个概念,指在运动中和受力作用后,形状和大小不变,而且内部各点的相对位置不变的物体.在 Unity3D 中,刚体组件赋予了游戏 ...

  4. SpringBoot+Shiro+LayUI权限管理系统项目-4.实现部门管理

    1.说明 只讲解关键部分,详细看源码,文章下方捐赠或QQ联系捐赠获取. 2.功能展示 3.业务模型 @Data @EqualsAndHashCode(callSuper = false) @Acces ...

  5. Innodb 存储引擎表

    目录 索引组织表 Innodb逻辑存储结构 表空间 段 区 页 行 Innodb 行记录格式 Compact Redundant 行溢出数据 Compressed 和 Dynamic 行记录格式 ch ...

  6. Docker实践之07-数据管理

    目录 一.数据卷概述 二.创建数据卷 三.查看数据卷 四.挂载数据卷 五.删除数据卷 六.挂载主机目录或文件 七.挂载数据卷与主机目录/文件的比较 一.数据卷概述 数据卷是一个可供一个或多个容器使用的 ...

  7. OSG开发笔记(二十九):OSG加载模型文件、加载3DMax三维型文件Demo

    前言   Osg深入之后需要打开模型文件,这些模型文件是已有的模型文件,加载入osg之后可以在常见中展示模型文件,该节点可以操作,多个逼真的模型的节点就实现了基本的场景构建.   Demo      ...

  8. 【LeetCode二叉树#14】验证二叉搜索树(巩固迭代中序遍历#1)

    验证二叉搜索树 力扣题目链接(opens new window) 给定一个二叉树,判断其是否是一个有效的二叉搜索树. 假设一个二叉搜索树具有如下特征: 节点的左子树只包含小于当前节点的数. 节点的右子 ...

  9. 16. Class字节码结构

    1. 相关概念 1.1字节码文件的跨平台性 Java 语言是跨平台的(write once, run anywhere) 当 Java 源代码成功编译成字节码后,如果想在不同的平台上面运行, 则无须再 ...

  10. Java 封装性的四种权限测试 + 总结

    *    总结封装性:Java提供了4中权限修饰符来修饰类及类的内部结构,体现类及类的内部结构再被调用时的可见性的大小 1 package com.bytezero.circle; 2 3 publi ...