Compare commits

..

8 Commits

Author SHA1 Message Date
819a2eb8ec refactor: 重构项目结构并优化路由组织
All checks were successful
部署开发环境 / deploy-dev (push) Successful in 8m30s
2025-10-28 12:20:16 +08:00
ea9ecb770d ops: 删除构建上传工具的CI配置
Some checks failed
部署开发环境 / deploy-dev (push) Has been cancelled
2025-10-11 12:37:36 +08:00
3476d66c0d refactor: 移除tool目录及相关依赖配置
Some checks failed
构建上传工具 / build-tool (push) Failing after 29s
部署开发环境 / deploy-dev (push) Successful in 1m7s
2025-10-11 12:35:43 +08:00
1f0158dd76 refactor(tool): 优化文件处理逻辑和错误处理
All checks were successful
构建上传工具 / build-tool (push) Successful in 54s
部署开发环境 / deploy-dev (push) Successful in 1m13s
2025-10-11 12:26:43 +08:00
f35fad442a refactor: 使用errgroup优化文件上传并发控制
All checks were successful
构建上传工具 / build-tool (push) Successful in 2m24s
部署开发环境 / deploy-dev (push) Successful in 3m11s
2025-10-10 00:07:48 +08:00
a505f2ddc9 Merge remote-tracking branch 'origin/main'
All checks were successful
构建上传工具 / build-tool (push) Successful in 1m27s
部署开发环境 / deploy-dev (push) Successful in 1m47s
2025-09-18 20:34:13 +08:00
42cfe0dc0f feat(token): 添加Token存在性检查 2025-09-18 20:32:19 +08:00
2c8e25bdf8 refactor: 更新页面标题为抖音数据去重 2025-09-17 12:21:31 +08:00
12 changed files with 87 additions and 253 deletions

View File

@@ -1,37 +0,0 @@
name: 构建上传工具
on: [ push ]
jobs:
build-tool:
env:
RUNNER_TOOL_CACHE: /toolcache
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 安装Go镜像
run: |
# 使用国内镜像下载 Go
wget https://mirrors.aliyun.com/golang/go1.25.1.linux-amd64.tar.gz -O go.tar.gz
# 解压并设置环境变量
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go.tar.gz
echo "/usr/local/go/bin" >> $GITHUB_PATH
env:
GOROOT: /usr/local/go
- name: 构建上传工具
run: |
go env -w CGO_ENABLED=0 \
&& go env -w GOARCH=amd64 \
&& go env -w GOOS=windows \
&& go mod tidy \
&& cd ./tool \
&& go build -o 上传工具.exe
- name: 上传构建文件
uses: actions/upload-artifact@v3
with:
name: 上传工具
path: tool/上传工具.exe

41
api/api.go Normal file
View File

@@ -0,0 +1,41 @@
package api
import (
"dypid/internal/controller"
"embed"
"io/fs"
"net/http"
"github.com/gin-gonic/gin"
)
func RegRoutes(r *gin.Engine) {
g := r.Group("/api") //初始化路由组 /api/xxxx
{
g.GET("/token", controller.ListTokenHandler) //获取token列表
g.POST("/token", controller.CreateTokenHandler) //创建token
g.PUT("/token", controller.UpdateTokenHandler) //更新token
g.DELETE("/token", controller.DeleteTokenHandler) //删除token
g.GET("/token/info", controller.GetTokenInfoHandler) //获取token信息
g.DELETE("/token/info", controller.DeleteTokenInfoHandler) //删除token数据库
}
{
g.GET("/data", controller.ReadDataHandler) //获取数据
g.POST("/data", controller.WriteDataHandler) //写入数据
}
}
func RegWebService(r *gin.Engine, webDir embed.FS) {
assets, _ := fs.Sub(webDir, "web/dist/assets")
r.StaticFS("/assets", http.FS(assets))
icon, _ := fs.ReadFile(webDir, "web/dist/favicon.ico")
r.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", icon)
})
indexHtml, _ := fs.ReadFile(webDir, "web/dist/index.html")
r.NoRoute(func(c *gin.Context) {
c.Data(200, "text/html; charset=utf-8", indexHtml)
})
}

2
go.mod
View File

@@ -3,7 +3,6 @@ module dypid
go 1.25 go 1.25
require ( require (
github.com/fsnotify/fsnotify v1.8.0
github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.10.1
github.com/redis/go-redis/v9 v9.12.1 github.com/redis/go-redis/v9 v9.12.1
@@ -16,6 +15,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect

View File

@@ -1,8 +1,8 @@
package controller package controller
import ( import (
"dypid/db"
"dypid/global" "dypid/global"
"dypid/internal/db"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"

View File

@@ -1,8 +1,8 @@
package controller package controller
import ( import (
"dypid/db"
"dypid/global" "dypid/global"
"dypid/internal/db"
"net/http" "net/http"
"strconv" "strconv"
@@ -26,6 +26,11 @@ func CreateTokenHandler(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()})
return return
} }
//检查Token是否存在
if db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, gin.H{"error": "创建Token失败Token已经存在"})
return
}
//创建Token //创建Token
err := db.CreateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes) err := db.CreateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes)
@@ -49,6 +54,11 @@ func UpdateTokenHandler(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()})
return return
} }
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, gin.H{"error": "更改失败Token不存在"})
return
}
err := db.UpdateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes) err := db.UpdateToken(input.Token, input.DedupObject, input.DataFormat, input.Notes)
if err != nil { if err != nil {
@@ -67,6 +77,11 @@ func DeleteTokenHandler(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": "参数不能为空 " + err.Error()})
return return
} }
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, gin.H{"error": "删除Token失败Token不存在"})
return
}
err := db.DeleteToken(input.Token) err := db.DeleteToken(input.Token)
if err != nil { if err != nil {
@@ -85,6 +100,11 @@ func GetTokenInfoHandler(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Token不能为空"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Token不能为空"})
return return
} }
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, gin.H{"error": "获取信息失败Token不存在"})
return
}
dedupObject, err := db.GetDedupObject(input.Token) dedupObject, err := db.GetDedupObject(input.Token)
if err != nil { if err != nil {
@@ -125,6 +145,11 @@ func DeleteTokenInfoHandler(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Token不能为空"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Token不能为空"})
return return
} }
//检查Token是否存在
if !db.CheckToken(input.Token) {
c.JSON(http.StatusBadRequest, gin.H{"error": "删除Token失败Token不存在"})
return
}
//检查token是否存在 //检查token是否存在
_, err := db.GetDedupObject(input.Token) _, err := db.GetDedupObject(input.Token)

View File

@@ -32,6 +32,16 @@ start:
} }
} }
func CheckToken(token string) bool {
InitLocalDB()
for _, t := range localDB {
if t.Token == token {
return true
}
}
return false
}
func ListToken() []Token { func ListToken() []Token {
InitLocalDB() InitLocalDB()
return localDB return localDB

38
main.go
View File

@@ -1,13 +1,11 @@
package main package main
import ( import (
"dypid/api"
"dypid/config" "dypid/config"
"dypid/controller" "dypid/internal/db"
"dypid/db"
"embed" "embed"
"fmt" "fmt"
"io/fs"
"net/http"
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -23,35 +21,13 @@ func main() {
//初始化一个http服务对象 //初始化一个http服务对象
gin.SetMode(config.APPConfig.RunMode) gin.SetMode(config.APPConfig.RunMode)
r := gin.Default() r := gin.Default()
//跨域设置 //跨域设置
r.Use(cors.Default()) r.Use(cors.Default())
//注册网页服务Vue
//Vue网站服务 api.RegWebService(r, webDir)
assets, _ := fs.Sub(webDir, "web/dist/assets") //注册API接口
r.StaticFS("/assets", http.FS(assets)) api.RegRoutes(r)
icon, _ := fs.ReadFile(webDir, "web/dist/favicon.ico")
r.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", icon)
})
indexHtml, _ := fs.ReadFile(webDir, "web/dist/index.html")
r.NoRoute(func(c *gin.Context) {
c.Data(200, "text/html; charset=utf-8", indexHtml)
})
//API接口
g := r.Group("/api") //初始化路由组 /api/xxxx
{
g.GET("/token", controller.ListTokenHandler) //获取token列表
g.POST("/token", controller.CreateTokenHandler) //创建token
g.PUT("/token", controller.UpdateTokenHandler) //更新token
g.DELETE("/token", controller.DeleteTokenHandler) //删除token
g.GET("/token/info", controller.GetTokenInfoHandler) //获取token信息
g.DELETE("/token/info", controller.DeleteTokenInfoHandler) //删除token数据库
}
{
g.GET("/data", controller.ReadDataHandler) //获取数据
g.POST("/data", controller.WriteDataHandler) //写入数据
}
// 监听并在 0.0.0.0:8080 上启动服务 // 监听并在 0.0.0.0:8080 上启动服务
fmt.Printf("服务器正在运行http://%s\n", config.APPConfig.Host) fmt.Printf("服务器正在运行http://%s\n", config.APPConfig.Host)

2
tool/.gitignore vendored
View File

@@ -1,2 +0,0 @@
./upload
config.toml

View File

@@ -1,179 +0,0 @@
package main
import (
"bufio"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
var httpClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 200,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 30 * time.Second,
}
func main() {
initConfig()
//检测./upload
fmt.Println("程序启动成功正在检测txt文件")
for {
files, err := getTxtFiles("./")
if err != nil {
fmt.Println(err)
return
}
if files != nil {
start := time.Now()
wg := sync.WaitGroup{}
for _, filePath := range files {
fmt.Println("正在上传文件:", filePath)
wg.Add(1)
go func() {
processFile(filePath)
err := os.Truncate(filePath, 0)
if err != nil {
fmt.Println("清空文件失败:", err)
}
wg.Done()
}()
}
wg.Wait()
fmt.Printf("上传完成,耗时:%s\n", time.Since(start))
}
time.Sleep(time.Minute)
}
}
func initConfig() {
//程序配置
viper.SetDefault("url", "http://localhost:8080")
viper.SetDefault("token", "")
viper.SetDefault("thread-count", 10)
//设置配置文件名和路径 ./config.toml
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("toml")
viper.SafeWriteConfig() //安全写入默认配置
//读取配置文件
if err := viper.ReadInConfig(); err != nil {
fmt.Errorf("无法读取配置文件: %w", err)
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
if err := viper.ReadInConfig(); err != nil {
fmt.Errorf("无法读取配置文件: %w", err)
}
})
}
func uploadDataToServer(data string) error {
params := url.Values{}
params.Set("token", viper.GetString("token"))
params.Set("data", data)
resp, err := httpClient.Post(viper.GetString("url")+"/api/data?"+params.Encode(), "application/x-www-form-urlencoded", strings.NewReader(""))
if err != nil {
return err
}
if resp != nil {
_, _ = io.Copy(io.Discard, resp.Body)
}
return err
}
// 获取目录中的所有txt文件
func getTxtFiles(dir string) (txtFiles []string, err error) {
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 只处理普通文件,跳过目录
if !info.Mode().IsRegular() {
return nil
}
// 检查文件扩展名是否为.txt
if strings.ToLower(filepath.Ext(path)) == ".txt" {
txtFiles = append(txtFiles, path)
}
return nil
})
return txtFiles, err
}
func processFile(filePath string) {
var wg sync.WaitGroup
// 打开文件
file, err := os.Open(filePath)
if err != nil {
fmt.Printf("无法打开文件 %s: %v\n", filePath, err)
return
}
defer file.Close()
// 创建行通道
lines := make(chan string, 100)
// 创建10个worker处理文件上传
for i := 0; i < viper.GetInt("thread-count"); i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
processLines(lines, workerID, filePath)
}(i)
}
// 读取文件并发送到通道
scanner := bufio.NewScanner(file)
lineCount := 0
for scanner.Scan() {
line := scanner.Text()
lines <- line
lineCount++
if lineCount%10000 == 0 {
fmt.Printf("文件【%s】处理进度%v%%\n", filePath, float64(lineCount)/40000*100)
}
}
close(lines)
wg.Wait()
if err := scanner.Err(); err != nil {
fmt.Printf("读取文件 %s 错误: %v\n", filePath, err)
return
}
fmt.Printf("文件【%s】处理完成共处理 %d 行数据\n", filePath, lineCount)
}
func processLines(lines <-chan string, workerID int, filePath string) {
for line := range lines {
// 跳过空行
if strings.TrimSpace(line) == "" {
continue
}
// 上传数据
if err := uploadDataToServer(line); err != nil {
fmt.Printf("Worker %d (文件 %s): 上传失败: %v\n", workerID, filePath, err)
}
}
}

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dypid</title> <title>抖音数据去重</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>