Enhance resume file upload handling
This commit is contained in:
@@ -32,27 +32,30 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
*/
|
*/
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
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 {
|
try {
|
||||||
// 尝试获取classpath下的static目录
|
|
||||||
Resource resource = resourceLoader.getResource("classpath:/static/");
|
Resource resource = resourceLoader.getResource("classpath:/static/");
|
||||||
try {
|
|
||||||
File staticDir = resource.getFile();
|
File staticDir = resource.getFile();
|
||||||
staticResourcePath = staticDir.getAbsolutePath();
|
staticResourcePath = staticDir.getAbsolutePath();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// 如果在jar包中,使用项目根目录
|
File targetStaticDir = new File(projectRoot, "target/classes/static");
|
||||||
String projectRoot = System.getProperty("user.dir");
|
if (targetStaticDir.exists()) {
|
||||||
File staticDir = new File(projectRoot, "src/main/resources/static");
|
staticResourcePath = targetStaticDir.getAbsolutePath();
|
||||||
if (!staticDir.exists()) {
|
} else {
|
||||||
staticDir = new File(projectRoot, "target/classes/static");
|
|
||||||
}
|
|
||||||
staticResourcePath = staticDir.getAbsolutePath();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 降级方案:使用项目根目录
|
|
||||||
String projectRoot = System.getProperty("user.dir");
|
|
||||||
staticResourcePath = new File(projectRoot, "src/main/resources/static").getAbsolutePath();
|
staticResourcePath = new File(projectRoot, "src/main/resources/static").getAbsolutePath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 配置静态资源访问
|
* 配置静态资源访问
|
||||||
@@ -78,4 +81,3 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
.setCachePeriod(3600);
|
.setCachePeriod(3600);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,14 +189,16 @@ public class ResumeController {
|
|||||||
String extension = "";
|
String extension = "";
|
||||||
int lastDotIndex = originalFilename.lastIndexOf(".");
|
int lastDotIndex = originalFilename.lastIndexOf(".");
|
||||||
if (lastDotIndex > 0) {
|
if (lastDotIndex > 0) {
|
||||||
extension = originalFilename.substring(lastDotIndex);
|
extension = originalFilename.substring(lastDotIndex).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证文件类型(允许PDF和Word文档)
|
// 验证文件类型(允许PDF和Word文档)
|
||||||
String contentType = file.getContentType();
|
String contentType = file.getContentType();
|
||||||
if (contentType != null && !contentType.equals("application/pdf")
|
boolean isAllowedContentType = "application/pdf".equals(contentType)
|
||||||
&& !contentType.equals("application/msword")
|
|| "application/msword".equals(contentType)
|
||||||
&& !contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) {
|
|| "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文档格式");
|
throw new BusinessException(ErrorCode.PARAMS_ERROR, "仅支持PDF和Word文档格式");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ public class FileService {
|
|||||||
*/
|
*/
|
||||||
private String basePath;
|
private String basePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件存储路径(相对于项目根目录)
|
||||||
|
*/
|
||||||
|
@Value("${file.storage.path:src/main/resources/static}")
|
||||||
|
private String storagePath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件访问基础URL(用于构建访问地址)
|
* 文件访问基础URL(用于构建访问地址)
|
||||||
*/
|
*/
|
||||||
@@ -44,36 +50,38 @@ public class FileService {
|
|||||||
*/
|
*/
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
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 {
|
try {
|
||||||
// 获取src/main/resources/static目录的绝对路径
|
|
||||||
Resource resource = resourceLoader.getResource("classpath:/static/");
|
Resource resource = resourceLoader.getResource("classpath:/static/");
|
||||||
File staticDir;
|
File staticDir = resource.getFile();
|
||||||
|
if (staticDir.exists()) {
|
||||||
try {
|
return staticDir.getAbsolutePath();
|
||||||
// 尝试获取资源文件的实际路径
|
}
|
||||||
staticDir = resource.getFile();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// 如果在jar包中运行,无法直接获取File,则使用项目根目录下的路径
|
log.warn("无法从classpath解析静态目录,尝试降级路径: {}", e.getMessage());
|
||||||
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();
|
Path targetPath = Paths.get(projectRoot, "target/classes/static");
|
||||||
|
if (Files.exists(targetPath)) {
|
||||||
// 确保目录存在
|
return targetPath.toAbsolutePath().toString();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resolvedPath.toAbsolutePath().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,4 +224,3 @@ public class FileService {
|
|||||||
return filePath.toAbsolutePath().toString();
|
return filePath.toAbsolutePath().toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,12 @@
|
|||||||
</a-upload>
|
</a-upload>
|
||||||
<div v-if="uploading" style="color: #1890ff">
|
<div v-if="uploading" style="color: #1890ff">
|
||||||
<a-spin size="small" /> 上传中...
|
<a-spin size="small" /> 上传中...
|
||||||
|
<a-progress
|
||||||
|
v-if="uploadProgress > 0"
|
||||||
|
:percent="uploadProgress"
|
||||||
|
size="small"
|
||||||
|
style="margin-top: 8px"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="formState.attachmentUrl" style="margin-top: 8px">
|
<div v-if="formState.attachmentUrl" style="margin-top: 8px">
|
||||||
<a-space>
|
<a-space>
|
||||||
@@ -47,7 +53,8 @@
|
|||||||
<FileTextOutlined />
|
<FileTextOutlined />
|
||||||
查看附件
|
查看附件
|
||||||
</a>
|
</a>
|
||||||
<a @click="formState.attachmentUrl = ''" style="color: #ff4d4f">删除</a>
|
<span style="color: #999">{{ uploadFileName }}</span>
|
||||||
|
<a @click="clearAttachment" style="color: #ff4d4f">删除</a>
|
||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!formState.attachmentUrl && !uploading" style="color: #999; font-size: 12px">
|
<div v-if="!formState.attachmentUrl && !uploading" style="color: #999; font-size: 12px">
|
||||||
@@ -73,7 +80,7 @@
|
|||||||
import NavBar from '@/components/NavBar.vue';
|
import NavBar from '@/components/NavBar.vue';
|
||||||
import { reactive, ref, onMounted, computed } from 'vue';
|
import { reactive, ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
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 { message, Upload } from 'ant-design-vue';
|
||||||
import { UploadOutlined, FileTextOutlined } from '@ant-design/icons-vue';
|
import { UploadOutlined, FileTextOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
@@ -83,6 +90,8 @@ const router = useRouter();
|
|||||||
const isEdit = computed(() => !!route.params.id);
|
const isEdit = computed(() => !!route.params.id);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
|
const uploadProgress = ref(0);
|
||||||
|
const uploadFileName = ref('');
|
||||||
|
|
||||||
const formState = reactive({
|
const formState = reactive({
|
||||||
resumeTitle: '',
|
resumeTitle: '',
|
||||||
@@ -106,6 +115,7 @@ const loadData = async () => {
|
|||||||
const res = await getResumeVoById({ id });
|
const res = await getResumeVoById({ id });
|
||||||
if (res.code === 0 && res.data) {
|
if (res.code === 0 && res.data) {
|
||||||
Object.assign(formState, res.data);
|
Object.assign(formState, res.data);
|
||||||
|
uploadFileName.value = extractFileName(formState.attachmentUrl);
|
||||||
} else {
|
} else {
|
||||||
message.error(res.message || '加载简历失败');
|
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 beforeUpload = (file: File) => {
|
||||||
const isPdf = file.type === 'application/pdf';
|
const isPdf = file.type === 'application/pdf';
|
||||||
const isWord = file.type === 'application/msword' ||
|
const isWord = file.type === 'application/msword' ||
|
||||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
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;
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||||
|
|
||||||
if (!isPdf && !isWord) {
|
if (!isPdf && !isWord && !isAllowedExtension) {
|
||||||
message.error('仅支持PDF和Word文档格式');
|
message.error('仅支持PDF和Word文档格式');
|
||||||
return Upload.LIST_IGNORE;
|
return Upload.LIST_IGNORE;
|
||||||
}
|
}
|
||||||
@@ -132,27 +150,62 @@ const beforeUpload = (file: File) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async (options: any) => {
|
const handleUpload = async (options: any) => {
|
||||||
const { file } = options;
|
const { file, onError, onSuccess } = options;
|
||||||
uploading.value = true;
|
uploading.value = true;
|
||||||
|
uploadProgress.value = 0;
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const res = await uploadResumeFile(formData);
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||||
if (res.code === 0 && res.data) {
|
const xhr = new XMLHttpRequest();
|
||||||
formState.attachmentUrl = res.data;
|
xhr.open('POST', `${baseUrl}/resume/upload`, true);
|
||||||
message.success('文件上传成功');
|
xhr.withCredentials = true;
|
||||||
} else {
|
xhr.upload.onprogress = (event) => {
|
||||||
message.error(res.message || '文件上传失败');
|
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) {
|
} catch (error: any) {
|
||||||
console.error('上传失败:', error);
|
console.error('上传失败:', error);
|
||||||
message.error(error.message || '文件上传失败');
|
message.error(error.message || '文件上传失败');
|
||||||
} finally {
|
uploadProgress.value = 0;
|
||||||
uploading.value = false;
|
uploading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearAttachment = () => {
|
||||||
|
formState.attachmentUrl = '';
|
||||||
|
uploadFileName.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
const onFinish = async (values: any) => {
|
const onFinish = async (values: any) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user