This commit is contained in:
=
2026-06-29 14:33:31 +08:00
parent a9a118cd3e
commit 4bc74ff159
3 changed files with 221 additions and 36 deletions
@@ -2,7 +2,6 @@ package com.budwk.app.sys.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.result.Result;
@@ -10,6 +9,7 @@ import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService;
import org.nutz.integration.jedis.pubsub.PubSubService;
import org.nutz.ioc.loader.annotation.Inject;
@@ -22,10 +22,13 @@ import org.nutz.mvc.upload.UploadAdaptor;
import redis.clients.jedis.ScanParams;
import redis.clients.jedis.ScanResult;
import java.util.List;
@IocBean
@At("/platform/sys/h5ScanCodeUploadFile")
@Ok("json:full")
@Api(tags = "h5扫码上传文件")
@Slf4j
public class SysH5ScanCodeUploadFileController {
@Inject
@@ -55,6 +58,8 @@ public class SysH5ScanCodeUploadFileController {
String url = sysFileService.uploadReturnUrl(SysFileEngineTypeEnum.MINIO.getValue(), file);
return Result.success().addData(url);
} catch (Exception e) {
log.error("h5扫码上传文件失败,loginName={}, fileName={}, fileSize={}",
getCurrentLoginName(), getUploadFileName(file), getUploadFileSize(file), e);
return Result.error(e.getMessage());
}
}
@@ -63,26 +68,59 @@ public class SysH5ScanCodeUploadFileController {
@At
@SLog(tag = "文件管理", msg = "同步到PC")
@SaCheckLogin
public Result syncPc(@Param("files") JSONObject[] jsonObject, @Param("timestamp") String timestamp) {
public Result syncPc(@Param("files") String files, @Param("timestamp") String timestamp) {
try {
if(ObjectUtil.isEmpty(jsonObject)){
if(ObjectUtil.isEmpty(files)){
return Result.error("请选择要同步的文件");
}
if(ObjectUtil.isEmpty(timestamp)){
return Result.error("二维码已失效,请刷新电脑端二维码后重试");
}
// 手机端以表单字符串提交附件 JSON,后端显式解析,避免参数自动转换失败导致 500。
List<NutMap> fileList = Json.fromJsonAsList(NutMap.class, files);
if(ObjectUtil.isEmpty(fileList)){
return Result.error("请选择要同步的文件");
}
ScanParams match = new ScanParams().match(RedisConstant.REDIS_KEY_WSROOM + SecurityUtil.getUserLoginname() + ":*");
ScanResult<String> scan = null;
int sendCount = 0;
do {
scan = redisService.scan(scan == null ? ScanParams.SCAN_POINTER_START : scan.getStringCursor(), match);
for (String key : scan.getResult()) {
NutMap data = NutMap.NEW().addv("action", "h5-scan-code-upload-file-" + timestamp).addv("files", jsonObject);
NutMap data = NutMap.NEW().addv("action", "h5-scan-code-upload-file-" + timestamp).addv("files", fileList);
pubSubService.fire(key, Json.toJson(data));
sendCount++;
}
} while (!scan.isCompleteIteration());
if(sendCount == 0){
return Result.error("未找到电脑端上传窗口,请保持电脑端二维码页面打开");
}
return Result.success().addData(null);
} catch (Exception e) {
log.error("h5扫码上传文件同步到PC失败,loginName={}, timestamp={}, filesLength={}",
getCurrentLoginName(), timestamp, ObjectUtil.isEmpty(files) ? 0 : files.length(), e);
return Result.error(e.getMessage());
}
}
private String getCurrentLoginName() {
try {
return SecurityUtil.getUserLoginname();
} catch (Exception e) {
return "unknown";
}
}
private String getUploadFileName(TempFile file) {
return file == null ? "" : file.getSubmittedFileName();
}
private long getUploadFileSize(TempFile file) {
return file == null ? 0 : file.getSize();
}
}
@@ -6,6 +6,7 @@
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview"
:on-exceed="handleExceed"
:before-upload="beforeUpload"
@@ -26,6 +27,7 @@
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-upload="beforeUpload"
@@ -67,6 +69,7 @@
:action="action"
:on-change="handleChange"
:on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview"
:on-remove="handleRemove"
:on-exceed="handleExceed"
@@ -205,15 +208,15 @@ module.exports = {
let tips = null
if (uploadNumber) {
tips = `只能上传<span style="color: red">${uploadNumber}</span>个文件;`
tips = '只能上传<span style="color: red">' + uploadNumber + '</span>个文件;'
}
if (fileAccept) {
tips = tips + `只能上传<span style="color: red">${fileAccept}</span>文件;`
tips = tips + '只能上传<span style="color: red">' + fileAccept + '</span>文件;'
} else {
tips = tips + '不限格式;'
}
if (fileSize) {
tips = tips + `单个文件大小不能超过<span style="color: red">${(fileSize / 1024 / 1024).toFixed(2)}</span>M`
tips = tips + '单个文件大小不能超过<span style="color: red">' + (fileSize / 1024 / 1024).toFixed(2) + '</span>M'
}
return tips
},
@@ -341,7 +344,7 @@ module.exports = {
if (valid) {
return true
}
this.$message.error(`文件只能是 ${this.fileAccept} 格式!`)
this.$message.error("文件只能是 " + this.fileAccept + " 格式!")
return false
},
@@ -441,15 +444,15 @@ module.exports = {
let result = []
if (response && response.code === 0 && file.status === "success") {
fileList.forEach((f) => {
if(f.response.data?.includes('webvpn')) {
if(f.response && f.response.data && f.response.data.includes('webvpn')) {
const index = f.response.data.indexOf('/platform')
f.response.data = f.response.data.substring(index)
this.$set(f.response, "data", f.response.data.substring(index))
}
result.push(f)
})
} else {
this.$message.error(response.msg)
fileList = fileList.splice(file, 1)
this.$message.error(response && response.msg ? response.msg : "文件上传失败,请稍后重试")
fileList = this.filterFileList(fileList, file)
}
if (result.length > 0) {
if (this.upload_result_category === "interval") {
@@ -484,6 +487,40 @@ module.exports = {
this.$emit("update:value", fileList)
},
handleError(error, file, fileList) {
this.$message.error(this.getUploadErrorMessage(error))
this.$emit("update:value", this.filterFileList(fileList, file))
},
filterFileList(fileList, file) {
if (!Array.isArray(fileList)) {
return []
}
return fileList.filter((item) => {
return !file || item.uid !== file.uid
})
},
getUploadErrorMessage(error) {
if (error && error.response && error.response.data && error.response.data.msg) {
return error.response.data.msg
}
if (error && error.status) {
return "文件上传失败,服务器返回" + error.status
}
return "文件上传失败,请检查文件后重试"
},
buildQrCodeAddress(timestamp) {
const currentLength = Array.isArray(this.fileList) ? this.fileList.length : 0
const remainNumber = this.upload_number ? Math.max(this.upload_number - currentLength, 0) : 0
let address = origin + "/platform/sys/h5ScanCodeUploadFile?timestamp=" + encodeURIComponent(timestamp)
address = address + "&upload_number=" + encodeURIComponent(remainNumber)
address = address + "&upload_size=" + encodeURIComponent(this.upload_size || "")
address = address + "&accept=" + encodeURIComponent(this.fileAccept || "")
return address
},
// 通过DOM获取上传的文件
uploadFileList() {
if (this.fileList) {
@@ -546,7 +583,7 @@ module.exports = {
mounted() {
const timestamp = new Date().getTime()
this.qrCodeAddress = origin + "/platform/sys/h5ScanCodeUploadFile?timestamp=" + timestamp
this.$set(this, "qrCodeAddress", this.buildQrCodeAddress(timestamp))
console.info("qrCodeAddress:", this.qrCodeAddress)
// 可选:轮询或 watch ready 状态
const checkReady = () => {
@@ -588,7 +625,7 @@ module.exports = {
if (validFiles.length < data.files.length) {
data.files = validFiles
this.$message.error(`文件只能是 ${this.fileAccept} 格式,已自动过滤掉不符合要求文件!`)
this.$message.error("文件只能是 " + this.fileAccept + " 格式,已自动过滤掉不符合要求文件!")
}
}
@@ -44,6 +44,13 @@
text-align: center;
}
.upload-tip {
margin-top: 12px;
color: #666;
font-size: 13px;
line-height: 1.6;
}
.van-uploader__tip {
color: var(--color-primary);
font-size: 14px;
@@ -78,8 +85,9 @@
></van-uploader>
<div class="upload-button">
<van-button block type="primary" @click="onSubmit">同步到电脑</van-button>
<van-button block type="primary" :disabled="submitting" @click="onSubmit">同步到电脑</van-button>
</div>
<div class="upload-tip">{{ uploadTips }}</div>
</div>
</body>
<script nonce="${cspNonce!}">
@@ -93,22 +101,97 @@
// accept: "image/*",
accept: "*",
upload_number: 9,
upload_size: 1024 * 1024 * 10,
submitting: false,
}
},
computed: {
uploadTips() {
const sizeText = (this.upload_size / 1024 / 1024).toFixed(2)
const acceptText = this.accept && this.accept !== "*" ? this.accept : "不限格式"
return "最多选择" + this.upload_number + "个文件,支持" + acceptText + ",单个文件不超过" + sizeText + "M"
}
},
created() {
this.initUploadConfig()
},
methods: {
GetQueryString(name) {
return new URLSearchParams(window.location.search).get(name)
},
initUploadConfig() {
const accept = this.GetQueryString("accept")
const uploadNumber = parseInt(this.GetQueryString("upload_number"), 10)
const uploadSize = parseInt(this.GetQueryString("upload_size"), 10)
if (accept) {
this.$set(this, "accept", accept)
}
if (!isNaN(uploadNumber)) {
this.$set(this, "upload_number", uploadNumber)
}
if (!isNaN(uploadSize) && uploadSize > 0) {
this.$set(this, "upload_size", uploadSize)
}
},
beforeRead(file) {
const files = Array.isArray(file) ? file : [file]
if (this.fileList.length + files.length > this.upload_number) {
this.$toast.fail("最多只能上传" + this.upload_number + "个文件")
return false
}
for (let i = 0; i < files.length; i++) {
const validMsg = this.validateFile(files[i])
if (validMsg) {
this.$toast.fail(validMsg)
return false
}
}
return true
},
afterRead(file) {},
buildUploadPromises(file) {
return
validateFile(file) {
if (!file) {
return "请选择要上传的文件"
}
if (file.size > this.upload_size) {
return "文件过大,请上传小于" + (this.upload_size / 1024 / 1024).toFixed(2) + "M的文件"
}
if (!this.accept || this.accept === "*") {
return ""
}
const fileName = file.name || ""
const dotIndex = fileName.lastIndexOf(".")
if (dotIndex === -1) {
return "文件必须包含有效的扩展名"
}
const fileType = fileName.substring(dotIndex).toUpperCase()
const acceptTypes = this.accept
.trim()
.split(",")
.map((type) => type.trim().toUpperCase())
if (acceptTypes.indexOf(fileType) === -1) {
return "文件只能是 " + this.accept + " 格式"
}
return ""
},
onSubmit() {
if (this.fileList.length === 0) {
this.$toast("请先选择文件")
return
}
const pendingFiles = this.fileList.filter((f) => {
return f.file
})
if (pendingFiles.length === 0) {
this.syncPc()
return
}
this.$set(this, "submitting", true)
const loading = this.$toast({
duration: 0,
overlay: true,
@@ -118,37 +201,39 @@
})
Promise.all(
this.fileList.map((f) => {
pendingFiles.map((f) => {
const formData = new FormData()
formData.append("file", f.file)
return axios.post("/platform/sys/h5ScanCodeUploadFile/upload", formData).then((axiosResp) => {
const resp = axiosResp.data
if (resp.code === 0) {
console.log(resp)
f.name = f.file.name
f.size = f.file.size
f.status = null
f.url = resp.data
f.response = resp
f.percentage = 100
f.isImage = true
this.$set(f, "name", f.file.name)
this.$set(f, "size", f.file.size)
this.$set(f, "status", null)
this.$set(f, "url", resp.data)
this.$set(f, "response", resp)
this.$set(f, "percentage", 100)
this.$set(f, "isImage", this.isImageFile(f.file.name))
delete f.file
delete f.content
console.log(f)
} else {
f.status = "fail"
this.$set(f, "status", "fail")
return Promise.reject(new Error(resp.msg || "文件上传失败"))
}
}).catch((error) => {
this.$set(f, "status", "fail")
return Promise.reject(error)
})
})
)
.then(() => {
loading.close()
console.log(this.fileList)
this.syncPc()
})
.catch((error) => {
loading.close()
this.$toast.fail(error)
this.$set(this, "submitting", false)
this.$toast.fail(this.getErrorMessage(error, "文件上传失败"))
})
},
@@ -157,15 +242,16 @@
this.$toast('请先选择文件')
return
}
console.log(this.fileList)
this.$set(this, "submitting", true)
const params = new URLSearchParams()
params.append("files", JSON.stringify(this.fileList))
params.append("timestamp", this.GetQueryString("timestamp") || "")
axios
.post(
"/platform/sys/h5ScanCodeUploadFile/syncPc",
{
files: JSON.stringify(this.fileList),
timestamp: this.GetQueryString("timestamp")
},
params.toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
@@ -176,9 +262,33 @@
if (resp.data.code === 0) {
this.$toast.success("上传成功,请关注电脑端信息")
} else {
this.$toast.fail(resp.data.msg)
this.$toast.fail(resp.data.msg || "同步到电脑失败")
}
this.$set(this, "submitting", false)
})
.catch((error) => {
this.$toast.fail(this.getErrorMessage(error, "同步到电脑失败"))
this.$set(this, "submitting", false)
})
},
isImageFile(fileName) {
const name = fileName || ""
const suffix = name.substring(name.lastIndexOf(".") + 1).toLowerCase()
return ["jpg", "jpeg", "png", "gif", "svg"].indexOf(suffix) !== -1
},
getErrorMessage(error, defaultMsg) {
if (error && error.response && error.response.data && error.response.data.msg) {
return error.response.data.msg
}
if (error && error.message && error.message !== "Network Error") {
return defaultMsg + "" + error.message
}
if (error && error.response && error.response.status) {
return defaultMsg + ",服务器返回" + error.response.status
}
return defaultMsg + ",请稍后重试"
}
}
})