feat(storage): 集成腾讯云COS实现简历PDF文件存储
- 配置腾讯云COS相关参数和密钥设置 - 新增CosConfig配置类和CosService对象存储服务 - 实现文件上传、删除、存在性检查等功能 - 修改简历上传接口使用COS存储替代本地存储 - 限制文件格式仅支持PDF并更新前端验证逻辑 - 添加删除COS文件的后端接口和前端调用 - 移除前端MyResumes页面中的旧PDF管理组件 - 更新上传和删除操作的安全验证和权限检查
This commit is contained in:
1
BOSSBackEnd/.gitignore
vendored
1
BOSSBackEnd/.gitignore
vendored
@@ -31,3 +31,4 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/src/main/resources/application-dev.yml
|
||||
|
||||
@@ -123,6 +123,12 @@
|
||||
<version>6.0.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!--腾讯云COS对象存储-->
|
||||
<dependency>
|
||||
<groupId>com.qcloud</groupId>
|
||||
<artifactId>cos_api</artifactId>
|
||||
<version>5.6.227</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
58
BOSSBackEnd/src/main/java/com/zds/boss/config/CosConfig.java
Normal file
58
BOSSBackEnd/src/main/java/com/zds/boss/config/CosConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@ import com.zds.boss.model.entity.Resume;
|
||||
import com.zds.boss.model.entity.User;
|
||||
import com.zds.boss.model.enums.UserRoleEnum;
|
||||
import com.zds.boss.model.vo.ResumeVO;
|
||||
import com.zds.boss.service.CosService;
|
||||
import com.zds.boss.service.FileService;
|
||||
import com.zds.boss.service.ResumeAddressService;
|
||||
import com.zds.boss.service.ResumeService;
|
||||
import com.zds.boss.service.UserService;
|
||||
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.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -44,6 +45,12 @@ public class ResumeController {
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
|
||||
@Resource
|
||||
private CosService cosService;
|
||||
|
||||
@Resource
|
||||
private ResumeAddressService resumeAddressService;
|
||||
|
||||
@Value("${file.max-size-mb:10}")
|
||||
private long maxSizeMb;
|
||||
|
||||
@@ -158,14 +165,18 @@ public class ResumeController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传简历附件文件
|
||||
* 上传简历附件文件到腾讯云COS
|
||||
*
|
||||
* @param file 文件
|
||||
* @param resumeId 简历ID(可选,如果传入则同时更新resume的attachment_url)
|
||||
* @param request HTTP请求
|
||||
* @return 文件访问URL
|
||||
*/
|
||||
@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()) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件不能为空");
|
||||
}
|
||||
@@ -192,36 +203,104 @@ public class ResumeController {
|
||||
extension = originalFilename.substring(lastDotIndex).toLowerCase();
|
||||
}
|
||||
|
||||
// 验证文件类型(允许PDF和Word文档)
|
||||
// 验证文件类型(仅允许PDF)
|
||||
String contentType = file.getContentType();
|
||||
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);
|
||||
boolean isAllowedContentType = "application/pdf".equals(contentType);
|
||||
boolean isAllowedExtension = ".pdf".equals(extension);
|
||||
if (!isAllowedContentType && !isAllowedExtension) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "仅支持PDF和Word文档格式");
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR, "仅支持PDF格式");
|
||||
}
|
||||
|
||||
try {
|
||||
// 生成唯一文件名:用户ID_时间戳_UUID.扩展名
|
||||
// 如果传入了resumeId,检查是否有权限操作该简历
|
||||
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 uniqueFileName = String.format("%d_%s_%s%s",
|
||||
loginUser.getId(), timestamp, UUID.randomUUID().toString().replace("-", ""), extension);
|
||||
String customFileName = String.format("%d_%s_%s",
|
||||
loginUser.getId(), timestamp, fileKey);
|
||||
|
||||
// 构建相对路径:resume/{userId}/{filename}
|
||||
String relativePath = String.format("resume/%d/%s", loginUser.getId(), uniqueFileName);
|
||||
// 构建存储目录:resume/{userId}
|
||||
String directory = String.format("resume/%d", loginUser.getId());
|
||||
|
||||
// 上传文件
|
||||
fileService.upload(relativePath, file);
|
||||
// 上传文件到腾讯云COS
|
||||
String fileUrl = cosService.uploadFile(file, directory, customFileName);
|
||||
|
||||
// 构建文件访问URL
|
||||
String fileUrl = fileService.buildUrl(relativePath);
|
||||
|
||||
log.info("用户 {} 上传文件成功: {}, URL: {}", loginUser.getId(), originalFilename, fileUrl);
|
||||
return ResultUtils.success(fileUrl);
|
||||
} catch (IOException e) {
|
||||
log.error("文件上传失败: {}", e.getMessage(), e);
|
||||
throw new BusinessException(ErrorCode.OPERATION_ERROR, "文件上传失败: " + e.getMessage());
|
||||
// 如果传入了resumeId,更新resume的attachment_url
|
||||
if (resumeId != null && resumeId > 0) {
|
||||
Resume resume = new Resume();
|
||||
resume.setId(resumeId);
|
||||
resume.setAttachmentUrl(fileUrl);
|
||||
resumeService.updateById(resume);
|
||||
log.info("已更新简历 {} 的附件URL", resumeId);
|
||||
}
|
||||
|
||||
// 保存或更新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);
|
||||
}
|
||||
}
|
||||
|
||||
159
BOSSBackEnd/src/main/java/com/zds/boss/service/CosService.java
Normal file
159
BOSSBackEnd/src/main/java/com/zds/boss/service/CosService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -71,3 +71,16 @@ file:
|
||||
path: src/main/resources/static
|
||||
# 最大文件大小(MB)
|
||||
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
|
||||
@@ -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 }) {
|
||||
return request<API.BaseResponseString>('/resume/upload', {
|
||||
method: 'POST',
|
||||
@@ -82,3 +82,17 @@ export async function uploadFile(body: {}, options?: { [key: string]: any }) {
|
||||
...(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 || {}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -35,26 +35,26 @@
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-card title="我的简历PDF" :bordered="false" style="margin-top: 16px">
|
||||
<div v-if="addressLoading">
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="myAddress?.address">
|
||||
<a-space>
|
||||
<a :href="myAddress.address" target="_blank">查看PDF</a>
|
||||
<a-popconfirm title="确定删除该PDF吗?" @confirm="deleteAddress">
|
||||
<a-button danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-upload :before-upload="beforeUpload" :show-upload-list="false" :custom-request="(opt:any)=>doUpload(opt.file)">
|
||||
<a-button type="primary">上传PDF</a-button>
|
||||
</a-upload>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
<!-- <a-card title="我的简历PDF" :bordered="false" style="margin-top: 16px">-->
|
||||
<!-- <div v-if="addressLoading">-->
|
||||
<!-- <a-spin />-->
|
||||
<!-- </div>-->
|
||||
<!-- <div v-else>-->
|
||||
<!-- <div v-if="myAddress?.address">-->
|
||||
<!-- <a-space>-->
|
||||
<!-- <a :href="myAddress.address" target="_blank">查看PDF</a>-->
|
||||
<!-- <a-popconfirm title="确定删除该PDF吗?" @confirm="deleteAddress">-->
|
||||
<!-- <a-button danger>删除</a-button>-->
|
||||
<!-- </a-popconfirm>-->
|
||||
<!-- </a-space>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div v-else>-->
|
||||
<!-- <a-upload :before-upload="beforeUpload" :show-upload-list="false" :custom-request="(opt:any)=>doUpload(opt.file)">-->
|
||||
<!-- <a-button type="primary">上传PDF</a-button>-->
|
||||
<!-- </a-upload>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </a-card>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
import NavBar from '@/components/NavBar.vue';
|
||||
import { ref, onMounted, reactive } from 'vue';
|
||||
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 { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
:before-upload="beforeUpload"
|
||||
:show-upload-list="false"
|
||||
:custom-request="handleUpload"
|
||||
accept=".pdf,.doc,.docx"
|
||||
accept=".pdf"
|
||||
>
|
||||
<a-button type="primary">
|
||||
<UploadOutlined />
|
||||
上传简历附件(PDF/Word)
|
||||
上传简历PDF
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div v-if="uploading" style="color: #1890ff">
|
||||
@@ -50,15 +50,17 @@
|
||||
<div v-if="formState.attachmentUrl" style="margin-top: 8px">
|
||||
<a-space>
|
||||
<a :href="formState.attachmentUrl" target="_blank" style="color: #1890ff">
|
||||
<FileTextOutlined />
|
||||
查看附件
|
||||
<FilePdfOutlined />
|
||||
查看PDF简历
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
<div v-if="!formState.attachmentUrl && !uploading" style="color: #999; font-size: 12px">
|
||||
支持PDF和Word格式,最大10MB
|
||||
仅支持PDF格式,最大10MB(文件将上传至腾讯云COS)
|
||||
</div>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
@@ -80,9 +82,9 @@
|
||||
import NavBar from '@/components/NavBar.vue';
|
||||
import { reactive, ref, onMounted, computed } from 'vue';
|
||||
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 { UploadOutlined, FileTextOutlined } from '@ant-design/icons-vue';
|
||||
import { UploadOutlined, FilePdfOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -132,14 +134,12 @@ const extractFileName = (url: string) => {
|
||||
|
||||
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 isAllowedExtension = extension === 'pdf';
|
||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||
|
||||
if (!isPdf && !isWord && !isAllowedExtension) {
|
||||
message.error('仅支持PDF和Word文档格式');
|
||||
if (!isPdf && !isAllowedExtension) {
|
||||
message.error('仅支持PDF格式');
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
if (!isLt10M) {
|
||||
@@ -157,6 +157,11 @@ const handleUpload = async (options: any) => {
|
||||
const formData = new FormData();
|
||||
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 xhr = new XMLHttpRequest();
|
||||
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 = '';
|
||||
uploadFileName.value = '';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user