This commit is contained in:
wuweihua
2026-10-22 14:46:57 +08:00
7 changed files with 100 additions and 13 deletions
@@ -11,6 +11,7 @@ import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.bms.common.config.BmsConfig; import com.bms.common.config.BmsConfig;
@@ -18,6 +19,7 @@ import com.bms.common.core.domain.AjaxResult;
import com.bms.common.utils.StringUtils; import com.bms.common.utils.StringUtils;
import com.bms.common.utils.file.FileUploadUtils; import com.bms.common.utils.file.FileUploadUtils;
import com.bms.common.utils.file.FileUtils; import com.bms.common.utils.file.FileUtils;
import com.bms.common.utils.file.MimeTypeUtils;
import com.bms.common.utils.file.OssStorage; import com.bms.common.utils.file.OssStorage;
import com.bms.framework.config.ServerConfig; import com.bms.framework.config.ServerConfig;
@@ -98,6 +100,56 @@ public class CommonController
} }
} }
/**
* 通用上传请求(OSS 存储,返回 oss:// 存储标识)。
* 客户附件、公司主体、收款凭证等业务附件走此接口;
* 下载统一经 /common/download/resource?resource=oss://... 鉴权转发。
*/
@PostMapping("/upload/oss")
public AjaxResult uploadFileOss(MultipartFile file,
@RequestParam(value = "module", required = false) String module) throws Exception
{
try
{
if (file == null || file.isEmpty())
{
return AjaxResult.error("上传文件不能为空");
}
String originalFilename = file.getOriginalFilename();
// 扩展名白名单与大小校验(默认 50MB,图片/文档/压缩包/视频/pdf)
FileUploadUtils.assertAllowed(file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
String extension = "";
int dotIndex = originalFilename != null ? originalFilename.lastIndexOf('.') : -1;
if (dotIndex >= 0)
{
extension = originalFilename.substring(dotIndex + 1).toLowerCase();
}
// module 仅允许字母数字下划线中划线,防止对象 key 注入
String moduleKey = StringUtils.defaultIfBlank(module, "misc").replaceAll("[^a-zA-Z0-9_-]", "");
if (moduleKey.isEmpty())
{
moduleKey = "misc";
}
String datePath = new java.text.SimpleDateFormat("yyyy/MM/dd").format(new java.util.Date());
String objectKey = "attachment/" + moduleKey + "/" + datePath + "/"
+ java.util.UUID.randomUUID().toString().replace("-", "") + "." + extension;
String storageKey = ossStorage.upload(file.getInputStream(), file.getSize(), objectKey, file.getContentType());
String downloadUrl = "/common/download/resource?resource="
+ java.net.URLEncoder.encode(storageKey, java.nio.charset.StandardCharsets.UTF_8);
AjaxResult ajax = AjaxResult.success();
ajax.put("url", downloadUrl);
ajax.put("fileName", storageKey);
ajax.put("newFileName", originalFilename);
ajax.put("originalFilename", originalFilename);
return ajax;
}
catch (Exception e)
{
log.error("upload file to oss failed", e);
return AjaxResult.error(e.getMessage());
}
}
/** /**
* 通用上传请求(多个) * 通用上传请求(多个)
*/ */
@@ -29,6 +29,13 @@ public class OssStorage {
/** 使用文件流上传,避免把整个账单读入内存。返回可持久化的存储标识。 */ /** 使用文件流上传,避免把整个账单读入内存。返回可持久化的存储标识。 */
public String upload(Path file, String objectKey, String contentType) throws IOException { public String upload(Path file, String objectKey, String contentType) throws IOException {
try (InputStream input = Files.newInputStream(file)) {
return upload(input, Files.size(file), objectKey, contentType);
}
}
/** 使用输入流上传(MultipartFile 直传,不落临时文件)。返回可持久化的存储标识。 */
public String upload(InputStream input, long size, String objectKey, String contentType) throws IOException {
validateObjectKey(objectKey); validateObjectKey(objectKey);
String prefix = properties.getPathPrefix(); String prefix = properties.getPathPrefix();
validateObjectKey(prefix); validateObjectKey(prefix);
@@ -37,9 +44,9 @@ public class OssStorage {
} }
objectKey = prefix + "/" + objectKey; objectKey = prefix + "/" + objectKey;
OSS client = createClient(); OSS client = createClient();
try (InputStream input = Files.newInputStream(file)) { try {
ObjectMetadata metadata = new ObjectMetadata(); ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(Files.size(file)); metadata.setContentLength(size);
metadata.setContentType(contentType); metadata.setContentType(contentType);
metadata.setObjectAcl(CannedAccessControlList.Private); metadata.setObjectAcl(CannedAccessControlList.Private);
client.putObject(new PutObjectRequest(properties.getBucket(), objectKey, input, metadata)); client.putObject(new PutObjectRequest(properties.getBucket(), objectKey, input, metadata));
@@ -29,7 +29,7 @@
<!-- 文件列表 --> <!-- 文件列表 -->
<transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul"> <transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList"> <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" underline="never" target="_blank"> <el-link :href="resolveFileHref(file.url)" underline="never" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span> <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link> </el-link>
<div class="ele-upload-list__item-content-action"> <div class="ele-upload-list__item-content-action">
@@ -200,6 +200,14 @@ function getFileName(name) {
} }
} }
// oss:// 存储的文件经后端鉴权接口转发下载,本地路径保持原样
function resolveFileHref(url) {
if (url && url.indexOf("oss://") === 0) {
return `${baseUrl}/common/download/resource?resource=` + encodeURIComponent(url)
}
return `${baseUrl}${url}`
}
// 对象转成指定字符串分隔 // 对象转成指定字符串分隔
function listToString(list, separator) { function listToString(list, separator) {
let strs = "" let strs = ""
+15 -2
View File
@@ -116,7 +116,10 @@ watch(() => props.modelValue, val => {
// 然后将数组转为对象数组 // 然后将数组转为对象数组
fileList.value = list.map(item => { fileList.value = list.map(item => {
if (typeof item === "string") { if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1 && !isExternal(item)) { if (item.indexOf("oss://") === 0) {
// oss:// 存储标识:经后端鉴权接口转发预览,原值保留在 name 供回传
item = { name: item, url: ossResourceUrl(item), oss: true }
} else if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
item = { name: baseUrl + item, url: baseUrl + item } item = { name: baseUrl + item, url: baseUrl + item }
} else { } else {
item = { name: item, url: item } item = { name: item, url: item }
@@ -173,7 +176,11 @@ function handleExceed() {
// 上传成功回调 // 上传成功回调
function handleUploadSuccess(res, file) { function handleUploadSuccess(res, file) {
if (res.code === 200) { if (res.code === 200) {
if (res.fileName && res.fileName.indexOf("oss://") === 0) {
uploadList.value.push({ name: res.fileName, url: ossResourceUrl(res.fileName), oss: true })
} else {
uploadList.value.push({ name: res.fileName, url: res.fileName }) uploadList.value.push({ name: res.fileName, url: res.fileName })
}
uploadedSuccessfully() uploadedSuccessfully()
} else { } else {
number.value-- number.value--
@@ -217,13 +224,19 @@ function handlePictureCardPreview(file) {
dialogVisible.value = true dialogVisible.value = true
} }
// oss:// 存储的图片经后端鉴权接口转发预览/下载
function ossResourceUrl(key) {
return baseUrl + "/common/download/resource?resource=" + encodeURIComponent(key)
}
// 对象转成指定字符串分隔 // 对象转成指定字符串分隔
function listToString(list, separator) { function listToString(list, separator) {
let strs = "" let strs = ""
separator = separator || "," separator = separator || ","
for (let i in list) { for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) { if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].url.replace(baseUrl, "") + separator // oss:// 项回传原始存储标识,其余去掉 baseUrl 前缀
strs += (list[i].oss ? list[i].name : list[i].url.replace(baseUrl, "")) + separator
} }
} }
return strs != "" ? strs.substr(0, strs.length - 1) : "" return strs != "" ? strs.substr(0, strs.length - 1) : ""
@@ -311,6 +311,7 @@
:show-file-list="false" :show-file-list="false"
:action="uploadFileUrl" :action="uploadFileUrl"
:headers="uploadHeaders" :headers="uploadHeaders"
:data="{ module: 'customer' }"
:before-upload="handleBeforeAttachmentUpload" :before-upload="handleBeforeAttachmentUpload"
:on-success="handleAttachmentUploadSuccess" :on-success="handleAttachmentUploadSuccess"
:on-error="handleAttachmentUploadError" :on-error="handleAttachmentUploadError"
@@ -506,12 +507,12 @@
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="营业执照" prop="businessLicenseUrl"> <el-form-item label="营业执照" prop="businessLicenseUrl">
<image-upload v-model="companyForm.businessLicenseUrl" :limit="1" :size="5" /> <image-upload v-model="companyForm.businessLicenseUrl" :limit="1" :size="5" :action="ossUploadAction" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="其他附件" prop="attachmentIds"> <el-form-item label="其他附件" prop="attachmentIds">
<file-upload v-model="companyForm.attachmentIds" :limit="10" :size="50" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx,.zip,.rar" /> <file-upload v-model="companyForm.attachmentIds" :limit="10" :size="50" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx,.zip,.rar" :action="ossUploadAction" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
@@ -905,7 +906,9 @@ function handleUserChange(uid, user, nameField) {
/* ---------------- 附件上传 ---------------- */ /* ---------------- 附件上传 ---------------- */
const baseUrl = import.meta.env.VITE_APP_BASE_API const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadFileUrl = ref(baseUrl + '/common/upload') const uploadFileUrl = ref(baseUrl + '/common/upload/oss')
// OSS 上传端点(相对路径,组件内部会拼 VITE_APP_BASE_API)
const ossUploadAction = '/common/upload/oss'
const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() }) const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
const attachmentTypeOptions = ref([ const attachmentTypeOptions = ref([
{ value: '0', label: '附件资料' }, { value: '0', label: '附件资料' },
@@ -726,6 +726,7 @@
:show-file-list="false" :show-file-list="false"
:action="uploadFileUrl" :action="uploadFileUrl"
:headers="uploadHeaders" :headers="uploadHeaders"
:data="{ module: 'receipt' }"
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess" :on-success="handleUploadSuccess"
:on-error="handleUploadError" :on-error="handleUploadError"
@@ -2194,7 +2195,7 @@ watch(activeTab, (tab) => {
}) })
/* ================= 附件上传 ================= */ /* ================= 附件上传 ================= */
const uploadFileUrl = ref(baseUrl + "/common/upload") const uploadFileUrl = ref(baseUrl + "/common/upload/oss")
const uploadHeaders = ref({ Authorization: "Bearer " + getToken() }) const uploadHeaders = ref({ Authorization: "Bearer " + getToken() })
const ATTACHMENT_SIZE_LIMIT = 50 const ATTACHMENT_SIZE_LIMIT = 50
const ATTACHMENT_EXTENSIONS = ["pdf", "jpg", "jpeg", "png", "gif", "doc", "docx", "xls", "xlsx", "zip", "rar"] const ATTACHMENT_EXTENSIONS = ["pdf", "jpg", "jpeg", "png", "gif", "doc", "docx", "xls", "xlsx", "zip", "rar"]
@@ -275,6 +275,7 @@
:show-file-list="false" :show-file-list="false"
:action="uploadFileUrl" :action="uploadFileUrl"
:headers="uploadHeaders" :headers="uploadHeaders"
:data="{ module: 'supplier' }"
:before-upload="handleBeforeAttachmentUpload" :before-upload="handleBeforeAttachmentUpload"
:on-success="handleAttachmentUploadSuccess" :on-success="handleAttachmentUploadSuccess"
:on-error="handleAttachmentUploadError" :on-error="handleAttachmentUploadError"
@@ -470,12 +471,12 @@
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="营业执照" prop="businessLicenseUrl"> <el-form-item label="营业执照" prop="businessLicenseUrl">
<image-upload v-model="companyForm.businessLicenseUrl" :limit="1" :size="5" /> <image-upload v-model="companyForm.businessLicenseUrl" :limit="1" :size="5" :action="ossUploadAction" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="其他附件" prop="attachmentIds"> <el-form-item label="其他附件" prop="attachmentIds">
<file-upload v-model="companyForm.attachmentIds" :limit="10" :size="50" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx,.zip,.rar" /> <file-upload v-model="companyForm.attachmentIds" :limit="10" :size="50" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx,.xls,.xlsx,.zip,.rar" :action="ossUploadAction" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
@@ -720,7 +721,9 @@ function handleUserChange(uid, user, nameField) {
/* ---------------- 附件上传 ---------------- */ /* ---------------- 附件上传 ---------------- */
const baseUrl = import.meta.env.VITE_APP_BASE_API const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadFileUrl = ref(baseUrl + '/common/upload') const uploadFileUrl = ref(baseUrl + '/common/upload/oss')
// OSS 上传端点(相对路径,组件内部会拼 VITE_APP_BASE_API)
const ossUploadAction = '/common/upload/oss'
const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() }) const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
const attachmentTypeOptions = ref([ const attachmentTypeOptions = ref([
{ value: '0', label: '附件资料' }, { value: '0', label: '附件资料' },