This commit is contained in:
xh
2025-07-24 02:05:23 +08:00
parent 41ff5171d6
commit 717438e97f
9 changed files with 160 additions and 63 deletions

View File

@@ -3,7 +3,6 @@ import axios from 'axios'
export interface FileUploaderOptions {
chunkSize?: number
fileName?: string
onSuccess?: (filePath: string) => void
onError?: (error: Error) => void
}
@@ -16,41 +15,65 @@ export default class FileUploader {
chunkSize: number = 1024 * 1024 // 1MB
chunkCount: number = 0
private uploading = false
private startChunkIndex = -1
onSuccess: FileUploaderOptions['onSuccess'] = () => {}
onError: FileUploaderOptions['onError'] = (error: Error) => {
console.log(error)
// 上传完成
success(filePath: string) {
this.uploading = false
this.startChunkIndex = -1
this.onSuccess(filePath)
}
onUploadProgress(chunkIndex: number, chunkLoaded: number, chunkTotal: number, loaded: number) {
onSuccess: FileUploaderOptions['onSuccess'] = (filePath) => {
console.log('上传完成', filePath)
}
error(error: Error) {
this.uploading = false
this.startChunkIndex = -1
this.onError(error)
}
onError: FileUploaderOptions['onError'] = (error: Error) => {
console.log('error', error)
}
onUploadProgress(chunkIndex: number, chunkLoaded: number, chunkTotal: number) {
console.log(
`当前分片: ${chunkIndex}/${this.chunkCount},分片进度${chunkLoaded}/${chunkTotal},总进度: ${loaded}/${this.fileSize}`
`当前分片: ${chunkIndex}/${this.chunkCount},分片进度${chunkLoaded}/${chunkTotal}}`
)
}
constructor(file: File, options: FileUploaderOptions) {
this.file = file
this.fileName = file.name
this.fileSize = file.size
constructor(options: FileUploaderOptions, file?: File) {
if (options?.chunkSize) {
this.chunkSize = options.chunkSize
}
this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
if (options?.fileName) {
this.fileName = options.fileName
}
if (options?.onSuccess) {
this.onSuccess = options.onSuccess
}
if (options?.onError) {
this.onError = options.onError
}
if (file) {
this.loadFile(file)
}
}
public loadFile(file: File, fileName?: string) {
if (this.uploading) {
this.error(new Error('请等待上一个文件上传完成'))
return
}
this.file = file
if (fileName) {
this.fileName = fileName
} else {
this.fileName = file.name
}
this.fileSize = file.size
this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
}
readerFile(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
if (!file) {
return reject(new Error('文件不存在'))
return reject(new Error('读取文件失败'))
}
const reader = new FileReader()
reader.onload = (e) => {
@@ -62,35 +85,39 @@ export default class FileUploader {
reader.readAsArrayBuffer(file)
})
}
// 开始/恢复上传
async start() {
// 开始上传
public async start() {
try {
if (!this.file) {
this.error(new Error('请选择文件后上传'))
return
}
if (this.uploading) {
this.error(new Error('正在上传中'))
return
}
this.uploading = true
const arrayBuffer = await this.readerFile(this.file)
const fileMd5 = this.getMd5(arrayBuffer)
this.fileMd5 = fileMd5 + '_' + this.fileSize
const isExistFilePath = await this.checkFileExist()
if (isExistFilePath) {
this.complete(isExistFilePath)
this.success(isExistFilePath)
return
}
const hasChunk = await this.getHasChunk()
this.startChunkIndex = hasChunk && hasChunk.length ? Math.max(...hasChunk) : 0
this.startChunkIndex = hasChunk && hasChunk.length ? Math.max(...hasChunk) : -1
console.log('hasChunk', hasChunk)
await this.splitChunks()
await this.mergeChunk()
} catch (error) {
this.onError(error)
this.error(error)
}
}
// 上传完成
complete(filePath) {
/* 合并分片 */
console.log('complete:', filePath)
this.onSuccess(filePath)
}
// 检查上传状态
getMd5(arrayBuffer: ArrayBuffer): string {
console.time('SparkMD5')
@@ -131,7 +158,7 @@ export default class FileUploader {
}
async splitChunks() {
for (let index = this.startChunkIndex; index < this.chunkCount; index++) {
for (let index = this.startChunkIndex + 1; index < this.chunkCount; index++) {
const chunkStart = index * this.chunkSize
const chunkEnd = Math.min(chunkStart + this.chunkSize, this.file.size)
const chunk = this.file.slice(chunkStart, chunkEnd)
@@ -152,9 +179,9 @@ export default class FileUploader {
// ((this.chunkSize * index + progressEvent.loaded) * 100) /
// this.fileSize
// ).toFixed(3)
const loaded = this.chunkSize * index + progressEvent.loaded //TODO progressEvent.loaded体积比文件大不能直接相加
// const loaded = this.chunkSize * index + progressEvent.loaded //TODO progressEvent.loaded体积比文件大不能直接相加
this.onUploadProgress(index, progressEvent.loaded, progressEvent.total, loaded)
this.onUploadProgress(index, progressEvent.loaded, progressEvent.total)
}
})
chunk = null
@@ -169,7 +196,7 @@ export default class FileUploader {
} catch (error) {
chunk = null
console.error(`分片 ${index + 1}/${this.chunkCount} 上传失败: ${error}`)
this.onError(error)
this.error(error)
}
}
async mergeChunk() {
@@ -182,14 +209,14 @@ export default class FileUploader {
})
if (res.data.code === 200) {
console.log('合并分片成功')
this.complete(res.data.data)
this.success(res.data.data)
} else {
console.log('MergeChunk', res)
this.onError(new Error('合并分片失败'))
this.error(new Error('合并分片失败'))
}
} catch (error) {
console.error(`合并分片失败: ${error}`)
this.onError(error)
this.error(error)
}
}
}

View File

@@ -8,17 +8,27 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import FileUploader from '@/utils/FileUploader'
const fileInput = ref<HTMLInputElement>()
let fileUploader: FileUploader | null = null
const fileUploader = new FileUploader({
chunkSize: 1024 * 1024 * 1,
onSuccess(filePath) {
ElMessage.success('上传成功:' + filePath)
},
onError(error) {
// console.error('error', error)
ElMessage.error(error.message)
}
})
function handleChange(e) {
const files = (e.target as HTMLInputElement).files
// console.log('e.target', e.target)
console.log('files', files)
if (files) {
fileUploader = new FileUploader(files[0], { chunkSize: 1024 * 1024 * 1 })
fileUploader.loadFile(files[0])
}
}
function btn() {

1
server/.gitignore vendored
View File

@@ -58,3 +58,4 @@ main
# air
tmp
dist/
uploads/

View File

@@ -40,26 +40,42 @@ var AdminConfig = adminConfig{
},
// 管理员账号id
SuperAdminId: 1,
ReqAdminIdKey: "admin_id",
ReqRoleIdKey: "role",
SuperAdminId: 1,
// 管理员账号key
ReqAdminIdKey: "admin_id",
// 角色key
ReqRoleIdKey: "role",
// 用户名key
ReqUsernameKey: "username",
// 昵称key
ReqNicknameKey: "nickname",
}
type adminConfig struct {
// 管理缓存键
BackstageManageKey string
BackstageRolesKey string
BackstageTokenKey string
BackstageTokenSet string
NotLoginUri []string
NotAuthUri []string
ShowWhitelistUri []string
SuperAdminId uint
ReqAdminIdKey string
ReqRoleIdKey string
ReqUsernameKey string
ReqNicknameKey string
// 角色缓存键
BackstageRolesKey string
// 令牌缓存键
BackstageTokenKey string
// 令牌的集合
BackstageTokenSet string
// 免登录验证
NotLoginUri []string
// 免权限验证
NotAuthUri []string
// 演示模式白名单
ShowWhitelistUri []string
// 管理员账号id
SuperAdminId uint
// 管理员账号key
ReqAdminIdKey string
// 角色key
ReqRoleIdKey string
// 用户名key
ReqUsernameKey string
// 昵称key
ReqNicknameKey string
}
func (cnf adminConfig) GetAdminId(c *gin.Context) uint {

View File

@@ -3,6 +3,7 @@ package commonController
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"x_admin/core/response"
@@ -33,8 +34,11 @@ type uploadChunkHandler struct {
tmpPath string
}
func (uh uploadChunkHandler) getFilePath(fileMd5 string) string {
return fmt.Sprintf("%s/%s", uh.uploadPath, fileMd5)
func (uh uploadChunkHandler) getFilePath(fileMd5 string, fileName string) string {
// 获取文件后缀
ext := filepath.Ext(fileName)
return fmt.Sprintf("%s/%s%s", uh.uploadPath, fileMd5, ext)
}
func (uh uploadChunkHandler) getChunkDir(fileMd5 string, chunkSize string) string {
return fmt.Sprintf("%s/%s_%s", uh.tmpPath, fileMd5, chunkSize)
@@ -91,6 +95,15 @@ func (uh uploadChunkHandler) CheckFileExist(c *gin.Context) {
".gz",
".bz2",
".xz",
".msi",
".exe",
".dmg",
".iso",
".app",
".deb",
".rpm",
".pkg",
".apk",
}
var fileExt = ""
for _, ext := range whiteList {
@@ -111,7 +124,7 @@ func (uh uploadChunkHandler) CheckFileExist(c *gin.Context) {
response.FailWithMsg(c, response.SystemError, "文件hash错误")
return
}
var filePath = uh.getFilePath(fileMd5)
var filePath = uh.getFilePath(fileMd5, fileName)
// 检查文件是否存在
if commonService.UploadChunkService.CheckFileExist(filePath) {
response.OkWithData(c, filePath)
@@ -209,7 +222,7 @@ func (uh uploadChunkHandler) MergeChunk(c *gin.Context) {
response.FailWithMsg(c, response.SystemError, "分片大小错误")
return
}
var filePath = uh.getFilePath(MergeChunk.FileMd5)
var filePath = uh.getFilePath(MergeChunk.FileMd5, MergeChunk.FileName)
var chunkDir = uh.getChunkDir(MergeChunk.FileMd5, fmt.Sprintf("%d", MergeChunk.ChunkSize))
err := commonService.UploadChunkService.MergeChunk(chunkDir, filePath, MergeChunk.ChunkCount)
if err != nil {

View File

@@ -27,23 +27,30 @@ type Response struct {
}
var (
// code 200成功
Success = RespType{code: 200, message: "成功"}
Failed = RespType{code: 300, message: "失败"}
// code 300失败
Failed = RespType{code: 300, message: "失败"}
// code 310参数校验错误
ParamsValidError = RespType{code: 310, message: "参数校验错误"}
// code 311参数类型错误
ParamsTypeError = RespType{code: 311, message: "参数类型错误"}
ParamsValidError = RespType{code: 310, message: "参数校验错误"}
ParamsTypeError = RespType{code: 311, message: "参数类型错误"}
RequestMethodError = RespType{code: 312, message: "请求方法错误"}
AssertArgumentError = RespType{code: 313, message: "断言参数错误"}
// code 330登录账号或密码错误
LoginAccountError = RespType{code: 330, message: "登录账号或密码错误"}
// code 331登录账号已被禁用了
LoginDisableError = RespType{code: 331, message: "登录账号已被禁用了"}
TokenEmpty = RespType{code: 332, message: "token参数为空"}
TokenInvalid = RespType{code: 333, message: "登录失效"}
// code 332 token参数为空
TokenEmpty = RespType{code: 332, message: "token参数为空"}
// code 333 登录失效
TokenInvalid = RespType{code: 333, message: "登录失效"}
// 无相关权限
NoPermission = RespType{code: 403, message: "无相关权限"}
Request404Error = RespType{code: 404, message: "请求接口不存在"}
Request405Error = RespType{code: 405, message: "请求方法不允许"}
// code 500系统错误
SystemError = RespType{code: 500, message: "系统错误"}
)

View File

@@ -111,7 +111,7 @@ func TokenAuth() gin.HandlerFunc {
c.Set(config.AdminConfig.ReqNicknameKey, mapping.Nickname)
// 免权限验证接口
if util.ToolsUtil.Contains(config.AdminConfig.NotAuthUri, auths) || uid == 1 {
if util.ToolsUtil.Contains(config.AdminConfig.NotAuthUri, auths) || uid == config.AdminConfig.SuperAdminId {
c.Next()
return
}

View File

@@ -2,6 +2,7 @@ package util
import (
"bytes"
"regexp"
"strings"
"unicode"
@@ -49,3 +50,9 @@ func (su stringUtil) ToUpperCamelCase(s string) string {
}
return strings.Join(words, "")
}
// 检查字符串只能包含字母、数字和下划线
func (su stringUtil) CheckSafeString(s string) bool {
reg := regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
return !reg.MatchString(s)
}

View File

@@ -0,0 +1,16 @@
package util
import (
"testing"
)
func TestCheckSafeString(t *testing.T) {
// 测试正常字符串
if StringUtil.CheckSafeString("abc123") {
t.Log("正常字符串")
}
// 测试包含特殊字符的字符串
if !StringUtil.CheckSafeString("abc123!") {
t.Log("包含特殊字符的字符串")
}
}