Merge branch 'dev'
This commit is contained in:
commit
392569bab4
@ -1,3 +1,3 @@
|
|||||||
MYURLS_PORT=8002
|
MYURLS_PORT=8080
|
||||||
MYURLS_DOMAIN=example.com
|
MYURLS_DOMAIN=example.com
|
||||||
MYURLS_TTL=180
|
MYURLS_PROTO=https
|
||||||
|
|||||||
3
.github/workflows/go.yml
vendored
3
.github/workflows/go.yml
vendored
@ -2,7 +2,8 @@ name: Github CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches:
|
||||||
|
- '*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux_amd64_build:
|
linux_amd64_build:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
|||||||
build/
|
build/
|
||||||
logs/
|
logs/
|
||||||
data/
|
data/
|
||||||
|
*.log
|
||||||
|
|
||||||
.env
|
.env
|
||||||
dist/
|
dist/
|
||||||
|
MyUrls
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@ -1,10 +1,10 @@
|
|||||||
FROM golang:1.21-alpine AS build
|
FROM golang:1.21-alpine AS build
|
||||||
RUN apk update && apk add upx
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY main.go go.mod go.sum .
|
COPY . .
|
||||||
RUN go mod tidy
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o myurls main.go \
|
# RUN go env -w GOPROXY=https://mirrors.cloud.tencent.com/go/,direct
|
||||||
&& upx myurls
|
RUN go mod download
|
||||||
|
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o myurls
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
21
Makefile
21
Makefile
@ -5,41 +5,40 @@ BINARY_DARWIN_ARM64="build/myurls-darwin-arm64"
|
|||||||
BINARY_WINDOWS="build/myurls-windows-x64"
|
BINARY_WINDOWS="build/myurls-windows-x64"
|
||||||
BINARY_ARM64="build/myurls-linux-arm64"
|
BINARY_ARM64="build/myurls-linux-arm64"
|
||||||
|
|
||||||
GOFILES="main.go"
|
|
||||||
VERSION=1.0.0
|
VERSION=1.0.0
|
||||||
BUILD=`date +%FT%T%z`
|
BUILD=`date +%FT%T%z`
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@echo ${BINARY_DEFAULT}
|
@echo ${BINARY_DEFAULT}
|
||||||
@CGO_ENABLED=0 go build -ldflags="-s -w" -o ${BINARY_DEFAULT} ${GOFILES}
|
@CGO_ENABLED=0 go build -ldflags="-s -w" -o ${BINARY_DEFAULT}
|
||||||
|
|
||||||
all:
|
all:
|
||||||
@echo ${BINARY_LINUX}
|
@echo ${BINARY_LINUX}
|
||||||
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_LINUX} ${GOFILES}
|
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_LINUX}
|
||||||
# @echo ${BINARY_DARWIN}
|
# @echo ${BINARY_DARWIN}
|
||||||
# @CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_DARWIN} ${GOFILES}
|
# @CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_DARWIN}
|
||||||
# @echo ${BINARY_DARWIN_ARM64}
|
# @echo ${BINARY_DARWIN_ARM64}
|
||||||
# @CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_DARWIN_ARM64} ${GOFILES}
|
# @CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_DARWIN_ARM64}
|
||||||
@echo ${BINARY_WINDOWS}
|
@echo ${BINARY_WINDOWS}
|
||||||
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_WINDOWS} ${GOFILES}
|
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_WINDOWS}
|
||||||
@echo ${BINARY_ARM64}
|
@echo ${BINARY_ARM64}
|
||||||
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_ARM64} ${GOFILES}
|
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_ARM64}
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
@echo ${BINARY_LINUX}
|
@echo ${BINARY_LINUX}
|
||||||
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_LINUX} ${GOFILES}
|
@CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_LINUX}
|
||||||
|
|
||||||
darwin:
|
darwin:
|
||||||
@echo ${BINARY_DARWIN}
|
@echo ${BINARY_DARWIN}
|
||||||
@CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_DARWIN} ${GOFILES}
|
@CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_DARWIN}
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
@echo ${BINARY_WINDOWS}
|
@echo ${BINARY_WINDOWS}
|
||||||
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_WINDOWS} ${GOFILES}
|
@CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o ${BINARY_WINDOWS}
|
||||||
|
|
||||||
aarch64:
|
aarch64:
|
||||||
@echo ${BINARY_ARM64}
|
@echo ${BINARY_ARM64}
|
||||||
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_ARM64} ${GOFILES}
|
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o ${BINARY_ARM64}
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@go mod tidy
|
@go mod tidy
|
||||||
|
|||||||
46
README.md
46
README.md
@ -6,10 +6,6 @@
|
|||||||
|
|
||||||
- [Dependencies](#dependencies)
|
- [Dependencies](#dependencies)
|
||||||
- [Docker](#docker)
|
- [Docker](#docker)
|
||||||
- [Deploy Online](#deploy-online)
|
|
||||||
- [Deploy on Railway](#deploy-on-railway)
|
|
||||||
- [部署](#部署)
|
|
||||||
- [添加域名](#添加域名)
|
|
||||||
- [Install](#install)
|
- [Install](#install)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [日志清理](#日志清理)
|
- [日志清理](#日志清理)
|
||||||
@ -35,7 +31,7 @@ sudo apt-get install redis-server -y
|
|||||||
现在你可以无需安装其他服务,使用 docker 或 [docker-compose](https://docs.docker.com/compose/install/) 部署本项目。注:请自行修改 .env 中参数。
|
现在你可以无需安装其他服务,使用 docker 或 [docker-compose](https://docs.docker.com/compose/install/) 部署本项目。注:请自行修改 .env 中参数。
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -d --restart always --name myurls careywong/myurls:latest -domain example.com -port 8002 -conn 127.0.0.1:6379 -passwd '' -ttl 90
|
docker run -d --restart always --name myurls careywong/myurls:latest -domain example.com -port 8002 -conn 127.0.0.1:6379 -password ''
|
||||||
```
|
```
|
||||||
|
|
||||||
```shell script
|
```shell script
|
||||||
@ -46,25 +42,6 @@ cp .env.example .env
|
|||||||
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
## Deploy Online
|
|
||||||
|
|
||||||
### Deploy on Railway
|
|
||||||
|
|
||||||
#### 部署
|
|
||||||
|
|
||||||
[](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fpzcn%2FMyurls-Railway&plugins=redis&envs=ENV_DOMAIN%2CENV_TTL%2CPORT&ENV_DOMAINDesc=Your+domain.&ENV_TTLDesc=Short+link+validity+period+%28day%29&PORTDesc=DO+NOT+CHANGE&ENV_TTLDefault=180&PORTDefault=80)
|
|
||||||
|
|
||||||
通过上方链接一键部署到Railway,并填入以下参数
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
|
|
||||||
- `DOMAIN` - 短链接域名,必填项,不需要添加https:// (如 abc.com)
|
|
||||||
- `TTL` - 短链接有效期,单位(天),默认180天 (default 180)
|
|
||||||
- `PORT` - 端口,保持80,请勿修改
|
|
||||||
|
|
||||||
#### 添加域名
|
|
||||||
|
|
||||||
在Cloudflare中添加域名,并配置SSL/TLS为完全及以上,并在Railway中接入该域名,参考[官方文档](https://docs.railway.app/deploy/exposing-your-app#lets-encrypt-ssl-certificates)。
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@ -77,7 +54,7 @@ make install
|
|||||||
生成可执行文件,目录位于 build/ 。默认当前平台,其他平台请参照 Makefile 或执行对应 go build 命令。
|
生成可执行文件,目录位于 build/ 。默认当前平台,其他平台请参照 Makefile 或执行对应 go build 命令。
|
||||||
|
|
||||||
```shell script
|
```shell script
|
||||||
bash release.sh
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@ -85,17 +62,18 @@ bash release.sh
|
|||||||
前往 [Actions](https://github.com/CareyWang/MyUrls/actions/workflows/go.yml) 下载对应平台可执行文件。
|
前往 [Actions](https://github.com/CareyWang/MyUrls/actions/workflows/go.yml) 下载对应平台可执行文件。
|
||||||
|
|
||||||
```shell script
|
```shell script
|
||||||
Usage:
|
Usage of ./MyUrls:
|
||||||
-conn string
|
-conn string
|
||||||
Redis连接,格式: host:port (default "127.0.0.1:6379")
|
address of the redis server (default "localhost:6379")
|
||||||
-domain string
|
-domain string
|
||||||
短链接域名,必填项
|
domain of the server (default "localhost:8080")
|
||||||
-passwd string
|
-h display help
|
||||||
Redis连接密码
|
-password string
|
||||||
-port int
|
password of the redis server
|
||||||
服务端口 (default 8002)
|
-port string
|
||||||
-ttl int
|
port to run the server on (default "8080")
|
||||||
短链接有效期,单位(天),默认180天。 (default 180)
|
-proto string
|
||||||
|
protocol of the server (default "https")
|
||||||
```
|
```
|
||||||
|
|
||||||
建议配合 [pm2](https://pm2.keymetrics.io/) 开启守护进程。
|
建议配合 [pm2](https://pm2.keymetrics.io/) 开启守护进程。
|
||||||
|
|||||||
13
const.go
Normal file
13
const.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"Code"`
|
||||||
|
Msg string `json:"Message"`
|
||||||
|
Data any `json:"Data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response codes
|
||||||
|
const ResponseCodeSuccess = 0 // Success
|
||||||
|
const ResponseCodeSuccessLegacy = 1 // Success
|
||||||
|
const ResponseCodeParamsCheckError = 1001 // Parameter check error
|
||||||
|
const ResponseCodeServerError = 1002 // Server error
|
||||||
@ -6,15 +6,15 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
env_file: .env
|
env_file: .env
|
||||||
ports:
|
ports:
|
||||||
- "${MYURLS_PORT}:8002"
|
- "${MYURLS_PORT}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/myurls/logs:/app/logs
|
- ./data/myurls/logs:/app/logs
|
||||||
depends_on:
|
depends_on:
|
||||||
- myurls-redis
|
- myurls-redis
|
||||||
entrypoint: ["/app/myurls", "-domain", "${MYURLS_DOMAIN}", "-conn", myurls-redis:6379, "-ttl", "${MYURLS_TTL}"]
|
entrypoint: ["/app/myurls", "-domain", "${MYURLS_DOMAIN}", "-conn", myurls-redis:6379]
|
||||||
|
|
||||||
myurls-redis:
|
myurls-redis:
|
||||||
image: "redis:6"
|
image: "redis:7"
|
||||||
container_name: myurls-redis
|
container_name: myurls-redis
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
11
go.mod
11
go.mod
@ -1,17 +1,22 @@
|
|||||||
module github.com/CareyWang/MyUrls
|
module github.com/CareyWang/MyUrls
|
||||||
|
|
||||||
go 1.20
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/gomodule/redigo v1.8.9
|
github.com/redis/go-redis/v9 v9.4.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
go.uber.org/zap v1.26.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.10.2 // indirect
|
github.com/bytedance/sonic v1.10.2 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
@ -26,8 +31,10 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.7.0 // indirect
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
golang.org/x/crypto v0.18.0 // indirect
|
golang.org/x/crypto v0.18.0 // indirect
|
||||||
golang.org/x/net v0.20.0 // indirect
|
golang.org/x/net v0.20.0 // indirect
|
||||||
|
|||||||
21
go.sum
21
go.sum
@ -1,7 +1,13 @@
|
|||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||||
@ -13,6 +19,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
@ -20,6 +28,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
|||||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
@ -28,9 +37,8 @@ github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ
|
|||||||
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
|
|
||||||
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
|
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
@ -59,6 +67,8 @@ github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOS
|
|||||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||||
|
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
@ -77,6 +87,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
@ -92,6 +108,7 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
112
handlers.go
Normal file
112
handlers.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTTL = time.Hour * 24 * 365 // 默认过期时间,1年
|
||||||
|
const defaultRenewTime = time.Hour * 48 // 默认续命时间,2天
|
||||||
|
const defaultShortKeyLength = 7 // 默认短链接长度,7位
|
||||||
|
|
||||||
|
// ShortToLongHandler gets the long URL from a short URL
|
||||||
|
func ShortToLongHandler() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
resp := Response{}
|
||||||
|
shortKey := c.Param("shortKey")
|
||||||
|
longURL := ShortToLong(c, shortKey)
|
||||||
|
if longURL == "" {
|
||||||
|
resp.Code = ResponseCodeServerError
|
||||||
|
resp.Msg = "failed to get long URL, please check the short URL if exists or expired"
|
||||||
|
|
||||||
|
c.JSON(404, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo
|
||||||
|
// check whether need renew expiration time
|
||||||
|
// only renew once per day
|
||||||
|
// if err := Renew(c, shortKey, defaultRenewTime); err != nil {
|
||||||
|
// logger.Warn("failed to renew short URL: ", err.Error())
|
||||||
|
// }
|
||||||
|
|
||||||
|
c.Redirect(301, longURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LongToShortParams struct {
|
||||||
|
LongUrl string `form:"longUrl" binding:"required"`
|
||||||
|
ShortKey string `form:"shortKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongToShortHandler creates a short URL from a long URL
|
||||||
|
func LongToShortHandler() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
resp := Response{}
|
||||||
|
|
||||||
|
// check parameters
|
||||||
|
req := LongToShortParams{}
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
resp.Code = ResponseCodeParamsCheckError
|
||||||
|
resp.Msg = "invalid parameters"
|
||||||
|
logger.Warn("invalid parameters: ", err.Error())
|
||||||
|
|
||||||
|
c.JSON(200, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容以前的实现,这里如果是 base64 编码的字符串,进行解码
|
||||||
|
_longUrl, err := base64.StdEncoding.DecodeString(req.LongUrl)
|
||||||
|
if err == nil {
|
||||||
|
req.LongUrl = string(_longUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate short key
|
||||||
|
if req.ShortKey == "" {
|
||||||
|
req.ShortKey = GenerateRandomString(defaultShortKeyLength)
|
||||||
|
}
|
||||||
|
// check whether short key exists
|
||||||
|
exists, err := CheckRedisKeyIfExist(c, req.ShortKey)
|
||||||
|
if err != nil {
|
||||||
|
resp.Code = ResponseCodeServerError
|
||||||
|
resp.Msg = "failed to check short key"
|
||||||
|
logger.Error("failed to check short key: ", err.Error())
|
||||||
|
|
||||||
|
c.JSON(200, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
resp.Code = ResponseCodeParamsCheckError
|
||||||
|
resp.Msg = "short key already exists, please use another one or leave it empty to generate automatically"
|
||||||
|
|
||||||
|
logger.Info("short key already exists: ", req.ShortKey)
|
||||||
|
c.JSON(200, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
options := &LongToShortOptions{
|
||||||
|
ShortKey: req.ShortKey,
|
||||||
|
URL: req.LongUrl,
|
||||||
|
expiration: defaultTTL,
|
||||||
|
}
|
||||||
|
if err := LongToShort(c, options); err != nil {
|
||||||
|
resp.Code = ResponseCodeServerError
|
||||||
|
resp.Msg = "failed to create short URL"
|
||||||
|
logger.Warn("failed to create short URL: ", err.Error())
|
||||||
|
|
||||||
|
c.JSON(200, resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shortURL := proto + "://" + domain + "/" + options.ShortKey
|
||||||
|
|
||||||
|
// 兼容以前的返回结构体
|
||||||
|
respDataLegacy := gin.H{
|
||||||
|
"Code": ResponseCodeSuccessLegacy,
|
||||||
|
"ShortUrl": shortURL,
|
||||||
|
}
|
||||||
|
c.JSON(200, respDataLegacy)
|
||||||
|
}
|
||||||
|
}
|
||||||
142
logger.go
Normal file
142
logger.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger *zap.SugaredLogger
|
||||||
|
|
||||||
|
func InitLogger() {
|
||||||
|
// 创建 logs 目录
|
||||||
|
if dir, err := os.Getwd(); err == nil {
|
||||||
|
logFilePath := dir + "/logs/"
|
||||||
|
if err := os.MkdirAll(logFilePath, 0777); err != nil {
|
||||||
|
panic("create log dir failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 zap logger
|
||||||
|
initZapLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 gin logger
|
||||||
|
func initLoggerForGin() *logrus.Logger {
|
||||||
|
logFilePath := ""
|
||||||
|
if dir, err := os.Getwd(); err == nil {
|
||||||
|
logFilePath = dir + "/logs/"
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(logFilePath, 0777); err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
logFileName := "access.log"
|
||||||
|
|
||||||
|
// 日志文件
|
||||||
|
fileName := path.Join(logFilePath, logFileName)
|
||||||
|
if _, err := os.Stat(fileName); err != nil {
|
||||||
|
if _, err := os.Create(fileName); err != nil {
|
||||||
|
panic("create log file failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入文件
|
||||||
|
src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实例化
|
||||||
|
_logger := logrus.New()
|
||||||
|
|
||||||
|
// 设置输出
|
||||||
|
_logger.SetOutput(src)
|
||||||
|
// logger.Out = src
|
||||||
|
|
||||||
|
// 设置日志级别
|
||||||
|
_logger.SetLevel(logrus.DebugLevel)
|
||||||
|
|
||||||
|
// 设置日志格式
|
||||||
|
_logger.Formatter = &logrus.JSONFormatter{}
|
||||||
|
|
||||||
|
return _logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// gin 文件日志
|
||||||
|
func LoggerToFile() gin.HandlerFunc {
|
||||||
|
_logger := initLoggerForGin()
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
logMap := make(map[string]any)
|
||||||
|
|
||||||
|
// 开始时间
|
||||||
|
startTime := time.Now()
|
||||||
|
logMap["startTime"] = startTime.Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// 处理请求
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
// 结束时间
|
||||||
|
endTime := time.Now()
|
||||||
|
logMap["endTime"] = endTime.Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// 执行时间
|
||||||
|
logMap["latencyTime"] = endTime.Sub(startTime).Microseconds()
|
||||||
|
|
||||||
|
// 请求方式
|
||||||
|
logMap["reqMethod"] = c.Request.Method
|
||||||
|
|
||||||
|
// 请求路由
|
||||||
|
logMap["reqUri"] = c.Request.RequestURI
|
||||||
|
|
||||||
|
// 状态码
|
||||||
|
logMap["statusCode"] = c.Writer.Status()
|
||||||
|
|
||||||
|
// 请求IP
|
||||||
|
logMap["clientIP"] = c.ClientIP()
|
||||||
|
|
||||||
|
// 请求 UA
|
||||||
|
logMap["clientUA"] = c.Request.UserAgent()
|
||||||
|
|
||||||
|
// 日志格式
|
||||||
|
// logJson, _ := json.Marshal(logMap)
|
||||||
|
// _logger.Info(string(logJson))
|
||||||
|
|
||||||
|
_logger.WithFields(logrus.Fields{
|
||||||
|
"startTime": logMap["startTime"],
|
||||||
|
"endTime": logMap["endTime"],
|
||||||
|
"latencyTime": logMap["latencyTime"],
|
||||||
|
"reqMethod": logMap["reqMethod"],
|
||||||
|
"reqUri": logMap["reqUri"],
|
||||||
|
"statusCode": logMap["statusCode"],
|
||||||
|
"clientIP": logMap["clientIP"],
|
||||||
|
"clientUA": logMap["clientUA"],
|
||||||
|
}).Info()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 zap logger
|
||||||
|
func initZapLogger() {
|
||||||
|
writeSyncer := getLogWriter()
|
||||||
|
encoder := getEncoder()
|
||||||
|
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
|
||||||
|
|
||||||
|
_logger := zap.New(core)
|
||||||
|
defer _logger.Sync()
|
||||||
|
|
||||||
|
logger = _logger.Sugar()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEncoder() zapcore.Encoder {
|
||||||
|
return zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogWriter() zapcore.WriteSyncer {
|
||||||
|
file, _ := os.Create("./logs/runtime.log")
|
||||||
|
return zapcore.AddSync(file)
|
||||||
|
}
|
||||||
52
logic.go
Normal file
52
logic.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShortToLong gets the long URL from a short URL
|
||||||
|
func ShortToLong(ctx context.Context, shortKey string) string {
|
||||||
|
rc := GetRedisClient()
|
||||||
|
return rc.Get(ctx, shortKey).Val()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongToShortOptions are the options for the LongToShort function
|
||||||
|
type LongToShortOptions struct {
|
||||||
|
ShortKey string
|
||||||
|
URL string
|
||||||
|
expiration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongToShort creates a short URL from a long URL
|
||||||
|
func LongToShort(ctx context.Context, options *LongToShortOptions) error {
|
||||||
|
rc := GetRedisClient()
|
||||||
|
return rc.SetEx(ctx, options.ShortKey, options.URL, options.expiration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renew updates the expiration time of a short URL
|
||||||
|
func Renew(ctx context.Context, shortKey string, expiration time.Duration) error {
|
||||||
|
rc := GetRedisClient()
|
||||||
|
|
||||||
|
rs := rc.TTL(ctx, shortKey)
|
||||||
|
if rs.Err() != nil {
|
||||||
|
return rs.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := rs.Val()
|
||||||
|
if ttl < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc.Expire(ctx, shortKey, ttl+expiration).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckRedisKeyIfExist(ctx context.Context, key string) (bool, error) {
|
||||||
|
rc := GetRedisClient()
|
||||||
|
rs := rc.Exists(ctx, key)
|
||||||
|
if rs.Err() != nil {
|
||||||
|
return false, rs.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
return rs.Val() > 0, nil
|
||||||
|
}
|
||||||
31
logic_test.go
Normal file
31
logic_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// FILEPATH: /root/CareyWang/MyUrls/logic_test.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLongToShortAndShortToLong(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
initRedisClient(mockRedisOptions)
|
||||||
|
|
||||||
|
shortKey := "testKey"
|
||||||
|
longURL := "https://example.com"
|
||||||
|
|
||||||
|
err := LongToShort(ctx, &LongToShortOptions{
|
||||||
|
ShortKey: shortKey,
|
||||||
|
URL: longURL,
|
||||||
|
expiration: 60 * time.Second,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// delete test data from redis
|
||||||
|
defer GetRedisClient().Del(ctx, shortKey)
|
||||||
|
|
||||||
|
resultLongURL := ShortToLong(ctx, shortKey)
|
||||||
|
assert.Equal(t, longURL, resultLongURL)
|
||||||
|
}
|
||||||
446
main.go
446
main.go
@ -1,391 +1,111 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gomodule/redigo/redis"
|
"github.com/redis/go-redis/v9"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response is the response structure
|
var helpFlag bool
|
||||||
type Response struct {
|
|
||||||
Code int
|
var (
|
||||||
Message string
|
port = "8080"
|
||||||
LongUrl string
|
domain = "localhost:8080"
|
||||||
ShortUrl string
|
proto = "https"
|
||||||
|
redisAddr = "localhost:6379"
|
||||||
|
redisPassword = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.BoolVar(&helpFlag, "h", false, "display help")
|
||||||
|
|
||||||
|
flag.StringVar(&port, "port", port, "port to run the server on")
|
||||||
|
flag.StringVar(&domain, "domain", domain, "domain of the server")
|
||||||
|
flag.StringVar(&proto, "proto", proto, "protocol of the server")
|
||||||
|
flag.StringVar(&redisAddr, "conn", redisAddr, "address of the redis server")
|
||||||
|
flag.StringVar(&redisPassword, "password", redisPassword, "password of the redis server")
|
||||||
}
|
}
|
||||||
|
|
||||||
// redisPoolConf is the Redis pool configuration.
|
|
||||||
type redisPoolConf struct {
|
|
||||||
maxIdle int
|
|
||||||
maxActive int
|
|
||||||
maxIdleTimeout int
|
|
||||||
host string
|
|
||||||
password string
|
|
||||||
db int
|
|
||||||
handleTimeout int
|
|
||||||
}
|
|
||||||
|
|
||||||
// letterBytes is a string containing all the characters used in the short URL generation.
|
|
||||||
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
|
|
||||||
// shortUrlLen is the length of the generated short URL.
|
|
||||||
const shortUrlLen = 7
|
|
||||||
|
|
||||||
// defaultPort is the default port number.
|
|
||||||
const defaultPort int = 8002
|
|
||||||
|
|
||||||
// defaultExpire is the redis ttl in days for a short URL.
|
|
||||||
const defaultExpire = 180
|
|
||||||
|
|
||||||
// defaultRedisConfig is the default Redis configuration.
|
|
||||||
const defaultRedisConfig = "127.0.0.1:6379"
|
|
||||||
|
|
||||||
// defaultLockPrefix is the default prefix for Redis locks.
|
|
||||||
const defaultLockPrefix = "myurls:lock:"
|
|
||||||
|
|
||||||
// defaultRenewal is the default renewal time for Redis locks.
|
|
||||||
const defaultRenewal = 1
|
|
||||||
|
|
||||||
// secondsPerDay is the number of seconds in a day.
|
|
||||||
const secondsPerDay = 24 * 3600
|
|
||||||
|
|
||||||
// redisPool is a connection pool for Redis.
|
|
||||||
var redisPool *redis.Pool
|
|
||||||
|
|
||||||
// redisPoolConfig is the Redis pool configuration.
|
|
||||||
var redisPoolConfig *redisPoolConf
|
|
||||||
|
|
||||||
// redisClient is a Redis client.
|
|
||||||
var redisClient redis.Conn
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
if helpFlag {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从环境变量中读取配置,且环境变量优先级高于命令行参数
|
||||||
|
parseEnvirons()
|
||||||
|
|
||||||
|
InitLogger()
|
||||||
|
|
||||||
|
// init and check redis
|
||||||
|
initRedisClient(&redis.Options{
|
||||||
|
Addr: redisAddr,
|
||||||
|
Password: redisPassword,
|
||||||
|
DB: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
rc := GetRedisClient()
|
||||||
|
rs := rc.Ping(ctx)
|
||||||
|
if rs.Err() != nil {
|
||||||
|
logger.Fatalln("redis ping failed: ", rs.Err())
|
||||||
|
}
|
||||||
|
logger.Info("redis ping success")
|
||||||
|
|
||||||
|
// GC optimize
|
||||||
|
ballast := make([]byte, 1<<30) // 预分配 1G 内存,不会实际占用物理内存,不可读写该变量
|
||||||
|
defer func() {
|
||||||
|
logger.Info("ballast len %v", len(ballast))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// start http server
|
||||||
|
run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnvirons() {
|
||||||
|
if p := os.Getenv("MYURLS_PORT"); p != "" {
|
||||||
|
port = p
|
||||||
|
}
|
||||||
|
if d := os.Getenv("MYURLS_DOMAIN"); d != "" {
|
||||||
|
domain = d
|
||||||
|
}
|
||||||
|
if p := os.Getenv("MYURLS_PROTO"); p != "" {
|
||||||
|
proto = p
|
||||||
|
}
|
||||||
|
if c := os.Getenv("MYURLS_REDIS_CONN"); c != "" {
|
||||||
|
redisAddr = c
|
||||||
|
}
|
||||||
|
if p := os.Getenv("MYURLS_REDIS_PASSWORD"); p != "" {
|
||||||
|
redisPassword = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
// init and run server
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
|
|
||||||
// Log 收集中间件
|
// logger
|
||||||
router.Use(LoggerToFile())
|
router.Use(LoggerToFile())
|
||||||
|
|
||||||
|
// static files
|
||||||
router.LoadHTMLGlob("public/*.html")
|
router.LoadHTMLGlob("public/*.html")
|
||||||
|
|
||||||
port := flag.Int("port", defaultPort, "服务端口")
|
|
||||||
domain := flag.String("domain", "", "短链接域名,必填项")
|
|
||||||
ttl := flag.Int("ttl", defaultExpire, "短链接有效期,单位(天),默认180天。")
|
|
||||||
conn := flag.String("conn", defaultRedisConfig, "Redis连接,格式: host:port")
|
|
||||||
passwd := flag.String("passwd", "", "Redis连接密码")
|
|
||||||
https := flag.Int("https", 1, "是否返回 https 短链接")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *domain == "" {
|
|
||||||
flag.Usage()
|
|
||||||
log.Fatalln("缺少关键参数")
|
|
||||||
}
|
|
||||||
|
|
||||||
redisPoolConfig = &redisPoolConf{
|
|
||||||
maxIdle: 1024,
|
|
||||||
maxActive: 1024,
|
|
||||||
maxIdleTimeout: 30,
|
|
||||||
host: *conn,
|
|
||||||
password: *passwd,
|
|
||||||
db: 0,
|
|
||||||
handleTimeout: 30,
|
|
||||||
}
|
|
||||||
initRedisPool()
|
|
||||||
|
|
||||||
router.GET("/", func(context *gin.Context) {
|
router.GET("/", func(context *gin.Context) {
|
||||||
context.HTML(http.StatusOK, "index.html", gin.H{
|
context.HTML(http.StatusOK, "index.html", gin.H{
|
||||||
"title": "MyUrls",
|
"title": "MyUrls",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 短链接生成
|
router.POST("/short", LongToShortHandler())
|
||||||
router.POST("/short", func(context *gin.Context) {
|
router.GET("/:shortKey", ShortToLongHandler())
|
||||||
res := &Response{
|
|
||||||
Code: 1,
|
logger.Infof("server running on :%s", port)
|
||||||
Message: "",
|
router.Run(fmt.Sprintf(":%s", port))
|
||||||
LongUrl: "",
|
|
||||||
ShortUrl: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
longUrl := context.PostForm("longUrl")
|
|
||||||
shortKey := context.PostForm("shortKey")
|
|
||||||
if longUrl == "" {
|
|
||||||
res.Code = 0
|
|
||||||
res.Message = "longUrl为空"
|
|
||||||
context.JSON(200, *res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_longUrl, _ := base64.StdEncoding.DecodeString(longUrl)
|
|
||||||
longUrl = string(_longUrl)
|
|
||||||
res.LongUrl = longUrl
|
|
||||||
|
|
||||||
// 根据有没有填写 short key,分别执行
|
|
||||||
if shortKey != "" {
|
|
||||||
redisClient := redisPool.Get()
|
|
||||||
|
|
||||||
// 检测短链是否已存在
|
|
||||||
_exists, _ := redis.String(redisClient.Do("get", shortKey))
|
|
||||||
if _exists != "" && _exists != longUrl {
|
|
||||||
res.Code = 0
|
|
||||||
res.Message = "短链接已存在,请更换key"
|
|
||||||
context.JSON(200, *res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 存储
|
|
||||||
_, _ = redisClient.Do("set", shortKey, longUrl)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
shortKey = longToShort(longUrl, *ttl*secondsPerDay)
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol := "http://"
|
|
||||||
if *https != 0 {
|
|
||||||
protocol = "https://"
|
|
||||||
}
|
|
||||||
res.ShortUrl = protocol + *domain + "/" + shortKey
|
|
||||||
|
|
||||||
// context.Header("Access-Control-Allow-Origin", "*")
|
|
||||||
context.JSON(200, *res)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 短链接跳转
|
|
||||||
router.GET("/:shortKey", func(context *gin.Context) {
|
|
||||||
shortKey := context.Param("shortKey")
|
|
||||||
longUrl := shortToLong(shortKey)
|
|
||||||
|
|
||||||
if longUrl == "" {
|
|
||||||
context.String(http.StatusNotFound, "短链接不存在或已过期")
|
|
||||||
} else {
|
|
||||||
context.Redirect(http.StatusMovedPermanently, longUrl)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// GC 优化
|
|
||||||
ballast := make([]byte, 1<<30) // 分配 1G 内存,不会实际占用物理内存,不可读写该变量
|
|
||||||
defer func() {
|
|
||||||
log.Println("ballast len %v", len(ballast))
|
|
||||||
}()
|
|
||||||
|
|
||||||
router.Run(fmt.Sprintf(":%d", *port))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 短链接转长链接
|
|
||||||
func shortToLong(shortKey string) string {
|
|
||||||
redisClient = redisPool.Get()
|
|
||||||
defer redisClient.Close()
|
|
||||||
|
|
||||||
longUrl, _ := redis.String(redisClient.Do("get", shortKey))
|
|
||||||
|
|
||||||
// 获取到长链接后,续命1天。每天仅允许续命1次。
|
|
||||||
if longUrl != "" {
|
|
||||||
renew(shortKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return longUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
// 长链接转短链接
|
|
||||||
func longToShort(longUrl string, ttl int) string {
|
|
||||||
redisClient = redisPool.Get()
|
|
||||||
defer redisClient.Close()
|
|
||||||
|
|
||||||
// 是否生成过该长链接对应短链接
|
|
||||||
longUrlMD5Bytes := md5.Sum([]byte(longUrl))
|
|
||||||
longUrlMD5 := hex.EncodeToString(longUrlMD5Bytes[:])
|
|
||||||
_existsKey, _ := redis.String(redisClient.Do("get", longUrlMD5))
|
|
||||||
if _existsKey != "" {
|
|
||||||
_, _ = redisClient.Do("expire", _existsKey, ttl)
|
|
||||||
|
|
||||||
log.Println("Hit cache: " + _existsKey)
|
|
||||||
return _existsKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重试三次
|
|
||||||
var shortKey string
|
|
||||||
for i := 0; i < 3; i++ {
|
|
||||||
shortKey = generate(shortUrlLen)
|
|
||||||
|
|
||||||
_existsLongUrl, _ := redis.String(redisClient.Do("get", shortKey))
|
|
||||||
if _existsLongUrl == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shortKey != "" {
|
|
||||||
_, _ = redisClient.Do("mset", shortKey, longUrl, longUrlMD5, shortKey)
|
|
||||||
|
|
||||||
_, _ = redisClient.Do("expire", shortKey, ttl)
|
|
||||||
_, _ = redisClient.Do("expire", longUrlMD5, secondsPerDay)
|
|
||||||
}
|
|
||||||
|
|
||||||
return shortKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// 续命
|
|
||||||
func renew(shortKey string) {
|
|
||||||
redisClient = redisPool.Get()
|
|
||||||
defer redisClient.Close()
|
|
||||||
|
|
||||||
// 加锁
|
|
||||||
lockKey := defaultLockPrefix + shortKey
|
|
||||||
lock, _ := redis.Int(redisClient.Do("setnx", lockKey, 1))
|
|
||||||
if lock == 1 {
|
|
||||||
// 设置锁过期时间
|
|
||||||
_, _ = redisClient.Do("expire", lockKey, defaultRenewal*secondsPerDay)
|
|
||||||
|
|
||||||
// 续命
|
|
||||||
ttl, err := redis.Int(redisClient.Do("ttl", shortKey))
|
|
||||||
if err == nil && ttl != -1 {
|
|
||||||
_, _ = redisClient.Do("expire", shortKey, ttl+defaultRenewal*secondsPerDay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate is a function that takes an integer bits and returns a string.
|
|
||||||
// The function generates a random string of length equal to bits using the letterBytes slice.
|
|
||||||
// The letterBytes slice contains characters that can be used to generate a random string.
|
|
||||||
// The generation of the random string is based on the current time using the UnixNano() function.
|
|
||||||
func generate(bits int) string {
|
|
||||||
// Create a byte slice b of length bits.
|
|
||||||
b := make([]byte, bits)
|
|
||||||
|
|
||||||
// Create a new random number generator with the current time as the seed.
|
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
|
|
||||||
// Generate a random byte for each element in the byte slice b using the letterBytes slice.
|
|
||||||
for i := range b {
|
|
||||||
b[i] = letterBytes[r.Intn(len(letterBytes))]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the byte slice to a string and return it.
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义 logger
|
|
||||||
func Logger() *logrus.Logger {
|
|
||||||
logFilePath := ""
|
|
||||||
if dir, err := os.Getwd(); err == nil {
|
|
||||||
logFilePath = dir + "/logs/"
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(logFilePath, 0777); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
logFileName := "access.log"
|
|
||||||
|
|
||||||
//日志文件
|
|
||||||
fileName := path.Join(logFilePath, logFileName)
|
|
||||||
if _, err := os.Stat(fileName); err != nil {
|
|
||||||
if _, err := os.Create(fileName); err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//写入文件
|
|
||||||
src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("err", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//实例化
|
|
||||||
logger := logrus.New()
|
|
||||||
|
|
||||||
//设置输出
|
|
||||||
logger.SetOutput(src)
|
|
||||||
// logger.Out = src
|
|
||||||
|
|
||||||
//设置日志级别
|
|
||||||
logger.SetLevel(logrus.DebugLevel)
|
|
||||||
|
|
||||||
//设置日志格式
|
|
||||||
logger.Formatter = &logrus.JSONFormatter{}
|
|
||||||
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文件日志
|
|
||||||
func LoggerToFile() gin.HandlerFunc {
|
|
||||||
logger := Logger()
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
logMap := make(map[string]interface{})
|
|
||||||
|
|
||||||
// 开始时间
|
|
||||||
startTime := time.Now()
|
|
||||||
logMap["startTime"] = startTime.Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
// 处理请求
|
|
||||||
c.Next()
|
|
||||||
|
|
||||||
// 结束时间
|
|
||||||
endTime := time.Now()
|
|
||||||
logMap["endTime"] = endTime.Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
// 执行时间
|
|
||||||
logMap["latencyTime"] = endTime.Sub(startTime).Microseconds()
|
|
||||||
|
|
||||||
// 请求方式
|
|
||||||
logMap["reqMethod"] = c.Request.Method
|
|
||||||
|
|
||||||
// 请求路由
|
|
||||||
logMap["reqUri"] = c.Request.RequestURI
|
|
||||||
|
|
||||||
// 状态码
|
|
||||||
logMap["statusCode"] = c.Writer.Status()
|
|
||||||
|
|
||||||
// 请求IP
|
|
||||||
logMap["clientIP"] = c.ClientIP()
|
|
||||||
|
|
||||||
// 请求 UA
|
|
||||||
logMap["clientUA"] = c.Request.UserAgent()
|
|
||||||
|
|
||||||
//日志格式
|
|
||||||
// logJson, _ := json.Marshal(logMap)
|
|
||||||
// logger.Info(string(logJson))
|
|
||||||
|
|
||||||
logger.WithFields(logrus.Fields{
|
|
||||||
"startTime": logMap["startTime"],
|
|
||||||
"endTime": logMap["endTime"],
|
|
||||||
"latencyTime": logMap["latencyTime"],
|
|
||||||
"reqMethod": logMap["reqMethod"],
|
|
||||||
"reqUri": logMap["reqUri"],
|
|
||||||
"statusCode": logMap["statusCode"],
|
|
||||||
"clientIP": logMap["clientIP"],
|
|
||||||
"clientUA": logMap["clientUA"],
|
|
||||||
}).Info()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// redis 连接池
|
|
||||||
func initRedisPool() {
|
|
||||||
// 建立连接池
|
|
||||||
redisPool = &redis.Pool{
|
|
||||||
MaxIdle: redisPoolConfig.maxIdle,
|
|
||||||
MaxActive: redisPoolConfig.maxActive,
|
|
||||||
IdleTimeout: time.Duration(redisPoolConfig.maxIdleTimeout) * time.Second,
|
|
||||||
Wait: true,
|
|
||||||
Dial: func() (redis.Conn, error) {
|
|
||||||
con, err := redis.Dial("tcp", redisPoolConfig.host,
|
|
||||||
redis.DialPassword(redisPoolConfig.password),
|
|
||||||
redis.DialDatabase(redisPoolConfig.db),
|
|
||||||
redis.DialConnectTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second),
|
|
||||||
redis.DialReadTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second),
|
|
||||||
redis.DialWriteTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return con, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
random.go
Normal file
28
random.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
// generate is a function that takes an integer bits and returns a string.
|
||||||
|
// The function generates a random string of length equal to bits using the letterBytes slice.
|
||||||
|
// The letterBytes slice contains characters that can be used to generate a random string.
|
||||||
|
// The generation of the random string is based on the current time using the UnixNano() function.
|
||||||
|
func GenerateRandomString(bits int) string {
|
||||||
|
// Create a byte slice b of length bits.
|
||||||
|
b := make([]byte, bits)
|
||||||
|
|
||||||
|
// Create a new random number generator with the current time as the seed.
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
// Generate a random byte for each element in the byte slice b using the letterBytes slice.
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[r.Intn(len(letterBytes))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the byte slice to a string and return it.
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
16
redis.go
Normal file
16
redis.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
var RedisClient *redis.Client
|
||||||
|
|
||||||
|
// initRedisClient is a function that takes a pointer to a RedisOptions struct and returns a pointer to a Redis client.
|
||||||
|
func initRedisClient(options *redis.Options) {
|
||||||
|
RedisClient = redis.NewClient(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRedisClient() *redis.Client {
|
||||||
|
return RedisClient
|
||||||
|
}
|
||||||
41
redis_test.go
Normal file
41
redis_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var mockRedisOptions = &redis.Options{
|
||||||
|
Addr: "localhost:6379",
|
||||||
|
Password: "",
|
||||||
|
DB: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRedisClient(t *testing.T) {
|
||||||
|
client := GetRedisClient()
|
||||||
|
assert.Nil(t, client)
|
||||||
|
|
||||||
|
initRedisClient(mockRedisOptions)
|
||||||
|
client = GetRedisClient()
|
||||||
|
assert.NotNil(t, client)
|
||||||
|
|
||||||
|
// Test redis exec commands and response
|
||||||
|
ctx := context.Background()
|
||||||
|
rs := client.Ping(ctx)
|
||||||
|
assert.Nil(t, rs.Err())
|
||||||
|
assert.Equal(t, "PONG", rs.Val())
|
||||||
|
|
||||||
|
rsCmd := GetRedisClient().Do(ctx, "dbsize")
|
||||||
|
assert.Nil(t, rsCmd.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGetRedisClient(b *testing.B) {
|
||||||
|
initRedisClient(mockRedisOptions)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
GetRedisClient().Get(context.Background(), "key")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user