章节

使用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. Codeforces Round #887 (Div. 2) A-D

    比赛链接 A 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; int a[507]; bool ...

  2. 【framework】ConfigurationContainer简介

    1 前言 ​ 如图所示,ConfigurationContainer 是 AMS 家族的重要基类:WindowContainer 继承自 ConfigurationContainer,是 WMS 家族 ...

  3. Swoole从入门到入土(24)——多进程[进程管理器Process\Manager]

    Swoole提供的进程管理器Process\Manage,基于 Process\Pool 实现.可以管理多个进程.相比与 Process\Pool,可以非常方便的创建多个执行不同任务的进程,并且可以控 ...

  4. spring boot+sqlite+mybatis实现增删改查例子

    主要是更换了下sqlite的数据源而已,其他代码不变. 我都贴一下吧,这个算是比较通用的基础增删改查代码. 1.创建test.db 可以使用Idea自带的Database插件配置,也可以命令行创建,具 ...

  5. Eclipse搭建Struts2项目

    最近在系统性的学习maven,碰到搭建struts2环境,特此记录一下. 1.创建maven工程 2.添加依赖 pom.xml文件内容如下 <project xmlns="http:/ ...

  6. 从 vs 的 rc 文件中获取版本号

    更新项目版本号时,需要与 rc 文件的 version 同步,比较方便的方法是直接从 rc 文件中获取版本号,并应用到程序中 // 删除日志检查 bool GetVersion() { // get ...

  7. 封装一些常用的 qt 控件

    在 qt 中需要做 toast 效果和图片 tip 效果,故开发了下面一个类 后续会继续添加一些常用的控件 tool_tip.h #include <qlabel.h> #include ...

  8. win32 - QueryDisplayConfig的使用

    QueryDisplayConfig函数检索关于所有显示设备的所有可能的显示路径,或视图,在当前设置的信息. C++样本: (开箱即用) 代码列出了所有显示器的名称和拓展模式 #include < ...

  9. 【Android 逆向】【攻防世界】APK逆向

    1. apk安装到手机,提示输入flag 2. jadx打开apk 定位到checkSN方法 public boolean checkSN(String userName, String sn) { ...

  10. python运算符---day04

    1.python运算符 (1)算数运算符: + - * / // % ** (2)比较运算符: > < >= <= == != (3)赋值运算符:= += -= *= /= / ...