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.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import com.budwk.app.base.annotation.SLog; import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.constant.RedisConstant; import com.budwk.app.base.constant.RedisConstant;
import com.budwk.app.base.result.Result; 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.sys.services.SysFileService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.nutz.integration.jedis.RedisService; import org.nutz.integration.jedis.RedisService;
import org.nutz.integration.jedis.pubsub.PubSubService; import org.nutz.integration.jedis.pubsub.PubSubService;
import org.nutz.ioc.loader.annotation.Inject; 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.ScanParams;
import redis.clients.jedis.ScanResult; import redis.clients.jedis.ScanResult;
import java.util.List;
@IocBean @IocBean
@At("/platform/sys/h5ScanCodeUploadFile") @At("/platform/sys/h5ScanCodeUploadFile")
@Ok("json:full") @Ok("json:full")
@Api(tags = "h5扫码上传文件") @Api(tags = "h5扫码上传文件")
@Slf4j
public class SysH5ScanCodeUploadFileController { public class SysH5ScanCodeUploadFileController {
@Inject @Inject
@@ -55,6 +58,8 @@ public class SysH5ScanCodeUploadFileController {
String url = sysFileService.uploadReturnUrl(SysFileEngineTypeEnum.MINIO.getValue(), file); String url = sysFileService.uploadReturnUrl(SysFileEngineTypeEnum.MINIO.getValue(), file);
return Result.success().addData(url); return Result.success().addData(url);
} catch (Exception e) { } catch (Exception e) {
log.error("h5扫码上传文件失败,loginName={}, fileName={}, fileSize={}",
getCurrentLoginName(), getUploadFileName(file), getUploadFileSize(file), e);
return Result.error(e.getMessage()); return Result.error(e.getMessage());
} }
} }
@@ -63,26 +68,59 @@ public class SysH5ScanCodeUploadFileController {
@At @At
@SLog(tag = "文件管理", msg = "同步到PC") @SLog(tag = "文件管理", msg = "同步到PC")
@SaCheckLogin @SaCheckLogin
public Result syncPc(@Param("files") JSONObject[] jsonObject, @Param("timestamp") String timestamp) { public Result syncPc(@Param("files") String files, @Param("timestamp") String timestamp) {
try { 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("请选择要同步的文件"); return Result.error("请选择要同步的文件");
} }
ScanParams match = new ScanParams().match(RedisConstant.REDIS_KEY_WSROOM + SecurityUtil.getUserLoginname() + ":*"); ScanParams match = new ScanParams().match(RedisConstant.REDIS_KEY_WSROOM + SecurityUtil.getUserLoginname() + ":*");
ScanResult<String> scan = null; ScanResult<String> scan = null;
int sendCount = 0;
do { do {
scan = redisService.scan(scan == null ? ScanParams.SCAN_POINTER_START : scan.getStringCursor(), match); scan = redisService.scan(scan == null ? ScanParams.SCAN_POINTER_START : scan.getStringCursor(), match);
for (String key : scan.getResult()) { 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)); pubSubService.fire(key, Json.toJson(data));
sendCount++;
} }
} while (!scan.isCompleteIteration()); } while (!scan.isCompleteIteration());
if(sendCount == 0){
return Result.error("未找到电脑端上传窗口,请保持电脑端二维码页面打开");
}
return Result.success().addData(null); return Result.success().addData(null);
} catch (Exception e) { } catch (Exception e) {
log.error("h5扫码上传文件同步到PC失败,loginName={}, timestamp={}, filesLength={}",
getCurrentLoginName(), timestamp, ObjectUtil.isEmpty(files) ? 0 : files.length(), e);
return Result.error(e.getMessage()); 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" :action="action"
:on-change="handleChange" :on-change="handleChange"
:on-success="handleSuccess" :on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview" :on-preview="handlePreview"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:before-upload="beforeUpload" :before-upload="beforeUpload"
@@ -26,6 +27,7 @@
:action="action" :action="action"
:on-change="handleChange" :on-change="handleChange"
:on-success="handleSuccess" :on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview" :on-preview="handlePreview"
:on-remove="handleRemove" :on-remove="handleRemove"
:before-upload="beforeUpload" :before-upload="beforeUpload"
@@ -67,6 +69,7 @@
:action="action" :action="action"
:on-change="handleChange" :on-change="handleChange"
:on-success="handleSuccess" :on-success="handleSuccess"
:on-error="handleError"
:on-preview="handlePreview" :on-preview="handlePreview"
:on-remove="handleRemove" :on-remove="handleRemove"
:on-exceed="handleExceed" :on-exceed="handleExceed"
@@ -205,15 +208,15 @@ module.exports = {
let tips = null let tips = null
if (uploadNumber) { if (uploadNumber) {
tips = `只能上传<span style="color: red">${uploadNumber}</span>个文件;` tips = '只能上传<span style="color: red">' + uploadNumber + '</span>个文件;'
} }
if (fileAccept) { if (fileAccept) {
tips = tips + `只能上传<span style="color: red">${fileAccept}</span>文件;` tips = tips + '只能上传<span style="color: red">' + fileAccept + '</span>文件;'
} else { } else {
tips = tips + '不限格式;' tips = tips + '不限格式;'
} }
if (fileSize) { 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 return tips
}, },
@@ -341,7 +344,7 @@ module.exports = {
if (valid) { if (valid) {
return true return true
} }
this.$message.error(`文件只能是 ${this.fileAccept} 格式!`) this.$message.error("文件只能是 " + this.fileAccept + " 格式!")
return false return false
}, },
@@ -441,15 +444,15 @@ module.exports = {
let result = [] let result = []
if (response && response.code === 0 && file.status === "success") { if (response && response.code === 0 && file.status === "success") {
fileList.forEach((f) => { 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') 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) result.push(f)
}) })
} else { } else {
this.$message.error(response.msg) this.$message.error(response && response.msg ? response.msg : "文件上传失败,请稍后重试")
fileList = fileList.splice(file, 1) fileList = this.filterFileList(fileList, file)
} }
if (result.length > 0) { if (result.length > 0) {
if (this.upload_result_category === "interval") { if (this.upload_result_category === "interval") {
@@ -484,6 +487,40 @@ module.exports = {
this.$emit("update:value", fileList) 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获取上传的文件 // 通过DOM获取上传的文件
uploadFileList() { uploadFileList() {
if (this.fileList) { if (this.fileList) {
@@ -546,7 +583,7 @@ module.exports = {
mounted() { mounted() {
const timestamp = new Date().getTime() 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) console.info("qrCodeAddress:", this.qrCodeAddress)
// 可选:轮询或 watch ready 状态 // 可选:轮询或 watch ready 状态
const checkReady = () => { const checkReady = () => {
@@ -588,7 +625,7 @@ module.exports = {
if (validFiles.length < data.files.length) { if (validFiles.length < data.files.length) {
data.files = validFiles data.files = validFiles
this.$message.error(`文件只能是 ${this.fileAccept} 格式,已自动过滤掉不符合要求文件!`) this.$message.error("文件只能是 " + this.fileAccept + " 格式,已自动过滤掉不符合要求文件!")
} }
} }
@@ -44,6 +44,13 @@
text-align: center; text-align: center;
} }
.upload-tip {
margin-top: 12px;
color: #666;
font-size: 13px;
line-height: 1.6;
}
.van-uploader__tip { .van-uploader__tip {
color: var(--color-primary); color: var(--color-primary);
font-size: 14px; font-size: 14px;
@@ -78,8 +85,9 @@
></van-uploader> ></van-uploader>
<div class="upload-button"> <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>
<div class="upload-tip">{{ uploadTips }}</div>
</div> </div>
</body> </body>
<script nonce="${cspNonce!}"> <script nonce="${cspNonce!}">
@@ -93,22 +101,97 @@
// accept: "image/*", // accept: "image/*",
accept: "*", accept: "*",
upload_number: 9, 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: { methods: {
GetQueryString(name) { GetQueryString(name) {
return new URLSearchParams(window.location.search).get(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) { 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 return true
}, },
afterRead(file) {}, afterRead(file) {},
buildUploadPromises(file) { validateFile(file) {
return 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() { 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({ const loading = this.$toast({
duration: 0, duration: 0,
overlay: true, overlay: true,
@@ -118,37 +201,39 @@
}) })
Promise.all( Promise.all(
this.fileList.map((f) => { pendingFiles.map((f) => {
const formData = new FormData() const formData = new FormData()
formData.append("file", f.file) formData.append("file", f.file)
return axios.post("/platform/sys/h5ScanCodeUploadFile/upload", formData).then((axiosResp) => { return axios.post("/platform/sys/h5ScanCodeUploadFile/upload", formData).then((axiosResp) => {
const resp = axiosResp.data const resp = axiosResp.data
if (resp.code === 0) { if (resp.code === 0) {
console.log(resp) this.$set(f, "name", f.file.name)
f.name = f.file.name this.$set(f, "size", f.file.size)
f.size = f.file.size this.$set(f, "status", null)
f.status = null this.$set(f, "url", resp.data)
f.url = resp.data this.$set(f, "response", resp)
f.response = resp this.$set(f, "percentage", 100)
f.percentage = 100 this.$set(f, "isImage", this.isImageFile(f.file.name))
f.isImage = true
delete f.file delete f.file
delete f.content delete f.content
console.log(f)
} else { } 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(() => { .then(() => {
loading.close() loading.close()
console.log(this.fileList)
this.syncPc() this.syncPc()
}) })
.catch((error) => { .catch((error) => {
loading.close() loading.close()
this.$toast.fail(error) this.$set(this, "submitting", false)
this.$toast.fail(this.getErrorMessage(error, "文件上传失败"))
}) })
}, },
@@ -157,15 +242,16 @@
this.$toast('请先选择文件') this.$toast('请先选择文件')
return 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 axios
.post( .post(
"/platform/sys/h5ScanCodeUploadFile/syncPc", "/platform/sys/h5ScanCodeUploadFile/syncPc",
{ params.toString(),
files: JSON.stringify(this.fileList),
timestamp: this.GetQueryString("timestamp")
},
{ {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
@@ -176,9 +262,33 @@
if (resp.data.code === 0) { if (resp.data.code === 0) {
this.$toast.success("上传成功,请关注电脑端信息") this.$toast.success("上传成功,请关注电脑端信息")
} else { } 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 + ",请稍后重试"
} }
} }
}) })