From f4e14431ec4aba92c9afd6bbbc498d7d34082d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=8D=E4=BC=9A=E7=BC=96=E7=A0=81=40=E6=B5=B7=E5=B1=B1?= =?UTF-8?q?=E4=BA=86?= <89328906+RichZDS@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:10:32 +0800 Subject: [PATCH] Enhance resume file upload handling --- .../java/com/zds/boss/config/WebConfig.java | 36 +++++---- .../zds/boss/controller/ResumeController.java | 10 ++- .../com/zds/boss/service/FileService.java | 67 +++++++++------- BOSSFrontEnd/BOSS/src/views/ResumeEdit.vue | 79 ++++++++++++++++--- 4 files changed, 128 insertions(+), 64 deletions(-) diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/config/WebConfig.java b/BOSSBackEnd/src/main/java/com/zds/boss/config/WebConfig.java index cef28b4..d9e4a30 100644 --- a/BOSSBackEnd/src/main/java/com/zds/boss/config/WebConfig.java +++ b/BOSSBackEnd/src/main/java/com/zds/boss/config/WebConfig.java @@ -32,25 +32,28 @@ public class WebConfig implements WebMvcConfigurer { */ @PostConstruct public void init() { + String projectRoot = System.getProperty("user.dir"); + File configuredDir = new File(fileStoragePath); + if (!configuredDir.isAbsolute()) { + configuredDir = new File(projectRoot, fileStoragePath); + } + + if (configuredDir.exists()) { + staticResourcePath = configuredDir.getAbsolutePath(); + return; + } + try { - // 尝试获取classpath下的static目录 Resource resource = resourceLoader.getResource("classpath:/static/"); - try { - File staticDir = resource.getFile(); - staticResourcePath = staticDir.getAbsolutePath(); - } catch (IOException e) { - // 如果在jar包中,使用项目根目录 - String projectRoot = System.getProperty("user.dir"); - File staticDir = new File(projectRoot, "src/main/resources/static"); - if (!staticDir.exists()) { - staticDir = new File(projectRoot, "target/classes/static"); - } - staticResourcePath = staticDir.getAbsolutePath(); + File staticDir = resource.getFile(); + staticResourcePath = staticDir.getAbsolutePath(); + } catch (IOException e) { + File targetStaticDir = new File(projectRoot, "target/classes/static"); + if (targetStaticDir.exists()) { + staticResourcePath = targetStaticDir.getAbsolutePath(); + } else { + staticResourcePath = new File(projectRoot, "src/main/resources/static").getAbsolutePath(); } - } catch (Exception e) { - // 降级方案:使用项目根目录 - String projectRoot = System.getProperty("user.dir"); - staticResourcePath = new File(projectRoot, "src/main/resources/static").getAbsolutePath(); } } @@ -78,4 +81,3 @@ public class WebConfig implements WebMvcConfigurer { .setCachePeriod(3600); } } - diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java b/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java index 4b10316..80403f9 100644 --- a/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java +++ b/BOSSBackEnd/src/main/java/com/zds/boss/controller/ResumeController.java @@ -189,14 +189,16 @@ public class ResumeController { String extension = ""; int lastDotIndex = originalFilename.lastIndexOf("."); if (lastDotIndex > 0) { - extension = originalFilename.substring(lastDotIndex); + extension = originalFilename.substring(lastDotIndex).toLowerCase(); } // 验证文件类型(允许PDF和Word文档) String contentType = file.getContentType(); - if (contentType != null && !contentType.equals("application/pdf") - && !contentType.equals("application/msword") - && !contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) { + boolean isAllowedContentType = "application/pdf".equals(contentType) + || "application/msword".equals(contentType) + || "application/vnd.openxmlformats-officedocument.wordprocessingml.document".equals(contentType); + boolean isAllowedExtension = ".pdf".equals(extension) || ".doc".equals(extension) || ".docx".equals(extension); + if (!isAllowedContentType && !isAllowedExtension) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "仅支持PDF和Word文档格式"); } diff --git a/BOSSBackEnd/src/main/java/com/zds/boss/service/FileService.java b/BOSSBackEnd/src/main/java/com/zds/boss/service/FileService.java index ba961b5..0106d00 100644 --- a/BOSSBackEnd/src/main/java/com/zds/boss/service/FileService.java +++ b/BOSSBackEnd/src/main/java/com/zds/boss/service/FileService.java @@ -27,6 +27,12 @@ public class FileService { */ private String basePath; + /** + * 文件存储路径(相对于项目根目录) + */ + @Value("${file.storage.path:src/main/resources/static}") + private String storagePath; + /** * 文件访问基础URL(用于构建访问地址) */ @@ -44,36 +50,38 @@ public class FileService { */ @PostConstruct public void init() { - try { - // 获取src/main/resources/static目录的绝对路径 - Resource resource = resourceLoader.getResource("classpath:/static/"); - File staticDir; - - try { - // 尝试获取资源文件的实际路径 - staticDir = resource.getFile(); - } catch (IOException e) { - // 如果在jar包中运行,无法直接获取File,则使用项目根目录下的路径 - String projectRoot = System.getProperty("user.dir"); - staticDir = new File(projectRoot, "src/main/resources/static"); - - // 如果项目根目录下不存在,尝试使用target/classes/static - if (!staticDir.exists()) { - staticDir = new File(projectRoot, "target/classes/static"); - } - } - - this.basePath = staticDir.getAbsolutePath(); - - // 确保目录存在 - initStorageDirectory(); - } catch (Exception e) { - log.error("初始化文件存储目录失败", e); - // 降级方案:使用项目根目录下的static目录 - String projectRoot = System.getProperty("user.dir"); - this.basePath = new File(projectRoot, "src/main/resources/static").getAbsolutePath(); - initStorageDirectory(); + this.basePath = resolveBasePath(); + initStorageDirectory(); + log.info("文件存储路径已初始化: {}", basePath); + } + + private String resolveBasePath() { + String projectRoot = System.getProperty("user.dir"); + Path configuredPath = Paths.get(storagePath); + Path resolvedPath = configuredPath.isAbsolute() + ? configuredPath + : Paths.get(projectRoot).resolve(configuredPath); + + if (Files.exists(resolvedPath)) { + return resolvedPath.toAbsolutePath().toString(); } + + try { + Resource resource = resourceLoader.getResource("classpath:/static/"); + File staticDir = resource.getFile(); + if (staticDir.exists()) { + return staticDir.getAbsolutePath(); + } + } catch (IOException e) { + log.warn("无法从classpath解析静态目录,尝试降级路径: {}", e.getMessage()); + } + + Path targetPath = Paths.get(projectRoot, "target/classes/static"); + if (Files.exists(targetPath)) { + return targetPath.toAbsolutePath().toString(); + } + + return resolvedPath.toAbsolutePath().toString(); } /** @@ -216,4 +224,3 @@ public class FileService { return filePath.toAbsolutePath().toString(); } } - diff --git a/BOSSFrontEnd/BOSS/src/views/ResumeEdit.vue b/BOSSFrontEnd/BOSS/src/views/ResumeEdit.vue index 57ebf1e..6fba74b 100644 --- a/BOSSFrontEnd/BOSS/src/views/ResumeEdit.vue +++ b/BOSSFrontEnd/BOSS/src/views/ResumeEdit.vue @@ -40,6 +40,12 @@
上传中... +
@@ -47,7 +53,8 @@ 查看附件 - 删除 + {{ uploadFileName }} + 删除
@@ -73,7 +80,7 @@ import NavBar from '@/components/NavBar.vue'; import { reactive, ref, onMounted, computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; -import { addResume, updateResume, getResumeVoById, uploadResumeFile } from '@/api/api/resumeController'; +import { addResume, updateResume, getResumeVoById } from '@/api/api/resumeController'; import { message, Upload } from 'ant-design-vue'; import { UploadOutlined, FileTextOutlined } from '@ant-design/icons-vue'; @@ -83,6 +90,8 @@ const router = useRouter(); const isEdit = computed(() => !!route.params.id); const loading = ref(false); const uploading = ref(false); +const uploadProgress = ref(0); +const uploadFileName = ref(''); const formState = reactive({ resumeTitle: '', @@ -106,6 +115,7 @@ const loadData = async () => { const res = await getResumeVoById({ id }); if (res.code === 0 && res.data) { Object.assign(formState, res.data); + uploadFileName.value = extractFileName(formState.attachmentUrl); } else { message.error(res.message || '加载简历失败'); } @@ -114,13 +124,21 @@ const loadData = async () => { } }; +const extractFileName = (url: string) => { + if (!url) return ''; + const parts = url.split('/'); + return parts[parts.length - 1] || ''; +}; + const beforeUpload = (file: File) => { const isPdf = file.type === 'application/pdf'; const isWord = file.type === 'application/msword' || file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + const extension = file.name.split('.').pop()?.toLowerCase(); + const isAllowedExtension = extension === 'pdf' || extension === 'doc' || extension === 'docx'; const isLt10M = file.size / 1024 / 1024 < 10; - if (!isPdf && !isWord) { + if (!isPdf && !isWord && !isAllowedExtension) { message.error('仅支持PDF和Word文档格式'); return Upload.LIST_IGNORE; } @@ -132,27 +150,62 @@ const beforeUpload = (file: File) => { }; const handleUpload = async (options: any) => { - const { file } = options; + const { file, onError, onSuccess } = options; uploading.value = true; + uploadProgress.value = 0; try { const formData = new FormData(); formData.append('file', file); - - const res = await uploadResumeFile(formData); - if (res.code === 0 && res.data) { - formState.attachmentUrl = res.data; - message.success('文件上传成功'); - } else { - message.error(res.message || '文件上传失败'); - } + + const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api'; + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${baseUrl}/resume/upload`, true); + xhr.withCredentials = true; + xhr.upload.onprogress = (event) => { + if (event.lengthComputable) { + uploadProgress.value = Math.round((event.loaded / event.total) * 100); + } + }; + xhr.onload = () => { + try { + const response = JSON.parse(xhr.responseText); + if (response.code === 0 && response.data) { + formState.attachmentUrl = response.data; + uploadFileName.value = extractFileName(response.data); + message.success('文件上传成功'); + onSuccess?.(response, file); + } else { + message.error(response.message || '文件上传失败'); + onError?.(response); + } + } catch (e) { + message.error('文件上传失败'); + onError?.(e); + } finally { + uploadProgress.value = 0; + uploading.value = false; + } + }; + xhr.onerror = () => { + message.error('文件上传失败'); + onError?.(new Error('Upload failed')); + uploadProgress.value = 0; + uploading.value = false; + }; + xhr.send(formData); } catch (error: any) { console.error('上传失败:', error); message.error(error.message || '文件上传失败'); - } finally { + uploadProgress.value = 0; uploading.value = false; } }; +const clearAttachment = () => { + formState.attachmentUrl = ''; + uploadFileName.value = ''; +}; + const onFinish = async (values: any) => { loading.value = true; try {