Compare commits

..

6 Commits

Author SHA1 Message Date
ygxbnet 75de353af6 refactor(uploader): 优化文件上传处理逻辑和资源管理
构建上传工具 / build-tool (push) Successful in 1m20s
- 简化响应体关闭逻辑,移除不必要的nil检查
- 调整后台状态推送频率,从500ms改为250ms
- 修复前端事件监听器注册顺序
- 移除未使用的进度变量
- 优化goroutine中的任务执行逻辑
- 改进文件路径显示,统一使用文件名而非完整路径
- 添加waitgroup等待确保资源正确释放
2026-04-28 01:07:43 +08:00
ygxbnet 12ef425b01 refactor(app): 移除调试打印语句
- 删除了 WriteConfig 方法中的 fmt.Println 调试代码
- 保留了核心配置写入功能不变
2026-04-28 00:14:06 +08:00
ygxbnet 34a3a70569 style(frontend): 调整表单项间距样式并优化进度列表布局
- 移除表单项内部的 8px 间距
- 为进度列表添加 12px 上边距以改善视觉层次
- 在上传器初始化时清除进度状态确保界面一致性
2026-04-28 00:01:54 +08:00
ygxbnet b050c36904 feat(App): 添加上传进度列表排序功能
- 引入 computed 属性用于对进度列表进行排序
- 按照上传状态优先级排序:已完成 > 未开始 > 上传中
- 将排序后的列表绑定到模板中的进度显示组件
- 优化用户体验,让完成和未开始的文件更易识别
2026-04-27 23:50:40 +08:00
ygxbnet f96f23360c feat(app): 添加自动启动和日志滚动功能并优化上传逻辑
- 增加了运行时自动启动上传配置选项
- 实现了日志输出的滚动控制功能
- 优化了上传进度显示和状态同步机制
- 提升了HTTP客户端连接池配置至500
- 改进了文件上传完成后的清理逻辑
- 添加了上下文取消检查避免资源泄露
- 完善了上传开始时的日志信息输出
2026-04-27 23:40:10 +08:00
ygxbnet d4cc335fbf refactor(app): 重构应用状态管理和配置常量定义
- 将全局变量 isRun 移动到 App 结构体内部作为实例字段
- 在 config.go 中定义配置键名为常量,提高代码可维护性
- 使用结构体实例字段替代全局变量管理上传状态
- 修改 StartLooking 函数中的上下文取消处理逻辑
- 移除上传程序退出日志的重复记录
2026-04-27 21:43:37 +08:00
5 changed files with 153 additions and 79 deletions
+17 -7
View File
@@ -16,10 +16,9 @@ type App struct {
logChan chan string logChan chan string
uploaderCTX context.Context uploaderCTX context.Context
uploaderCancel context.CancelFunc uploaderCancel context.CancelFunc
isRun bool
} }
var isRun = false
func NewApp() *App { func NewApp() *App {
return &App{} return &App{}
} }
@@ -33,15 +32,24 @@ func (a *App) startup(ctx context.Context) {
go func() { go func() {
for log := range a.logChan { for log := range a.logChan {
runtime.EventsEmit(a.ctx, "log", log) runtime.EventsEmit(a.ctx, "log", log)
time.Sleep(time.Millisecond)
}
}()
// 后台 goroutine 持续推送运行状态
go func() {
for {
time.Sleep(250 * time.Millisecond)
runtime.EventsEmit(a.ctx, "is-run", a.isRun)
} }
}() }()
//在程序启动时运行上传程序 //在程序启动时运行上传程序
a.uploaderCTX, a.uploaderCancel = context.WithCancel(a.ctx)
if config.APPConfig.IsRunOnStart { if config.APPConfig.IsRunOnStart {
time.Sleep(time.Second) time.Sleep(time.Second)
isRun = true a.uploaderCTX, a.uploaderCancel = context.WithCancel(a.ctx)
go uploader.StartLooking(a.uploaderCTX, &a.logChan, config.APPConfig.CheckDir) go uploader.StartLooking(a.uploaderCTX, &a.logChan, config.APPConfig.CheckDir)
a.isRun = true
} }
} }
@@ -57,20 +65,22 @@ func (a *App) GetConfig() config.Config {
} }
func (a *App) WriteConfig(key string, value any) { func (a *App) WriteConfig(key string, value any) {
fmt.Println(key, value)
config.WriteConfig(key, value) config.WriteConfig(key, value)
} }
func (a *App) StartUpload() { func (a *App) StartUpload() {
if isRun { if a.isRun {
return return
} }
a.uploaderCTX, a.uploaderCancel = context.WithCancel(a.ctx) a.uploaderCTX, a.uploaderCancel = context.WithCancel(a.ctx)
go uploader.StartLooking(a.uploaderCTX, &a.logChan, config.APPConfig.CheckDir) go uploader.StartLooking(a.uploaderCTX, &a.logChan, config.APPConfig.CheckDir)
a.isRun = true
} }
func (a *App) StopUpload() { func (a *App) StopUpload() {
if isRun { if a.isRun {
a.uploaderCancel() a.uploaderCancel()
} }
a.isRun = false
uploader.AddLog(&a.logChan, "上传程序已退出")
} }
+38 -20
View File
@@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref, nextTick, watch} from 'vue' import {ref, nextTick, watch, computed} from 'vue'
import {ElMessage} from 'element-plus' import {ElMessage} from 'element-plus'
import {ElMessageBox} from 'element-plus' import {ElMessageBox} from 'element-plus'
import {SelectPath, GetConfig, WriteConfig, StartUpload, StopUpload} from '../wailsjs/go/main/App'; import {SelectPath, GetConfig, WriteConfig, StartUpload, StopUpload} from '../wailsjs/go/main/App';
@@ -12,11 +12,12 @@ const token = ref('')
const checkDir = ref('') const checkDir = ref('')
const concurrentFiles = ref(1) const concurrentFiles = ref(1)
const uploadThreads = ref(1) const uploadThreads = ref(1)
const autoStart = ref(false)
const progress = ref(0)
const isRunning = ref(false) const isRunning = ref(false)
const logOutput = ref<string[]>([]) const logOutput = ref<string[]>([])
const logContentRef = ref<HTMLElement>() const logContentRef = ref<HTMLElement>()
const logRoll = ref(true)
interface FileProgress { interface FileProgress {
name: string name: string
@@ -29,10 +30,21 @@ const progressList = ref<FileProgress[]>([
// {name: '测试文件1.txt', uploaded: 100, total: 500, percentage: 20}, // {name: '测试文件1.txt', uploaded: 100, total: 500, percentage: 20},
]) ])
const sortedProgressList = computed(() => {
return [...progressList.value].sort((a, b) => {
const getPriority = (item: FileProgress) => {
if (item.percentage === 100) return 2
if (item.percentage === 0) return 1
return 0
}
return getPriority(a) - getPriority(b)
})
})
const addLog = (msg: string) => { const addLog = (msg: string) => {
logOutput.value.push(`[${new Date().toLocaleString()}]` + msg) logOutput.value.push(`[${new Date().toLocaleString()}]` + msg)
nextTick(() => { nextTick(() => {
if (logContentRef.value) { if (logContentRef.value && logRoll.value) {
logContentRef.value.scrollTop = logContentRef.value.scrollHeight logContentRef.value.scrollTop = logContentRef.value.scrollHeight
} }
}) })
@@ -58,26 +70,15 @@ const startRun = () => {
ElMessage.warning('请选择检测目录') ElMessage.warning('请选择检测目录')
return return
} }
isRunning.value = true
progress.value = 0
addLog("===============================================")
// addLog(`开始运行...`)
addLog(`服务器: ${serverUrl.value}`)
addLog(`检测目录: ${checkDir.value}`)
addLog(`同时处理文件数: ${concurrentFiles.value}`)
addLog(`单文件上传线程: ${uploadThreads.value}`)
addLog("===============================================")
StartUpload() StartUpload()
} }
const stopRun = () => { const stopRun = () => {
if (isRunning.value) {
isRunning.value = false
addLog(`正在停止运行`) addLog(`正在停止运行`)
StopUpload().then(() => { StopUpload().then(() => {
ElMessage.info('已停止运行') ElMessage.info('已停止运行')
}) })
}
} }
const clearLog = () => { const clearLog = () => {
@@ -100,6 +101,7 @@ try {
checkDir.value = config.check_dir checkDir.value = config.check_dir
concurrentFiles.value = config.handle_file_count concurrentFiles.value = config.handle_file_count
uploadThreads.value = config.thread_count uploadThreads.value = config.thread_count
autoStart.value = config.is_run_on_start
LogPrint(`[${new Date().toLocaleString()}] 配置已加载`) LogPrint(`[${new Date().toLocaleString()}] 配置已加载`)
}) })
@@ -122,13 +124,19 @@ watch(concurrentFiles, () => {
watch(uploadThreads, () => { watch(uploadThreads, () => {
WriteConfig("thread-count", uploadThreads.value) WriteConfig("thread-count", uploadThreads.value)
}) })
watch(autoStart, () => {
WriteConfig("is-run-on-start", autoStart.value)
})
EventsOn("log", (msg) => { EventsOn("is-run", (run) => {
addLog(msg) isRunning.value = run
}) })
EventsOn("progress", (progress) => { EventsOn("progress", (progress) => {
progressList.value = progress progressList.value = progress
}) })
EventsOn("log", (msg) => {
addLog(msg)
})
</script> </script>
<template> <template>
@@ -162,10 +170,14 @@ EventsOn("progress", (progress) => {
<el-input-number v-model="uploadThreads" :min="1" :max="100" :disabled="isRunning"/> <el-input-number v-model="uploadThreads" :min="1" :max="100" :disabled="isRunning"/>
</div> </div>
<div class="form-item">
<el-checkbox v-model="autoStart" label="运行时自动启动上传" size="large" :disabled="isRunning"/>
</div>
<div class="form-item"> <div class="form-item">
<label>上传进度</label> <label>上传进度</label>
<div class="progress-list"> <div class="progress-list">
<div v-for="(item, index) in progressList" :key="index" class="progress-item"> <div v-for="(item, index) in sortedProgressList" :key="index" class="progress-item">
<span class="file-name">{{ item.name }}</span> <span class="file-name">{{ item.name }}</span>
<el-progress :percentage="item.percentage" :status="item.percentage === 100 ? 'success' : undefined"/> <el-progress :percentage="item.percentage" :status="item.percentage === 100 ? 'success' : undefined"/>
<span class="progress-text">{{ item.uploaded }}/{{ item.total }}</span> <span class="progress-text">{{ item.uploaded }}/{{ item.total }}</span>
@@ -181,7 +193,10 @@ EventsOn("progress", (progress) => {
</div> </div>
<div class="right-panel"> <div class="right-panel">
<div class="log-header">日志输出</div> <div class="log-header">
日志输出
<el-checkbox v-model="logRoll" label="开启日志滚动"/>
</div>
<div class="log-content" ref="logContentRef"> <div class="log-content" ref="logContentRef">
<div v-for="(log, index) in logOutput" :key="index" class="log-line">{{ log }}</div> <div v-for="(log, index) in logOutput" :key="index" class="log-line">{{ log }}</div>
</div> </div>
@@ -212,7 +227,6 @@ EventsOn("progress", (progress) => {
.form-item { .form-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px;
} }
.form-item label { .form-item label {
@@ -238,6 +252,7 @@ EventsOn("progress", (progress) => {
} }
.progress-list { .progress-list {
margin-top: 12px;
max-height: 160px; max-height: 160px;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;
@@ -286,6 +301,9 @@ EventsOn("progress", (progress) => {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #303133; color: #303133;
display: flex;
justify-content: space-between;
align-items: center;
} }
.log-content { .log-content {
+4 -5
View File
@@ -11,8 +11,8 @@ import (
var httpClient = &http.Client{ var httpClient = &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
MaxIdleConns: 200, MaxIdleConns: 500,
MaxIdleConnsPerHost: 200, MaxIdleConnsPerHost: 500,
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
}, },
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
@@ -38,9 +38,8 @@ func UploadDataToServer(ctx context.Context, data string) error {
if err != nil { if err != nil {
return err return err
} }
if resp != nil { io.Copy(io.Discard, resp.Body)
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close() resp.Body.Close()
}
return err return err
} }
+15 -6
View File
@@ -18,6 +18,15 @@ type Config struct {
var APPConfig Config var APPConfig Config
const (
Url = "url"
Token = "token"
ThreadCount = "thread-count"
HandleFileCount = "handle-file-count"
IsRunOnStart = "is-run-on-start"
CheckDir = "check-dir"
)
func InitConfig() { func InitConfig() {
// 设置默认配置 // 设置默认配置
defaultConfig := Config{ defaultConfig := Config{
@@ -28,12 +37,12 @@ func InitConfig() {
IsRunOnStart: false, IsRunOnStart: false,
CheckDir: "", CheckDir: "",
} }
viper.SetDefault("url", defaultConfig.Url) viper.SetDefault(Url, defaultConfig.Url)
viper.SetDefault("token", defaultConfig.Token) viper.SetDefault(Token, defaultConfig.Token)
viper.SetDefault("thread-count", defaultConfig.ThreadCount) viper.SetDefault(ThreadCount, defaultConfig.ThreadCount)
viper.SetDefault("handle-file-count", defaultConfig.HandleFileCount) viper.SetDefault(HandleFileCount, defaultConfig.HandleFileCount)
viper.SetDefault("is-run-on-start", defaultConfig.IsRunOnStart) viper.SetDefault(IsRunOnStart, defaultConfig.IsRunOnStart)
viper.SetDefault("looking-path", defaultConfig.CheckDir) viper.SetDefault(CheckDir, defaultConfig.CheckDir)
//设置配置文件名和路径 ./config.toml //设置配置文件名和路径 ./config.toml
viper.AddConfigPath(".") viper.AddConfigPath(".")
+42 -4
View File
@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -33,9 +34,23 @@ type Progress struct {
} }
func StartLooking(ctx context.Context, logChan *chan string, lookingPath string) { func StartLooking(ctx context.Context, logChan *chan string, lookingPath string) {
AddLog(logChan, "===============================================")
AddLog(logChan, `服务器: `+config.APPConfig.Url)
AddLog(logChan, `Token: `+config.APPConfig.Token)
AddLog(logChan, `检测目录: `+config.APPConfig.CheckDir)
AddLog(logChan, `同时处理文件数: `+strconv.Itoa(config.APPConfig.HandleFileCount))
AddLog(logChan, `单文件上传线程: `+strconv.Itoa(config.APPConfig.ThreadCount))
AddLog(logChan, "===============================================")
progress.Clear()
//推送上传进度
go func() { go func() {
for { for {
time.Sleep(500 * time.Millisecond) select {
case <-ctx.Done():
return
default:
time.Sleep(250 * time.Millisecond)
var pg []Progress var pg []Progress
progress.Range(func(key, value any) bool { progress.Range(func(key, value any) bool {
@@ -45,6 +60,7 @@ func StartLooking(ctx context.Context, logChan *chan string, lookingPath string)
}) })
runtime.EventsEmit(ctx, "progress", pg) runtime.EventsEmit(ctx, "progress", pg)
} }
}
}() }()
for { for {
@@ -52,7 +68,6 @@ func StartLooking(ctx context.Context, logChan *chan string, lookingPath string)
select { select {
case <-time.After(time.Minute): case <-time.After(time.Minute):
case <-ctx.Done(): case <-ctx.Done():
AddLog(logChan, "上传程序已退出")
return return
} }
} }
@@ -103,6 +118,7 @@ func uploadData(ctx context.Context, logChan *chan string, lookingPath string) {
fileLines[filepath.Base(filePath)] = lineCount fileLines[filepath.Base(filePath)] = lineCount
isAllEmpty = false isAllEmpty = false
AddLog(logChan, fmt.Sprintf("%s 文件行数:%v", filepath.Base(filePath), lineCount)) AddLog(logChan, fmt.Sprintf("%s 文件行数:%v", filepath.Base(filePath), lineCount))
progress.Store(filepath.Base(filePath), Progress{FileName: filepath.Base(filePath), Total: lineCount, Uploaded: 0, Percentage: 0})
} }
} }
if isAllEmpty { if isAllEmpty {
@@ -121,6 +137,10 @@ func uploadData(ctx context.Context, logChan *chan string, lookingPath string) {
g.SetLimit(config.APPConfig.HandleFileCount) // 设置同时处理文件数 g.SetLimit(config.APPConfig.HandleFileCount) // 设置同时处理文件数
// 执行所有任务 // 执行所有任务
for _, task := range tasks { for _, task := range tasks {
select {
case <-egctx.Done():
return
default:
g.Go(func() error { g.Go(func() error {
select { select {
case <-egctx.Done(): case <-egctx.Done():
@@ -130,6 +150,10 @@ func uploadData(ctx context.Context, logChan *chan string, lookingPath string) {
processFile(egctx, logChan, task.FilePath, task.FileLines) processFile(egctx, logChan, task.FilePath, task.FileLines)
select {
case <-egctx.Done():
return egctx.Err()
default:
//上传完成,清空文件 //上传完成,清空文件
err := os.Truncate(task.FilePath, 0) err := os.Truncate(task.FilePath, 0)
if err != nil { if err != nil {
@@ -137,9 +161,15 @@ func uploadData(ctx context.Context, logChan *chan string, lookingPath string) {
} }
return nil return nil
} }
}
}) })
} }
}
select {
case <-ctx.Done():
return
default:
// 等待所有任务完成 // 等待所有任务完成
if err := g.Wait(); err != nil { if err := g.Wait(); err != nil {
AddLog(logChan, fmt.Sprintf("任务执行出错: %v", err)) AddLog(logChan, fmt.Sprintf("任务执行出错: %v", err))
@@ -149,6 +179,7 @@ func uploadData(ctx context.Context, logChan *chan string, lookingPath string) {
AddLog(logChan, fmt.Sprintf("上传完成,耗时:%s", time.Since(start).String())) AddLog(logChan, fmt.Sprintf("上传完成,耗时:%s", time.Since(start).String()))
progress.Clear() progress.Clear()
}
} }
// 获取目录中的所有txt文件 // 获取目录中的所有txt文件
@@ -189,10 +220,15 @@ func processFile(ctx context.Context, logChan *chan string, filePath string, fil
var countLine int32 = 0 var countLine int32 = 0
// 创建指定个worker同时处理文件上传 // 创建指定个worker同时处理文件上传
for i := 0; i < config.APPConfig.ThreadCount; i++ { for i := 0; i < config.APPConfig.ThreadCount; i++ {
select {
case <-ctx.Done():
return
default:
wg.Go(func() { wg.Go(func() {
processLines(ctx, logChan, &lines, i, filePath, &countLine) processLines(ctx, logChan, &lines, i, filePath, &countLine)
}) })
} }
}
// 读取文件并发送到通道 // 读取文件并发送到通道
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
@@ -211,6 +247,7 @@ func processFile(ctx context.Context, logChan *chan string, filePath string, fil
select { select {
case <-ctx.Done(): case <-ctx.Done():
close(lines) close(lines)
wg.Wait()
return return
default: default:
progress.Store(filepath.Base(filePath), progress.Store(filepath.Base(filePath),
@@ -230,7 +267,7 @@ func processFile(ctx context.Context, logChan *chan string, filePath string, fil
return return
} }
AddLog(logChan, fmt.Sprintf("文件【%s】处理完成,共处理 %d 行数据", filePath, countLine)) AddLog(logChan, fmt.Sprintf("文件【%s】处理完成,共处理 %d 行数据", filepath.Base(filePath), countLine))
} }
func processLines(ctx context.Context, logChan *chan string, lines *chan string, workerID int, filePath string, countLine *int32) { func processLines(ctx context.Context, logChan *chan string, lines *chan string, workerID int, filePath string, countLine *int32) {
@@ -245,13 +282,14 @@ func processLines(ctx context.Context, logChan *chan string, lines *chan string,
} }
// 上传数据 // 上传数据
if err := api.UploadDataToServer(ctx, line); err != nil { if err := api.UploadDataToServer(ctx, line); err != nil {
AddLog(logChan, fmt.Sprintf("Worker %d (文件 %s): 上传失败: %v", workerID, filePath, err)) AddLog(logChan, fmt.Sprintf("Worker %d (文件 %s): 上传失败: %v", workerID, filepath.Base(filePath), err))
} }
atomic.AddInt32(countLine, 1) atomic.AddInt32(countLine, 1)
} }
} }
} }
// AddLog 添加日志
func AddLog(logChan *chan string, message string) { func AddLog(logChan *chan string, message string) {
*logChan <- message *logChan <- message
} }