feat(storage): 集成腾讯云COS实现简历PDF文件存储

- 配置腾讯云COS相关参数和密钥设置
- 新增CosConfig配置类和CosService对象存储服务
- 实现文件上传、删除、存在性检查等功能
- 修改简历上传接口使用COS存储替代本地存储
- 限制文件格式仅支持PDF并更新前端验证逻辑
- 添加删除COS文件的后端接口和前端调用
- 移除前端MyResumes页面中的旧PDF管理组件
- 更新上传和删除操作的安全验证和权限检查
This commit is contained in:
2026-01-14 22:57:15 +08:00
parent a146c28cf8
commit fcf095728e
11 changed files with 513 additions and 67 deletions

View File

@@ -31,3 +31,4 @@ build/
### VS Code ### ### VS Code ###
.vscode/ .vscode/
/src/main/resources/application-dev.yml

View File

@@ -123,6 +123,12 @@
<version>6.0.0</version> <version>6.0.0</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<!--腾讯云COS对象存储-->
<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.6.227</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -0,0 +1,58 @@
package com.zds.boss.config;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.region.Region;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 腾讯云COS配置类
*/
@Configuration
@ConfigurationProperties(prefix = "cos")
@Data
public class CosConfig {
/**
* 密钥ID
*/
private String secretId;
/**
* 密钥Key
*/
private String secretKey;
/**
* 地域
*/
private String region;
/**
* 存储桶名称
*/
private String bucketName;
/**
* 访问域名前缀
*/
private String urlPrefix;
/**
* 创建COS客户端Bean
*/
@Bean
public COSClient cosClient() {
// 初始化用户身份信息
COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
// 设置bucket的区域
ClientConfig clientConfig = new ClientConfig(new Region(region));
// 生成cos客户端
return new COSClient(cred, clientConfig);
}
}

View File

@@ -14,7 +14,9 @@ import com.zds.boss.model.entity.Resume;
import com.zds.boss.model.entity.User; import com.zds.boss.model.entity.User;
import com.zds.boss.model.enums.UserRoleEnum; import com.zds.boss.model.enums.UserRoleEnum;
import com.zds.boss.model.vo.ResumeVO; import com.zds.boss.model.vo.ResumeVO;
import com.zds.boss.service.CosService;
import com.zds.boss.service.FileService; import com.zds.boss.service.FileService;
import com.zds.boss.service.ResumeAddressService;
import com.zds.boss.service.ResumeService; import com.zds.boss.service.ResumeService;
import com.zds.boss.service.UserService; import com.zds.boss.service.UserService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -24,7 +26,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -44,6 +45,12 @@ public class ResumeController {
@Resource @Resource
private FileService fileService; private FileService fileService;
@Resource
private CosService cosService;
@Resource
private ResumeAddressService resumeAddressService;
@Value("${file.max-size-mb:10}") @Value("${file.max-size-mb:10}")
private long maxSizeMb; private long maxSizeMb;
@@ -158,14 +165,18 @@ public class ResumeController {
} }
/** /**
* 上传简历附件文件 * 上传简历附件文件到腾讯云COS
* *
* @param file 文件 * @param file 文件
* @param resumeId 简历ID可选如果传入则同时更新resume的attachment_url
* @param request HTTP请求 * @param request HTTP请求
* @return 文件访问URL * @return 文件访问URL
*/ */
@PostMapping("/upload") @PostMapping("/upload")
public BaseResponse<String> uploadFile(@RequestParam("file") MultipartFile file, HttpServletRequest request) { public BaseResponse<String> uploadFile(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "resumeId", required = false) Long resumeId,
HttpServletRequest request) {
if (file == null || file.isEmpty()) { if (file == null || file.isEmpty()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件不能为空"); throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件不能为空");
} }
@@ -192,36 +203,104 @@ public class ResumeController {
extension = originalFilename.substring(lastDotIndex).toLowerCase(); extension = originalFilename.substring(lastDotIndex).toLowerCase();
} }
// 验证文件类型允许PDF和Word文档 // 验证文件类型(允许PDF
String contentType = file.getContentType(); String contentType = file.getContentType();
boolean isAllowedContentType = "application/pdf".equals(contentType) boolean isAllowedContentType = "application/pdf".equals(contentType);
|| "application/msword".equals(contentType) boolean isAllowedExtension = ".pdf".equals(extension);
|| "application/vnd.openxmlformats-officedocument.wordprocessingml.document".equals(contentType);
boolean isAllowedExtension = ".pdf".equals(extension) || ".doc".equals(extension) || ".docx".equals(extension);
if (!isAllowedContentType && !isAllowedExtension) { if (!isAllowedContentType && !isAllowedExtension) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "仅支持PDF和Word文档格式"); throw new BusinessException(ErrorCode.PARAMS_ERROR, "仅支持PDF格式");
} }
try { // 如果传入了resumeId检查是否有权限操作该简历
// 生成唯一文件名用户ID_时间戳_UUID.扩展名 if (resumeId != null && resumeId > 0) {
Resume resume = resumeService.getById(resumeId);
if (resume == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "简历不存在");
}
if (!resume.getUserId().equals(loginUser.getId())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权操作该简历");
}
}
// 生成随机文件Key用于resume_address表
String fileKey = UUID.randomUUID().toString().replace("-", "");
// 生成唯一文件名用户ID_时间戳_UUID
String timestamp = String.valueOf(System.currentTimeMillis()); String timestamp = String.valueOf(System.currentTimeMillis());
String uniqueFileName = String.format("%d_%s_%s%s", String customFileName = String.format("%d_%s_%s",
loginUser.getId(), timestamp, UUID.randomUUID().toString().replace("-", ""), extension); loginUser.getId(), timestamp, fileKey);
// 构建相对路径resume/{userId}/{filename} // 构建存储目录resume/{userId}
String relativePath = String.format("resume/%d/%s", loginUser.getId(), uniqueFileName); String directory = String.format("resume/%d", loginUser.getId());
// 上传文件 // 上传文件到腾讯云COS
fileService.upload(relativePath, file); String fileUrl = cosService.uploadFile(file, directory, customFileName);
// 构建文件访问URL // 如果传入了resumeId更新resume的attachment_url
String fileUrl = fileService.buildUrl(relativePath); if (resumeId != null && resumeId > 0) {
Resume resume = new Resume();
log.info("用户 {} 上传文件成功: {}, URL: {}", loginUser.getId(), originalFilename, fileUrl); resume.setId(resumeId);
return ResultUtils.success(fileUrl); resume.setAttachmentUrl(fileUrl);
} catch (IOException e) { resumeService.updateById(resume);
log.error("文件上传失败: {}", e.getMessage(), e); log.info("已更新简历 {} 的附件URL", resumeId);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "文件上传失败: " + e.getMessage());
} }
// 保存或更新resume_address记录
resumeAddressService.saveOrUpdateByUserId(loginUser.getId(), resumeId, fileUrl, fileKey);
log.info("已保存简历地址记录userId: {}, fileKey: {}", loginUser.getId(), fileKey);
log.info("用户 {} 上传简历PDF成功: {}, URL: {}", loginUser.getId(), originalFilename, fileUrl);
return ResultUtils.success(fileUrl);
}
/**
* 删除COS中的简历附件文件
*
* @param fileUrl 文件URL
* @param resumeId 简历ID可选如果传入则同时清空resume的attachment_url
* @param request HTTP请求
* @return 是否删除成功
*/
@PostMapping("/delete-file")
public BaseResponse<Boolean> deleteFile(
@RequestParam("fileUrl") String fileUrl,
@RequestParam(value = "resumeId", required = false) Long resumeId,
HttpServletRequest request) {
if (fileUrl == null || fileUrl.isEmpty()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件URL不能为空");
}
User loginUser = userService.getLoginUser(request);
// 验证URL是否属于当前用户安全检查
String userDirectory = String.format("resume/%d/", loginUser.getId());
if (!fileUrl.contains(userDirectory)) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权删除该文件");
}
// 删除COS中的文件
boolean result = cosService.deleteFile(fileUrl);
if (result) {
log.info("用户 {} 删除文件成功: {}", loginUser.getId(), fileUrl);
// 如果传入了resumeId清空resume的attachment_url
if (resumeId != null && resumeId > 0) {
Resume resume = resumeService.getById(resumeId);
if (resume != null && resume.getUserId().equals(loginUser.getId())) {
resume.setAttachmentUrl("");
resumeService.updateById(resume);
log.info("已清空简历 {} 的附件URL", resumeId);
}
}
// 删除resume_address记录
resumeAddressService.deleteByUserId(loginUser.getId());
log.info("已删除用户 {} 的简历地址记录", loginUser.getId());
} else {
log.warn("用户 {} 删除文件失败: {}", loginUser.getId(), fileUrl);
}
return ResultUtils.success(result);
} }
} }

View File

@@ -0,0 +1,159 @@
package com.zds.boss.service;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.model.*;
import com.zds.boss.config.CosConfig;
import com.zds.boss.exception.BusinessException;
import com.zds.boss.exception.ErrorCode;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
/**
* 腾讯云COS对象存储服务
*/
@Service
@Slf4j
public class CosService {
@Resource
private COSClient cosClient;
@Resource
private CosConfig cosConfig;
/**
* 上传文件到COS
*
* @param file 要上传的文件
* @param directory 存储目录resume/123
* @param customName 自定义文件名不含扩展名为null时使用UUID
* @return 文件的访问URL
*/
public String uploadFile(MultipartFile file, String directory, String customName) {
// 获取原始文件名和扩展名
String originalFilename = file.getOriginalFilename();
String extension = "";
if (originalFilename != null && originalFilename.contains(".")) {
extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
}
// 生成文件名
String fileName;
if (customName != null && !customName.isEmpty()) {
fileName = customName + extension;
} else {
fileName = UUID.randomUUID().toString().replace("-", "") + extension;
}
// 构建完整的对象Key路径
String key = directory + "/" + fileName;
// 移除开头的斜杠(如果有)
if (key.startsWith("/")) {
key = key.substring(1);
}
try (InputStream inputStream = file.getInputStream()) {
// 设置对象元数据
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 创建上传请求
PutObjectRequest putObjectRequest = new PutObjectRequest(
cosConfig.getBucketName(),
key,
inputStream,
metadata
);
// 执行上传
PutObjectResult result = cosClient.putObject(putObjectRequest);
log.info("文件上传成功Key: {}, ETag: {}", key, result.getETag());
// 返回文件访问URL
return cosConfig.getUrlPrefix() + "/" + key;
} catch (IOException e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "文件上传失败: " + e.getMessage());
}
}
/**
* 上传文件到COS使用默认UUID文件名
*
* @param file 要上传的文件
* @param directory 存储目录
* @return 文件的访问URL
*/
public String uploadFile(MultipartFile file, String directory) {
return uploadFile(file, directory, null);
}
/**
* 删除COS中的文件
*
* @param fileUrl 文件的完整URL
* @return 是否删除成功
*/
public boolean deleteFile(String fileUrl) {
if (fileUrl == null || fileUrl.isEmpty()) {
return false;
}
try {
// 从URL中提取对象Key
String key = extractKeyFromUrl(fileUrl);
if (key == null) {
log.warn("无法从URL中提取Key: {}", fileUrl);
return false;
}
// 删除对象
cosClient.deleteObject(cosConfig.getBucketName(), key);
log.info("文件删除成功Key: {}", key);
return true;
} catch (Exception e) {
log.error("文件删除失败: {}", e.getMessage(), e);
return false;
}
}
/**
* 从URL中提取对象Key
*
* @param fileUrl 文件URL
* @return 对象Key
*/
private String extractKeyFromUrl(String fileUrl) {
String urlPrefix = cosConfig.getUrlPrefix();
if (fileUrl.startsWith(urlPrefix)) {
String key = fileUrl.substring(urlPrefix.length());
if (key.startsWith("/")) {
key = key.substring(1);
}
return key;
}
return null;
}
/**
* 检查文件是否存在
*
* @param key 对象Key
* @return 是否存在
*/
public boolean doesObjectExist(String key) {
try {
return cosClient.doesObjectExist(cosConfig.getBucketName(), key);
} catch (Exception e) {
log.error("检查文件是否存在失败: {}", e.getMessage(), e);
return false;
}
}
}

View File

@@ -0,0 +1,37 @@
package com.zds.boss.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zds.boss.model.entity.ResumeAddress;
/**
* 简历地址服务
*/
public interface ResumeAddressService extends IService<ResumeAddress> {
/**
* 根据用户ID获取简历地址
*
* @param userId 用户ID
* @return 简历地址
*/
ResumeAddress getByUserId(Long userId);
/**
* 保存或更新简历地址
*
* @param userId 用户ID
* @param resumeId 简历ID可为空
* @param address 文件URL
* @param fileKey 文件Key
* @return 简历地址
*/
ResumeAddress saveOrUpdateByUserId(Long userId, Long resumeId, String address, String fileKey);
/**
* 根据用户ID删除简历地址
*
* @param userId 用户ID
* @return 是否成功
*/
boolean deleteByUserId(Long userId);
}

View File

@@ -0,0 +1,56 @@
package com.zds.boss.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zds.boss.mapper.ResumeAddressMapper;
import com.zds.boss.model.entity.ResumeAddress;
import com.zds.boss.service.ResumeAddressService;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* 简历地址服务实现
*/
@Service
public class ResumeAddressServiceImpl extends ServiceImpl<ResumeAddressMapper, ResumeAddress>
implements ResumeAddressService {
@Override
public ResumeAddress getByUserId(Long userId) {
return this.getOne(new LambdaQueryWrapper<ResumeAddress>()
.eq(ResumeAddress::getUserId, userId));
}
@Override
public ResumeAddress saveOrUpdateByUserId(Long userId, Long resumeId, String address, String fileKey) {
ResumeAddress existing = getByUserId(userId);
if (existing != null) {
// 更新现有记录
existing.setResumeId(resumeId);
existing.setAddress(address);
existing.setFileKey(fileKey);
existing.setUpdatedAt(new Date());
this.updateById(existing);
return existing;
} else {
// 创建新记录
ResumeAddress resumeAddress = new ResumeAddress();
resumeAddress.setUserId(userId);
resumeAddress.setResumeId(resumeId);
resumeAddress.setAddress(address);
resumeAddress.setFileKey(fileKey);
resumeAddress.setCreatedAt(new Date());
resumeAddress.setUpdatedAt(new Date());
this.save(resumeAddress);
return resumeAddress;
}
}
@Override
public boolean deleteByUserId(Long userId) {
return this.remove(new LambdaQueryWrapper<ResumeAddress>()
.eq(ResumeAddress::getUserId, userId));
}
}

View File

@@ -71,3 +71,16 @@ file:
path: src/main/resources/static path: src/main/resources/static
# 最大文件大小MB # 最大文件大小MB
max-size-mb: 10 max-size-mb: 10
# 腾讯云COS配置
cos:
# 密钥ID
secret-id:
# 密钥Key生产环境请使用环境变量或配置中心
secret-key:
# 地域
region: ap-shanghai
# 存储桶名称
bucket-name: bosss-1336488383
# 访问域名前缀
url-prefix: https://bosss-1336488383.cos.ap-shanghai.myqcloud.com

View File

@@ -71,7 +71,7 @@ export async function updateResume(
}) })
} }
/** 此处后端没有提供注释 POST /resume/upload */ /** 上传简历PDF文件到腾讯云COS POST /resume/upload */
export async function uploadFile(body: {}, options?: { [key: string]: any }) { export async function uploadFile(body: {}, options?: { [key: string]: any }) {
return request<API.BaseResponseString>('/resume/upload', { return request<API.BaseResponseString>('/resume/upload', {
method: 'POST', method: 'POST',
@@ -82,3 +82,17 @@ export async function uploadFile(body: {}, options?: { [key: string]: any }) {
...(options || {}), ...(options || {}),
}) })
} }
/** 删除COS中的简历附件文件 POST /resume/delete-file */
export async function deleteResumeFile(
params: { fileUrl: string; resumeId?: number },
options?: { [key: string]: any }
) {
return request<API.BaseResponseBoolean>('/resume/delete-file', {
method: 'POST',
params: {
...params,
},
...(options || {}),
})
}

View File

@@ -35,26 +35,26 @@
</a-table> </a-table>
</a-card> </a-card>
<a-card title="我的简历PDF" :bordered="false" style="margin-top: 16px"> <!-- <a-card title="我的简历PDF" :bordered="false" style="margin-top: 16px">-->
<div v-if="addressLoading"> <!-- <div v-if="addressLoading">-->
<a-spin /> <!-- <a-spin />-->
</div> <!-- </div>-->
<div v-else> <!-- <div v-else>-->
<div v-if="myAddress?.address"> <!-- <div v-if="myAddress?.address">-->
<a-space> <!-- <a-space>-->
<a :href="myAddress.address" target="_blank">查看PDF</a> <!-- <a :href="myAddress.address" target="_blank">查看PDF</a>-->
<a-popconfirm title="确定删除该PDF吗" @confirm="deleteAddress"> <!-- <a-popconfirm title="确定删除该PDF吗" @confirm="deleteAddress">-->
<a-button danger>删除</a-button> <!-- <a-button danger>删除</a-button>-->
</a-popconfirm> <!-- </a-popconfirm>-->
</a-space> <!-- </a-space>-->
</div> <!-- </div>-->
<div v-else> <!-- <div v-else>-->
<a-upload :before-upload="beforeUpload" :show-upload-list="false" :custom-request="(opt:any)=>doUpload(opt.file)"> <!-- <a-upload :before-upload="beforeUpload" :show-upload-list="false" :custom-request="(opt:any)=>doUpload(opt.file)">-->
<a-button type="primary">上传PDF</a-button> <!-- <a-button type="primary">上传PDF</a-button>-->
</a-upload> <!-- </a-upload>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
</a-card> <!-- </a-card>-->
</div> </div>
</template> </template>
@@ -62,7 +62,7 @@
import NavBar from '@/components/NavBar.vue'; import NavBar from '@/components/NavBar.vue';
import { ref, onMounted, reactive } from 'vue'; import { ref, onMounted, reactive } from 'vue';
import { listResumeVoByPage, deleteResume } from '@/api/api/resumeController'; import { listResumeVoByPage, deleteResume } from '@/api/api/resumeController';
import { uploadResumePdf, getMyResumeAddress, deleteMyResumeAddress } from '@/api/api/resumeAddressController'; import { } from '@/api/api/resumeController';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';

View File

@@ -31,11 +31,11 @@
:before-upload="beforeUpload" :before-upload="beforeUpload"
:show-upload-list="false" :show-upload-list="false"
:custom-request="handleUpload" :custom-request="handleUpload"
accept=".pdf,.doc,.docx" accept=".pdf"
> >
<a-button type="primary"> <a-button type="primary">
<UploadOutlined /> <UploadOutlined />
上传简历附件PDF/Word 上传简历PDF
</a-button> </a-button>
</a-upload> </a-upload>
<div v-if="uploading" style="color: #1890ff"> <div v-if="uploading" style="color: #1890ff">
@@ -50,15 +50,17 @@
<div v-if="formState.attachmentUrl" style="margin-top: 8px"> <div v-if="formState.attachmentUrl" style="margin-top: 8px">
<a-space> <a-space>
<a :href="formState.attachmentUrl" target="_blank" style="color: #1890ff"> <a :href="formState.attachmentUrl" target="_blank" style="color: #1890ff">
<FileTextOutlined /> <FilePdfOutlined />
查看附件 查看PDF简历
</a> </a>
<span style="color: #999">{{ uploadFileName }}</span> <span style="color: #999">{{ uploadFileName }}</span>
<a @click="clearAttachment" style="color: #ff4d4f">删除</a> <a-popconfirm title="确定要删除此PDF文件吗" @confirm="clearAttachment">
<a style="color: #ff4d4f">删除</a>
</a-popconfirm>
</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">
支持PDF和Word格式最大10MB 支持PDF格式最大10MB文件将上传至腾讯云COS
</div> </div>
</a-space> </a-space>
</a-form-item> </a-form-item>
@@ -80,9 +82,9 @@
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 } from '@/api/api/resumeController'; import { addResume, updateResume, getResumeVoById, deleteResumeFile } 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, FilePdfOutlined } from '@ant-design/icons-vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -132,14 +134,12 @@ const extractFileName = (url: string) => {
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' ||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
const extension = file.name.split('.').pop()?.toLowerCase(); const extension = file.name.split('.').pop()?.toLowerCase();
const isAllowedExtension = extension === 'pdf' || extension === 'doc' || extension === 'docx'; const isAllowedExtension = extension === 'pdf';
const isLt10M = file.size / 1024 / 1024 < 10; const isLt10M = file.size / 1024 / 1024 < 10;
if (!isPdf && !isWord && !isAllowedExtension) { if (!isPdf && !isAllowedExtension) {
message.error('仅支持PDF和Word文档格式'); message.error('仅支持PDF格式');
return Upload.LIST_IGNORE; return Upload.LIST_IGNORE;
} }
if (!isLt10M) { if (!isLt10M) {
@@ -157,6 +157,11 @@ const handleUpload = async (options: any) => {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
// 如果是编辑模式传入resumeId以便同时更新resume的attachment_url
if (isEdit.value && route.params.id) {
formData.append('resumeId', String(route.params.id));
}
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api'; const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api';
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('POST', `${baseUrl}/resume/upload`, true); xhr.open('POST', `${baseUrl}/resume/upload`, true);
@@ -201,7 +206,25 @@ const handleUpload = async (options: any) => {
} }
}; };
const clearAttachment = () => { const clearAttachment = async () => {
if (formState.attachmentUrl) {
try {
// 调用后端接口删除COS中的文件如果是编辑模式传入resumeId
const params: { fileUrl: string; resumeId?: number } = { fileUrl: formState.attachmentUrl };
if (isEdit.value && route.params.id) {
params.resumeId = Number(route.params.id);
}
const res = await deleteResumeFile(params);
if (res.code === 0) {
message.success('文件删除成功');
} else {
message.warning('文件删除可能失败,请手动确认');
}
} catch (error) {
console.error('删除文件失败:', error);
message.warning('文件删除失败');
}
}
formState.attachmentUrl = ''; formState.attachmentUrl = '';
uploadFileName.value = ''; uploadFileName.value = '';
}; };