浏览代码

Change the cache driver and tidy up the project structure.

CareyWong 1 年之前
父节点
当前提交
cfd7923530
共有 18 个文件被更改,包括 567 次插入408 次删除
  1. 2 2
      .env.example
  2. 2 1
      .github/workflows/go.yml
  3. 2 0
      .gitignore
  4. 5 5
      Dockerfile
  5. 10 11
      Makefile
  6. 12 34
      README.md
  7. 13 0
      const.go
  8. 3 3
      docker-compose.yaml
  9. 9 2
      go.mod
  10. 19 2
      go.sum
  11. 112 0
      handlers.go
  12. 142 0
      logger.go
  13. 52 0
      logic.go
  14. 31 0
      logic_test.go
  15. 68 348
      main.go
  16. 28 0
      random.go
  17. 16 0
      redis.go
  18. 41 0
      redis_test.go

+ 2 - 2
.env.example

@@ -1,3 +1,3 @@
-MYURLS_PORT=8002
+MYURLS_PORT=8080
 MYURLS_DOMAIN=example.com
 MYURLS_DOMAIN=example.com
-MYURLS_TTL=180
+MYURLS_PROTO=https

+ 2 - 1
.github/workflows/go.yml

@@ -2,7 +2,8 @@ name: Github CI
 
 
 on:
 on:
   push:
   push:
-    branches: [master]
+    branches: 
+      - '*'
 
 
 jobs:
 jobs:
   linux_amd64_build:
   linux_amd64_build:

+ 2 - 0
.gitignore

@@ -2,6 +2,8 @@
 build/
 build/
 logs/
 logs/
 data/
 data/
+*.log
 
 
 .env
 .env
 dist/
 dist/
+MyUrls 

+ 5 - 5
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 .
-RUN go mod tidy
-RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o myurls main.go \
-    && upx myurls
+COPY . .
+
+# RUN go env -w GOPROXY=https://mirrors.cloud.tencent.com/go/,direct
+RUN go mod download
+RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o myurls
 
 
 FROM scratch
 FROM scratch
 WORKDIR /app
 WORKDIR /app

+ 10 - 11
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

+ 12 - 34
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
-
-#### 部署
-
-[![Deploy on Railway](https://railway.app/button.svg)](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
-        短链接域名,必填项
-  -passwd string
-        Redis连接密码
-  -port int
-        服务端口 (default 8002)
-  -ttl int
-        短链接有效期,单位(天),默认180天。 (default 180)
+        domain of the server (default "localhost:8080")
+  -h    display help
+  -password string
+        password of the redis server
+  -port string
+        port to run the server on (default "8080")
+  -proto string
+        protocol of the server (default "https")
 ```
 ```
 
 
 建议配合 [pm2](https://pm2.keymetrics.io/) 开启守护进程。
 建议配合 [pm2](https://pm2.keymetrics.io/) 开启守护进程。

+ 13 - 0
const.go

@@ -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

+ 3 - 3
docker-compose.yaml

@@ -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:

+ 9 - 2
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

+ 19 - 2
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 - 0
handlers.go

@@ -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 - 0
logger.go

@@ -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 - 0
logic.go

@@ -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 - 0
logic_test.go

@@ -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)
+}

+ 68 - 348
main.go

@@ -1,391 +1,111 @@
 package main
 package main
 
 
 import (
 import (
-	"crypto/md5"
-	"encoding/base64"
-	"encoding/hex"
+	"context"
 	"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/sirupsen/logrus"
+	"github.com/redis/go-redis/v9"
 )
 )
 
 
-// Response is the response structure
-type Response struct {
-	Code     int
-	Message  string
-	LongUrl  string
-	ShortUrl string
-}
-
-// 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
+var helpFlag bool
 
 
-// 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
+var (
+	port          = "8080"
+	domain        = "localhost:8080"
+	proto         = "https"
+	redisAddr     = "localhost:6379"
+	redisPassword = ""
+)
 
 
-// redisPoolConfig is the Redis pool configuration.
-var redisPoolConfig *redisPoolConf
+func init() {
+	flag.BoolVar(&helpFlag, "h", false, "display help")
 
 
-// redisClient is a Redis client.
-var redisClient redis.Conn
+	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")
+}
 
 
 func main() {
 func main() {
-	gin.SetMode(gin.ReleaseMode)
-	router := gin.Default()
-
-	// Log 收集中间件
-	router.Use(LoggerToFile())
-
-	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()
 	flag.Parse()
-
-	if *domain == "" {
+	if helpFlag {
 		flag.Usage()
 		flag.Usage()
-		log.Fatalln("缺少关键参数")
-	}
-
-	redisPoolConfig = &redisPoolConf{
-		maxIdle:        1024,
-		maxActive:      1024,
-		maxIdleTimeout: 30,
-		host:           *conn,
-		password:       *passwd,
-		db:             0,
-		handleTimeout:  30,
+		os.Exit(0)
 	}
 	}
-	initRedisPool()
 
 
-	router.GET("/", func(context *gin.Context) {
-		context.HTML(http.StatusOK, "index.html", gin.H{
-			"title": "MyUrls",
-		})
-	})
-
-	// 短链接生成
-	router.POST("/short", func(context *gin.Context) {
-		res := &Response{
-			Code:     1,
-			Message:  "",
-			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)
-		}
+	// 从环境变量中读取配置,且环境变量优先级高于命令行参数
+	parseEnvirons()
 
 
-		protocol := "http://"
-		if *https != 0 {
-			protocol = "https://"
-		}
-		res.ShortUrl = protocol + *domain + "/" + shortKey
+	InitLogger()
 
 
-		// context.Header("Access-Control-Allow-Origin", "*")
-		context.JSON(200, *res)
+	// init and check redis
+	initRedisClient(&redis.Options{
+		Addr:     redisAddr,
+		Password: redisPassword,
+		DB:       0,
 	})
 	})
 
 
-	// 短链接跳转
-	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)
-		}
-	})
+	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 优化
-	ballast := make([]byte, 1<<30) // 分配 1G 内存,不会实际占用物理内存,不可读写该变量
+	// GC optimize
+	ballast := make([]byte, 1<<30) // 预分配 1G 内存,不会实际占用物理内存,不可读写该变量
 	defer func() {
 	defer func() {
-		log.Println("ballast len %v", len(ballast))
+		logger.Info("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
+	// start http server
+	run()
 }
 }
 
 
-// 续命
-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)
-		}
+func parseEnvirons() {
+	if p := os.Getenv("MYURLS_PORT"); p != "" {
+		port = p
 	}
 	}
-}
-
-// 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))]
+	if d := os.Getenv("MYURLS_DOMAIN"); d != "" {
+		domain = d
 	}
 	}
-
-	// 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 p := os.Getenv("MYURLS_PROTO"); p != "" {
+		proto = p
 	}
 	}
-	if err := os.MkdirAll(logFilePath, 0777); err != nil {
-		fmt.Println(err.Error())
+	if c := os.Getenv("MYURLS_REDIS_CONN"); c != "" {
+		redisAddr = c
 	}
 	}
-	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())
-		}
+	if p := os.Getenv("MYURLS_REDIS_PASSWORD"); p != "" {
+		redisPassword = p
 	}
 	}
-
-	//写入文件
-	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()
+func run() {
+	// init and run server
+	gin.SetMode(gin.ReleaseMode)
+	router := gin.Default()
 
 
-		// 请求IP
-		logMap["clientIP"] = c.ClientIP()
+	// logger
+	router.Use(LoggerToFile())
 
 
-		// 请求 UA
-		logMap["clientUA"] = c.Request.UserAgent()
+	// static files
+	router.LoadHTMLGlob("public/*.html")
 
 
-		//日志格式
-		// logJson, _ := json.Marshal(logMap)
-		// logger.Info(string(logJson))
+	router.GET("/", func(context *gin.Context) {
+		context.HTML(http.StatusOK, "index.html", gin.H{
+			"title": "MyUrls",
+		})
+	})
 
 
-		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()
-	}
-}
+	router.POST("/short", LongToShortHandler())
+	router.GET("/:shortKey", ShortToLongHandler())
 
 
-// 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
-		},
-	}
+	logger.Infof("server running on :%s", port)
+	router.Run(fmt.Sprintf(":%s", port))
 }
 }

+ 28 - 0
random.go

@@ -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 - 0
redis.go

@@ -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 - 0
redis_test.go

@@ -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")
+	}
+}