使用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语言开发一个短链接服务:五、添加和获取短链接的更多相关文章
- C#开发BIMFACE系列10 服务端API之获取文件下载链接
系列目录 [已更新最新开发文章,点击查看详细] 通过BIMFACE控制台或者调用服务接口上传文件成功后,默认场景下需要下载该源文件,下载文件一般需要知道文件的下载链接即可.BIMACE平台提供 ...
- C#开发BIMFACE系列15 服务端API之获取模型的View token
系列目录 [已更新最新开发文章,点击查看详细] 在<C#开发BIMFACE系列3 服务端API之获取应用访问凭证AccessToken>中详细介绍了应用程序访问API的令牌凭证.我 ...
- C#开发BIMFACE系列3 服务端API之获取应用访问凭证AccessToken
系列目录 [已更新最新开发文章,点击查看详细] BIMFACE 平台为开发者提供了大量的服务器端 API 与 JavaScript API,用于二次开发 BIM 的相关应用. BIMFACE ...
- C#开发BIMFACE系列7 服务端API之获取文件信息列表
系列目录 [已更新最新开发文章,点击查看详细] 本文详细介绍如何获取BIMFACE平台中所有上传过的文件信息列表. 请求地址:GET https://file.bimface.com/file ...
- C#开发BIMFACE系列8 服务端API之获取文件上传状态信息
系列目录 [已更新最新开发文章,点击查看详细] 在BIMFACE控制台上传文件,上传过程及结束后它会自动告诉你文件的上传状态,目前有三种状态:uploading,success,failure ...
- C#开发BIMFACE系列9 服务端API之获取应用支持的文件类型
系列目录 [已更新最新开发文章,点击查看详细] BIMFACE最核心能力之一是工程文件格式转换.无需安装插件,支持数十种工程文件格式在云端转换,完整保留原始文件信息.开发者将告别原始文件解析烦 ...
- C#开发BIMFACE系列19 服务端API之获取模型数据4:获取多个构件的共同属性
系列目录 [已更新最新开发文章,点击查看详细] 在前几篇博客中介绍了一个三维文件/模型包含多个构建,每个构建又是由多种材质组成,每个构建都有很多属性.不同的构建也有可能包含相同的属性. 上图中 ...
- C#开发BIMFACE系列21 服务端API之获取模型数据6:获取单模型的楼层信息
系列目录 [已更新最新开发文章,点击查看详细] 一个文件/模型中可能包含多个楼层信息,获取楼层信息对于前端页面的动态展示非常有帮助.本篇介绍获取一个文件/模型中可能包含多个楼层信息的详细方法. ...
- C#开发BIMFACE系列24 服务端API之获取模型数据9:获取单个房间信息
系列目录 [已更新最新开发文章,点击查看详细] 大厦建筑模型中,基本上包含多个楼层,每个楼层包含多个房间等信息.在<C#开发BIMFACE系列21 服务端API之获取模型数据6:获取单模 ...
- C#开发BIMFACE系列25 服务端API之获取模型数据10:获取楼层对应面积分区列表
系列目录 [已更新最新开发文章,点击查看详细] 在<C#开发BIMFACE系列22 服务端API之获取模型数据7:获取多个模型的楼层信息>中,返回的楼层信息结果中包含了楼层的具体信 ...
随机推荐
- IIS配置跨域
在IIS里找到HTTP响应标头 添加如下两个标头 Access-Control-Allow-Headers:Content-Type, api_key, Authorization Access-Co ...
- NVME(学习杂谈)—Asynchronous Event
Asynchronous Event Request Host Software Recommendations 当一个异步事件请求完成(提供Event Type,Event Information, ...
- 【Unity3D】刚体组件Rigidbody
1 前言 刚体(Rigidbody)是运动学(Kinematic)中的一个概念,指在运动中和受力作用后,形状和大小不变,而且内部各点的相对位置不变的物体.在 Unity3D 中,刚体组件赋予了游戏 ...
- SpringBoot+Shiro+LayUI权限管理系统项目-4.实现部门管理
1.说明 只讲解关键部分,详细看源码,文章下方捐赠或QQ联系捐赠获取. 2.功能展示 3.业务模型 @Data @EqualsAndHashCode(callSuper = false) @Acces ...
- Innodb 存储引擎表
目录 索引组织表 Innodb逻辑存储结构 表空间 段 区 页 行 Innodb 行记录格式 Compact Redundant 行溢出数据 Compressed 和 Dynamic 行记录格式 ch ...
- Docker实践之07-数据管理
目录 一.数据卷概述 二.创建数据卷 三.查看数据卷 四.挂载数据卷 五.删除数据卷 六.挂载主机目录或文件 七.挂载数据卷与主机目录/文件的比较 一.数据卷概述 数据卷是一个可供一个或多个容器使用的 ...
- OSG开发笔记(二十九):OSG加载模型文件、加载3DMax三维型文件Demo
前言 Osg深入之后需要打开模型文件,这些模型文件是已有的模型文件,加载入osg之后可以在常见中展示模型文件,该节点可以操作,多个逼真的模型的节点就实现了基本的场景构建. Demo ...
- 【LeetCode二叉树#14】验证二叉搜索树(巩固迭代中序遍历#1)
验证二叉搜索树 力扣题目链接(opens new window) 给定一个二叉树,判断其是否是一个有效的二叉搜索树. 假设一个二叉搜索树具有如下特征: 节点的左子树只包含小于当前节点的数. 节点的右子 ...
- 16. Class字节码结构
1. 相关概念 1.1字节码文件的跨平台性 Java 语言是跨平台的(write once, run anywhere) 当 Java 源代码成功编译成字节码后,如果想在不同的平台上面运行, 则无须再 ...
- Java 封装性的四种权限测试 + 总结
* 总结封装性:Java提供了4中权限修饰符来修饰类及类的内部结构,体现类及类的内部结构再被调用时的可见性的大小 1 package com.bytezero.circle; 2 3 publi ...