perf: optimize batch task creation with single transaction
批量创建任务使用单事务数据库插入,显著提升性能。 移除任务列表的批量取消/删除进度显示(批处理接口无意义)。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+59
-22
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"bilidown/internal/task"
|
"bilidown/internal/task"
|
||||||
"bilidown/internal/util"
|
"bilidown/internal/util"
|
||||||
@@ -24,54 +25,90 @@ func createTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
util.Res{Success: false, Message: "任务列表为空"}.Write(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
db := util.MustGetDB()
|
db := util.MustGetDB()
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// 预先获取下载目录,避免循环中重复查询
|
// 预先获取下载目录
|
||||||
folder, err := util.GetCurrentFolder(db)
|
folder, err := util.GetCurrentFolder(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.Res{Success: false, Message: fmt.Sprintf("获取下载目录失败: %v", err)}.Write(w)
|
util.Res{Success: false, Message: fmt.Sprintf("获取下载目录失败: %v", err)}.Write(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 批量验证参数
|
||||||
for _, item := range body {
|
for _, item := range body {
|
||||||
if !util.CheckBvidFormat(item.Bvid) {
|
if !util.CheckBvidFormat(item.Bvid) {
|
||||||
util.Res{Success: false, Message: "bvid 格式错误"}.Write(w)
|
util.Res{Success: false, Message: fmt.Sprintf("bvid 格式错误: %s", item.Bvid)}.Write(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if item.Cover == "" || item.Title == "" || item.Owner == "" {
|
if item.Title == "" || item.Owner == "" {
|
||||||
util.Res{Success: false, Message: "参数错误"}.Write(w)
|
util.Res{Success: false, Message: "标题或作者为空"}.Write(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量数据库插入(单事务)
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
util.Res{Success: false, Message: fmt.Sprintf("开启事务失败: %v", err)}.Write(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !util.IsValidURL(item.Cover) {
|
stmt, err := tx.Prepare(`INSERT INTO "task" ("bvid", "cid", "format", "title", "owner", "cover", "status", "folder", "duration", "download_type")
|
||||||
util.Res{Success: false, Message: "封面链接格式错误"}.Write(w)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
||||||
return
|
if err != nil {
|
||||||
}
|
tx.Rollback()
|
||||||
if !util.IsValidURL(item.Audio) {
|
util.Res{Success: false, Message: fmt.Sprintf("准备语句失败: %v", err)}.Write(w)
|
||||||
util.Res{Success: false, Message: "音频链接格式错误"}.Write(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !util.IsValidURL(item.Video) {
|
|
||||||
util.Res{Success: false, Message: "视频链接格式错误"}.Write(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !util.IsValidFormatCode(item.Format) {
|
|
||||||
util.Res{Success: false, Message: "清晰度代码错误"}.Write(w)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
tasks := make([]*task.Task, 0, len(body))
|
||||||
|
for _, item := range body {
|
||||||
item.Folder = folder
|
item.Folder = folder
|
||||||
item.Status = "waiting"
|
item.Status = "waiting"
|
||||||
|
item.Title = util.FilterFileName(item.Title)
|
||||||
_task := task.Task{TaskInDB: item}
|
_task := task.Task{TaskInDB: item}
|
||||||
_task.Title = util.FilterFileName(_task.Title)
|
|
||||||
err = _task.Create(db)
|
util.SqliteLock.Lock()
|
||||||
|
result, err := stmt.Exec(
|
||||||
|
item.Bvid, item.Cid, item.Format, item.Title, item.Owner,
|
||||||
|
item.Cover, item.Status, item.Folder, item.Duration, item.DownloadType,
|
||||||
|
)
|
||||||
|
util.SqliteLock.Unlock()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.Res{Success: false, Message: fmt.Sprintf("_task.Create: %v.", err)}.Write(w)
|
tx.Rollback()
|
||||||
|
util.Res{Success: false, Message: fmt.Sprintf("插入任务失败: %v", err)}.Write(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_task.ID, _ = result.LastInsertId()
|
||||||
|
_task.CreateAt = time.Now()
|
||||||
|
tasks = append(tasks, &_task)
|
||||||
|
}
|
||||||
|
|
||||||
|
util.SqliteLock.Lock()
|
||||||
|
err = tx.Commit()
|
||||||
|
util.SqliteLock.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
util.Res{Success: false, Message: fmt.Sprintf("提交事务失败: %v", err)}.Write(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量启动下载任务
|
||||||
|
for _, _task := range tasks {
|
||||||
go _task.Start()
|
go _task.Start()
|
||||||
}
|
}
|
||||||
util.Res{Success: true, Message: "创建成功"}.Write(w)
|
|
||||||
|
util.Res{Success: true, Message: fmt.Sprintf("成功创建 %d 个任务", len(tasks))}.Write(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getActiveTask(w http.ResponseWriter, r *http.Request) {
|
func getActiveTask(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
+6
-57
@@ -55,34 +55,10 @@ export class TaskRoute implements VanComponent {
|
|||||||
t.selected.val && !t.deleting.val && (t.statusState.val == 'waiting' || t.statusState.val == 'running')
|
t.selected.val && !t.deleting.val && (t.statusState.val == 'waiting' || t.statusState.val == 'running')
|
||||||
))
|
))
|
||||||
|
|
||||||
/** 批量操作进度 */
|
|
||||||
batchProgress = van.state(0)
|
|
||||||
batchTotal = van.state(0)
|
|
||||||
batchOperation = van.state<'cancel' | 'delete' | ''>('')
|
|
||||||
batchInProgress = van.derive(() => this.batchTotal.val > 0 && this.batchProgress.val < this.batchTotal.val)
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.element = this.Root()
|
this.element = this.Root()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 固定在顶部的进度条 */
|
|
||||||
BatchProgressBar() {
|
|
||||||
return div({
|
|
||||||
class: 'sticky-top bg-body p-3 border-bottom',
|
|
||||||
hidden: () => !this.batchInProgress.val,
|
|
||||||
style: 'z-index: 10;'
|
|
||||||
},
|
|
||||||
div({ class: 'text-center fs-5' }, () => {
|
|
||||||
const op = this.batchOperation.val == 'cancel' ? '取消' : '删除'
|
|
||||||
return `正在批量${op}任务 (${this.batchProgress.val}/${this.batchTotal.val})`
|
|
||||||
}),
|
|
||||||
div({ class: 'progress' }, div({
|
|
||||||
class: 'progress-bar progress-bar-striped progress-bar-animated bg-primary',
|
|
||||||
style: () => `width: ${this.batchProgress.val / this.batchTotal.val * 100}%`
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Root() {
|
Root() {
|
||||||
const _that = this
|
const _that = this
|
||||||
return Route({
|
return Route({
|
||||||
@@ -91,8 +67,6 @@ export class TaskRoute implements VanComponent {
|
|||||||
return div(
|
return div(
|
||||||
() => _that.loading.val ? LoadingBox() : '',
|
() => _that.loading.val ? LoadingBox() : '',
|
||||||
() => div({ class: 'vstack gap-3', hidden: _that.loading.val },
|
() => div({ class: 'vstack gap-3', hidden: _that.loading.val },
|
||||||
// 固定在顶部的批量操作进度条
|
|
||||||
_that.BatchProgressBar(),
|
|
||||||
// 批量操作工具栏
|
// 批量操作工具栏
|
||||||
div({ class: 'hstack gap-2', hidden: () => _that.taskList.val.length == 0 },
|
div({ class: 'hstack gap-2', hidden: () => _that.taskList.val.length == 0 },
|
||||||
input({
|
input({
|
||||||
@@ -110,54 +84,29 @@ export class TaskRoute implements VanComponent {
|
|||||||
button({
|
button({
|
||||||
class: 'btn btn-sm btn-outline-danger',
|
class: 'btn btn-sm btn-outline-danger',
|
||||||
hidden: () => _that.selectedCancellable.val.length == 0,
|
hidden: () => _that.selectedCancellable.val.length == 0,
|
||||||
disabled: () => _that.batchInProgress.val,
|
|
||||||
onclick() {
|
onclick() {
|
||||||
if (!confirm(`确定要取消选中的 ${_that.selectedCancellable.val.length} 个任务吗?`)) return
|
if (!confirm(`确定要取消选中的 ${_that.selectedCancellable.val.length} 个任务吗?`)) return
|
||||||
const tasks = [..._that.selectedCancellable.val]
|
const ids = _that.selectedCancellable.val.map(t => t.id)
|
||||||
_that.batchOperation.val = 'cancel'
|
cancelTasks(ids).then(() => {
|
||||||
_that.batchTotal.val = tasks.length
|
_that.selectedCancellable.val.forEach(t => {
|
||||||
_that.batchProgress.val = 0
|
|
||||||
|
|
||||||
let completed = 0
|
|
||||||
tasks.forEach(t => {
|
|
||||||
cancelTask(t.id).then(() => {
|
|
||||||
t.statusState.val = 'error'
|
t.statusState.val = 'error'
|
||||||
t.selected.val = false
|
t.selected.val = false
|
||||||
}).finally(() => {
|
|
||||||
completed++
|
|
||||||
_that.batchProgress.val = completed
|
|
||||||
if (completed >= tasks.length) {
|
|
||||||
setTimeout(() => {
|
|
||||||
_that.batchTotal.val = 0
|
|
||||||
_that.batchProgress.val = 0
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
}).catch(error => {
|
||||||
|
alert(error.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, () => `批量取消 (${_that.selectedCancellable.val.length})`),
|
}, () => `批量取消 (${_that.selectedCancellable.val.length})`),
|
||||||
button({
|
button({
|
||||||
class: 'btn btn-sm btn-outline-secondary',
|
class: 'btn btn-sm btn-outline-secondary',
|
||||||
hidden: () => _that.selectedDeletable.val.length == 0,
|
hidden: () => _that.selectedDeletable.val.length == 0,
|
||||||
disabled: () => _that.batchInProgress.val,
|
|
||||||
onclick() {
|
onclick() {
|
||||||
if (!confirm(`确定要删除选中的 ${_that.selectedDeletable.val.length} 个任务吗?`)) return
|
if (!confirm(`确定要删除选中的 ${_that.selectedDeletable.val.length} 个任务吗?`)) return
|
||||||
const tasks = [..._that.selectedDeletable.val]
|
const ids = _that.selectedDeletable.val.map(t => t.id)
|
||||||
_that.batchOperation.val = 'delete'
|
|
||||||
_that.batchTotal.val = tasks.length
|
|
||||||
_that.batchProgress.val = 0
|
|
||||||
|
|
||||||
const ids = tasks.map(t => t.id)
|
|
||||||
deleteTasks(ids).then(() => {
|
deleteTasks(ids).then(() => {
|
||||||
_that.batchProgress.val = tasks.length
|
|
||||||
_that.taskList.val = _that.taskList.val.filter(taskInDB => !ids.includes(taskInDB.id))
|
_that.taskList.val = _that.taskList.val.filter(taskInDB => !ids.includes(taskInDB.id))
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
alert(error.message)
|
alert(error.message)
|
||||||
}).finally(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
_that.batchTotal.val = 0
|
|
||||||
_that.batchProgress.val = 0
|
|
||||||
}, 500)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, () => `批量删除 (${_that.selectedDeletable.val.length})`),
|
}, () => `批量删除 (${_that.selectedDeletable.val.length})`),
|
||||||
|
|||||||
Reference in New Issue
Block a user