新增文件可下载标识、加密短信验证支持等
This commit is contained in:
@@ -340,7 +340,8 @@ CREATE TABLE infra_file (
|
|||||||
updater varchar(64) DEFAULT '' NULL,
|
updater varchar(64) DEFAULT '' NULL,
|
||||||
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
deleted bit DEFAULT '0' NOT NULL,
|
deleted bit DEFAULT '0' NOT NULL,
|
||||||
DOWNLOAD_COUNT INT DEFAULT 0 NOT NULL
|
DOWNLOAD_COUNT INT DEFAULT 0 NOT NULL,
|
||||||
|
DOWNLOADABLE SMALLINT DEFAULT 1 NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
COMMENT ON COLUMN infra_file.id IS '文件编号';
|
COMMENT ON COLUMN infra_file.id IS '文件编号';
|
||||||
@@ -358,6 +359,7 @@ COMMENT ON COLUMN infra_file.updater IS '更新者';
|
|||||||
COMMENT ON COLUMN infra_file.update_time IS '更新时间';
|
COMMENT ON COLUMN infra_file.update_time IS '更新时间';
|
||||||
COMMENT ON COLUMN infra_file.deleted IS '是否删除';
|
COMMENT ON COLUMN infra_file.deleted IS '是否删除';
|
||||||
COMMENT ON COLUMN INFRA_FILE.DOWNLOAD_COUNT IS '下载次数';
|
COMMENT ON COLUMN INFRA_FILE.DOWNLOAD_COUNT IS '下载次数';
|
||||||
|
COMMENT ON COLUMN INFRA_FILE.DOWNLOADABLE IS '是否可下载(1是,0否)';
|
||||||
COMMENT ON TABLE infra_file IS '文件表';
|
COMMENT ON TABLE infra_file IS '文件表';
|
||||||
|
|
||||||
CREATE INDEX idx_infra_file_hash ON infra_file(hash);
|
CREATE INDEX idx_infra_file_hash ON infra_file(hash);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.zt.plat.framework.common.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码发送方式
|
||||||
|
*/
|
||||||
|
public enum VerifyCodeSendType {
|
||||||
|
SMS, // 短信验证码
|
||||||
|
E_OFFICE // e办消息推送
|
||||||
|
}
|
||||||
@@ -22,4 +22,6 @@ public class FileCreateReqDTO {
|
|||||||
@NotEmpty(message = "文件内容不能为空")
|
@NotEmpty(message = "文件内容不能为空")
|
||||||
private byte[] content;
|
private byte[] content;
|
||||||
|
|
||||||
|
@Schema(description = "是否可下载(true是,false否)", example = "true")
|
||||||
|
private Boolean downloadable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,7 @@ public class FileRespDTO {
|
|||||||
@Schema(description = "文件下载次数")
|
@Schema(description = "文件下载次数")
|
||||||
private Integer downloadCount;
|
private Integer downloadCount;
|
||||||
|
|
||||||
|
@Schema(description = "是否可下载(true是,false否)")
|
||||||
|
private Boolean downloadable;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.zt.plat.module.infra.controller.admin.file;
|
|||||||
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.IoUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.core.util.URLUtil;
|
import cn.hutool.core.util.URLUtil;
|
||||||
|
import com.zt.plat.framework.common.enums.VerifyCodeSendType;
|
||||||
import com.zt.plat.framework.common.exception.ServiceException;
|
import com.zt.plat.framework.common.exception.ServiceException;
|
||||||
import com.zt.plat.framework.common.pojo.CommonResult;
|
import com.zt.plat.framework.common.pojo.CommonResult;
|
||||||
import com.zt.plat.framework.common.pojo.PageResult;
|
import com.zt.plat.framework.common.pojo.PageResult;
|
||||||
@@ -21,6 +22,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
@@ -43,12 +45,17 @@ import static com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils.w
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class FileController {
|
public class FileController {
|
||||||
|
|
||||||
|
@Value("${zt.file.preview-base-url:}")
|
||||||
|
private String previewBaseUrl;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private FileService fileService;
|
private FileService fileService;
|
||||||
|
|
||||||
@GetMapping("/get")
|
@GetMapping("/get")
|
||||||
@Operation(summary = "获取文件预览地址", description = "根据 fileId 返回文件预览 url(kkfile)")
|
@Operation(summary = "获取文件预览地址", description = "根据 fileId 返回文件预览 url(kkfile)")
|
||||||
public CommonResult<FileRespVO> getPreviewUrl(@RequestParam("fileId") Long fileId) {
|
public CommonResult<FileRespVO> getPreviewUrl(@RequestParam("fileId") Long fileId,
|
||||||
|
@RequestParam(value = "code", required = false) String code,
|
||||||
|
HttpServletRequest request) throws Exception {
|
||||||
FileDO fileDO = fileService.getActiveFileById(fileId);
|
FileDO fileDO = fileService.getActiveFileById(fileId);
|
||||||
if (fileDO == null) {
|
if (fileDO == null) {
|
||||||
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
|
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
|
||||||
@@ -59,6 +66,27 @@ public class FileController {
|
|||||||
|
|
||||||
// FileDO 转换为 FileRespVO
|
// FileDO 转换为 FileRespVO
|
||||||
FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class);
|
FileRespVO fileRespVO = BeanUtils.toBean(fileDO, FileRespVO.class);
|
||||||
|
|
||||||
|
// 加密文件:塞入“临时解密预览 URL”
|
||||||
|
if (Boolean.TRUE.equals(fileRespVO.getIsEncrypted())) { // FileDO 通过 aesIv 判断加密
|
||||||
|
|
||||||
|
if (cn.hutool.core.util.StrUtil.isBlank(code)) {
|
||||||
|
return CommonResult.error(HttpStatus.BAD_REQUEST.value(), "加密文件预览需要验证码 code");
|
||||||
|
}
|
||||||
|
// 验证通过:发放给 kkfile 用的短期 token(kkfile 不带登录态)
|
||||||
|
Long userId = getLoginUserId();
|
||||||
|
boolean flag = fileService.verifyCode(fileId, userId, code);
|
||||||
|
if(!flag){
|
||||||
|
return CommonResult.customize(null, HttpStatus.INTERNAL_SERVER_ERROR.value(), "验证码错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = fileService.generatePreviewToken(fileId, userId);
|
||||||
|
|
||||||
|
String baseUrl = buildPublicBaseUrl(request); // 见下方函数
|
||||||
|
String decryptUrl = baseUrl + "/admin-api/infra/file/preview-decrypt?fileId=" + fileId + "&token=" + token;
|
||||||
|
fileRespVO.setUrl(decryptUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return success(fileRespVO);
|
return success(fileRespVO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,15 +194,32 @@ public class FileController {
|
|||||||
}
|
}
|
||||||
@GetMapping("/generate-download-code")
|
@GetMapping("/generate-download-code")
|
||||||
@Operation(summary = "获取下载验证码")
|
@Operation(summary = "获取下载验证码")
|
||||||
public CommonResult<FileRespVO> preDownloadEncrypt(@RequestParam("fileId") Long fileId) {
|
public CommonResult<FileRespVO> preDownloadEncrypt(@RequestParam("fileId") Long fileId,
|
||||||
|
@RequestParam(value = "sendType", required = false) String sendType // 可选:SMS / E_OFFICE
|
||||||
|
) {
|
||||||
Long userId = getLoginUserId();
|
Long userId = getLoginUserId();
|
||||||
|
|
||||||
|
// 解析 sendType(允许为空)
|
||||||
|
VerifyCodeSendType sendTypeEnum = null;
|
||||||
|
if (sendType != null && !sendType.trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
sendTypeEnum = VerifyCodeSendType.valueOf(sendType.trim().toUpperCase());
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return CommonResult.error(HttpStatus.BAD_REQUEST.value(),
|
||||||
|
"sendType 参数不合法,可选:SMS / E_OFFICE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FileDO activeFileById = fileService.getActiveFileById(fileId);
|
FileDO activeFileById = fileService.getActiveFileById(fileId);
|
||||||
if (activeFileById == null) {
|
if (activeFileById == null) {
|
||||||
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
|
return CommonResult.error(HttpStatus.NOT_FOUND.value(), "文件不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
FileRespVO fileRespVO = BeanUtils.toBean(activeFileById, FileRespVO.class);
|
FileRespVO fileRespVO = BeanUtils.toBean(activeFileById, FileRespVO.class);
|
||||||
try {
|
try {
|
||||||
fileService.generateFileVerificationCode(fileId, userId);
|
String code = fileService.generateFileVerificationCode(fileId, userId);
|
||||||
|
if(sendTypeEnum != null)
|
||||||
|
fileService.sendVerifyCode(code, sendTypeEnum); // 发送验证码
|
||||||
return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), "验证码已生成,请使用验证码下载文件");
|
return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), "验证码已生成,请使用验证码下载文件");
|
||||||
} catch (ServiceException e) {
|
} catch (ServiceException e) {
|
||||||
return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), e.getMessage());
|
return CommonResult.customize(fileRespVO, HttpStatus.OK.value(), e.getMessage());
|
||||||
@@ -191,4 +236,53 @@ public class FileController {
|
|||||||
}
|
}
|
||||||
return CommonResult.customize(null, HttpStatus.OK.value(), "验证码校验通过");
|
return CommonResult.customize(null, HttpStatus.OK.value(), "验证码校验通过");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/preview-decrypt")
|
||||||
|
@PermitAll
|
||||||
|
@TenantIgnore
|
||||||
|
@Operation(summary = "加密文件预览解密流(供 kkfile 拉取)")
|
||||||
|
public void previewDecrypt(@RequestParam("fileId") Long fileId,
|
||||||
|
@RequestParam("token") String token,
|
||||||
|
HttpServletResponse response) throws Exception {
|
||||||
|
|
||||||
|
boolean ok = fileService.verifyPreviewToken(fileId, token);
|
||||||
|
if (!ok) {
|
||||||
|
response.setStatus(HttpStatus.FORBIDDEN.value());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileDO fileDO = fileService.getActiveFileById(fileId);
|
||||||
|
if (fileDO == null) {
|
||||||
|
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// byte[] content = fileService.getDecryptedBytes(fileId);
|
||||||
|
response.setHeader("Cache-Control", "no-store");
|
||||||
|
response.setContentType(fileDO.getType());
|
||||||
|
|
||||||
|
String filename = java.net.URLEncoder.encode(fileDO.getName(), java.nio.charset.StandardCharsets.UTF_8);
|
||||||
|
response.setHeader("Content-Disposition", "inline; filename*=UTF-8''" + filename);
|
||||||
|
|
||||||
|
// cn.hutool.core.io.IoUtil.write(response.getOutputStream(), true, content);
|
||||||
|
fileService.writeDecryptedToStream(fileId, response.getOutputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildPublicBaseUrl(HttpServletRequest request) {
|
||||||
|
if (previewBaseUrl != null && !previewBaseUrl.isBlank()) {
|
||||||
|
return previewBaseUrl.endsWith("/")
|
||||||
|
? previewBaseUrl.substring(0, previewBaseUrl.length() - 1)
|
||||||
|
: previewBaseUrl;
|
||||||
|
}
|
||||||
|
// 兜底:从请求推断
|
||||||
|
String scheme = request.getHeader("X-Forwarded-Proto");
|
||||||
|
if (scheme == null) scheme = request.getScheme();
|
||||||
|
|
||||||
|
String host = request.getHeader("X-Forwarded-Host");
|
||||||
|
if (host == null) host = request.getHeader("Host");
|
||||||
|
if (host == null) host = request.getServerName() + ":" + request.getServerPort();
|
||||||
|
|
||||||
|
return scheme + "://" + host;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import java.util.Date;
|
|||||||
@Accessors(chain = true)
|
@Accessors(chain = true)
|
||||||
public class FileRespVO {
|
public class FileRespVO {
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
// 加密附件不返回 url
|
// 不可下载 或 加密附件不返回 url
|
||||||
if (Boolean.TRUE.equals(this.isEncrypted)) {
|
if (Boolean.FALSE.equals(this.downloadable) || Boolean.TRUE.equals(this.isEncrypted)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 如果 url 已经是临时下载地址(如预签名 URL),直接返回
|
// 如果 url 已经是临时下载地址(如预签名 URL),直接返回
|
||||||
@@ -62,8 +62,8 @@ public class FileRespVO {
|
|||||||
private String previewUrl;
|
private String previewUrl;
|
||||||
|
|
||||||
public String getPreviewUrl() {
|
public String getPreviewUrl() {
|
||||||
// 加密附件不返回 previewUrl
|
// 不可下载不返回 previewUrl
|
||||||
if (Boolean.TRUE.equals(this.isEncrypted)) {
|
if (Boolean.FALSE.equals(this.downloadable) ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 仅当 url 不为空时生成
|
// 仅当 url 不为空时生成
|
||||||
@@ -75,7 +75,15 @@ public class FileRespVO {
|
|||||||
if (onlinePreview == null || onlinePreview.isEmpty()) {
|
if (onlinePreview == null || onlinePreview.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String presignedUrl = this.getUrl();
|
// 添加加密文件预览逻辑
|
||||||
|
String presignedUrl = null;
|
||||||
|
if (Boolean.TRUE.equals(this.isEncrypted)) {
|
||||||
|
if (url != null && (url.startsWith("http://") || url.startsWith("https://"))) {
|
||||||
|
presignedUrl = url;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
presignedUrl = this.getUrl();
|
||||||
|
}
|
||||||
if (presignedUrl == null || presignedUrl.isEmpty()) {
|
if (presignedUrl == null || presignedUrl.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -102,4 +110,6 @@ public class FileRespVO {
|
|||||||
@Schema(description = "下载次数")
|
@Schema(description = "下载次数")
|
||||||
private Integer downloadCount;
|
private Integer downloadCount;
|
||||||
|
|
||||||
|
@Schema(description = "是否可下载(true是,false否)")
|
||||||
|
private Boolean downloadable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ public class FileDO extends BaseDO {
|
|||||||
*/
|
*/
|
||||||
private Integer downloadCount;
|
private Integer downloadCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否可下载(true是,false否)
|
||||||
|
*/
|
||||||
|
private Boolean downloadable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否加密
|
* 是否加密
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@@ -6,4 +6,7 @@ package com.zt.plat.module.infra.dal.redis;
|
|||||||
public class RedisKeyConstants {
|
public class RedisKeyConstants {
|
||||||
public static final String FILE_VERIFICATION_CODE = "infra:file:verification_code:%d:%d";
|
public static final String FILE_VERIFICATION_CODE = "infra:file:verification_code:%d:%d";
|
||||||
public static final String FILE_VERIFICATION_CODE_USER_SET = "infra:file:verification_code:user:%d";
|
public static final String FILE_VERIFICATION_CODE_USER_SET = "infra:file:verification_code:user:%d";
|
||||||
|
|
||||||
|
// 加密文件预览token
|
||||||
|
public static final String FILE_PREVIEW_TOKEN = "infra:file:preview-token:%s";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package com.zt.plat.module.infra.framework.rpc.config;
|
|||||||
|
|
||||||
import com.zt.plat.module.system.api.permission.PermissionApi;
|
import com.zt.plat.module.system.api.permission.PermissionApi;
|
||||||
import com.zt.plat.module.system.api.permission.RoleApi;
|
import com.zt.plat.module.system.api.permission.RoleApi;
|
||||||
|
import com.zt.plat.module.system.api.sms.SmsSendApi;
|
||||||
import com.zt.plat.module.system.api.user.AdminUserApi;
|
import com.zt.plat.module.system.api.user.AdminUserApi;
|
||||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration(value = "infraRpcConfiguration", proxyBeanMethods = false)
|
@Configuration(value = "infraRpcConfiguration", proxyBeanMethods = false)
|
||||||
@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class})
|
@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class, SmsSendApi.class })
|
||||||
public class RpcConfiguration {
|
public class RpcConfiguration {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.zt.plat.module.infra.service.file;
|
package com.zt.plat.module.infra.service.file;
|
||||||
|
|
||||||
|
import com.zt.plat.framework.common.enums.VerifyCodeSendType;
|
||||||
import com.zt.plat.framework.common.pojo.PageResult;
|
import com.zt.plat.framework.common.pojo.PageResult;
|
||||||
import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||||
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||||
@@ -10,6 +11,8 @@ import com.zt.plat.module.infra.dal.dataobject.file.FileDO;
|
|||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件 Service 接口
|
* 文件 Service 接口
|
||||||
*
|
*
|
||||||
@@ -72,6 +75,14 @@ public interface FileService {
|
|||||||
*/
|
*/
|
||||||
String generateFileVerificationCode(Long fileId, Long userId);
|
String generateFileVerificationCode(Long fileId, Long userId);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送验证码
|
||||||
|
* @param code 验证码
|
||||||
|
* @param verifyCodeSendType 发送类型
|
||||||
|
*/
|
||||||
|
void sendVerifyCode(String code, VerifyCodeSendType verifyCodeSendType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验验证码并返回解密后的文件内容
|
* 校验验证码并返回解密后的文件内容
|
||||||
*/
|
*/
|
||||||
@@ -125,4 +136,25 @@ public interface FileService {
|
|||||||
* @param fileId
|
* @param fileId
|
||||||
*/
|
*/
|
||||||
void incDownloadCount(Long fileId);
|
void incDownloadCount(Long fileId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临时生成文件预览token
|
||||||
|
* @param fileId 文件ID
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 临时token
|
||||||
|
*/
|
||||||
|
String generatePreviewToken(Long fileId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文件预览token
|
||||||
|
* @param fileId 文件ID
|
||||||
|
* @param token 用户ID
|
||||||
|
* @return 临时token
|
||||||
|
*/
|
||||||
|
boolean verifyPreviewToken(Long fileId, String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验预览 token 后,将文件内容解密并写入输出流(用于预览)
|
||||||
|
*/
|
||||||
|
void writeDecryptedToStream(Long fileId, OutputStream outputStream) throws Exception;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package com.zt.plat.module.infra.service.file;
|
|||||||
|
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||||
import cn.hutool.core.io.FileUtil;
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
import cn.hutool.core.lang.Assert;
|
import cn.hutool.core.lang.Assert;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.hutool.crypto.digest.DigestUtil;
|
import cn.hutool.crypto.digest.DigestUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.zt.plat.framework.common.enums.VerifyCodeSendType;
|
||||||
import com.zt.plat.framework.common.pojo.PageResult;
|
import com.zt.plat.framework.common.pojo.PageResult;
|
||||||
import com.zt.plat.framework.common.util.object.BeanUtils;
|
import com.zt.plat.framework.common.util.object.BeanUtils;
|
||||||
|
import com.zt.plat.framework.security.core.LoginUser;
|
||||||
|
import com.zt.plat.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
import com.zt.plat.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||||
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||||
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
|
import com.zt.plat.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
|
||||||
@@ -21,6 +25,9 @@ import com.zt.plat.module.infra.framework.file.core.client.FileClient;
|
|||||||
import com.zt.plat.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
|
import com.zt.plat.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO;
|
||||||
import com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils;
|
import com.zt.plat.module.infra.framework.file.core.utils.FileTypeUtils;
|
||||||
import com.zt.plat.module.infra.util.VerificationCodeUtil;
|
import com.zt.plat.module.infra.util.VerificationCodeUtil;
|
||||||
|
import com.zt.plat.module.system.api.permission.RoleApi;
|
||||||
|
import com.zt.plat.module.system.api.sms.SmsSendApi;
|
||||||
|
import com.zt.plat.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -31,10 +38,9 @@ import org.springframework.stereotype.Service;
|
|||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Base64;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
|
import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
|
||||||
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static com.zt.plat.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
@@ -51,8 +57,17 @@ public class FileServiceImpl implements FileService {
|
|||||||
|
|
||||||
@Value("${zt.AES.key}")
|
@Value("${zt.AES.key}")
|
||||||
private String aesKey;
|
private String aesKey;
|
||||||
@Value("${zt.verify-code:}")
|
@Value("${zt.verify-code:666666}")
|
||||||
private String fixedVerifyCode;
|
private String fixedVerifyCode;
|
||||||
|
// 加密文件预览token过期时间
|
||||||
|
@Value("${zt.file.preview-expire-seconds:300}")
|
||||||
|
private Integer previewExpireSeconds;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RoleApi roleApi;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SmsSendApi smsSendApi;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private StringRedisTemplate stringRedisTemplate;
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
@@ -65,6 +80,31 @@ public class FileServiceImpl implements FileService {
|
|||||||
return VerificationCodeUtil.generateCode(codeKey, userSetKey, stringRedisTemplate);
|
return VerificationCodeUtil.generateCode(codeKey, userSetKey, stringRedisTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sendVerifyCode(String code, VerifyCodeSendType verifyCodeSendType) {
|
||||||
|
if(verifyCodeSendType == null) return;
|
||||||
|
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||||
|
Assert.notNull(loginUser,"用户未登录或权限不足!");
|
||||||
|
if(loginUser == null) return;
|
||||||
|
if (VerifyCodeSendType.SMS.equals(verifyCodeSendType)) {
|
||||||
|
Map<String, Object> templateParams = new HashMap<>();
|
||||||
|
templateParams.put("code",code);
|
||||||
|
SmsSendSingleToUserReqDTO smsSendReqDTO = new SmsSendSingleToUserReqDTO();
|
||||||
|
if(loginUser.getInfo().get(LoginUser.INFO_KEY_PHONE)!=null)
|
||||||
|
smsSendReqDTO.setMobile(loginUser.getInfo().get(LoginUser.INFO_KEY_PHONE));
|
||||||
|
smsSendReqDTO.setUserId(loginUser.getId());
|
||||||
|
smsSendReqDTO.setTemplateCode("test_02");
|
||||||
|
smsSendReqDTO.setTemplateParams(templateParams);
|
||||||
|
smsSendApi.sendSingleSmsToAdmin(smsSendReqDTO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VerifyCodeSendType.E_OFFICE.equals(verifyCodeSendType)) {
|
||||||
|
// TODO 预留实现接口
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] verifyCodeAndGetFile(Long fileId, Long userId, String code) throws Exception {
|
public byte[] verifyCodeAndGetFile(Long fileId, Long userId, String code) throws Exception {
|
||||||
// 开发模式下,验证码直接获取配置进行比对
|
// 开发模式下,验证码直接获取配置进行比对
|
||||||
@@ -349,4 +389,36 @@ public class FileServiceImpl implements FileService {
|
|||||||
fileMapper.incDownloadCount(fileId);
|
fileMapper.incDownloadCount(fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String generatePreviewToken(Long fileId, Long userId) {
|
||||||
|
// 你也可以加:validateFileExists(fileId)
|
||||||
|
String token = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
String key = String.format(RedisKeyConstants.FILE_PREVIEW_TOKEN, token);
|
||||||
|
stringRedisTemplate.opsForValue().set(key, String.valueOf(fileId),
|
||||||
|
java.time.Duration.ofSeconds(previewExpireSeconds));
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean verifyPreviewToken(Long fileId, String token) {
|
||||||
|
String key = String.format(RedisKeyConstants.FILE_PREVIEW_TOKEN, token);
|
||||||
|
String val = stringRedisTemplate.opsForValue().get(key);
|
||||||
|
if (val == null || !val.equals(String.valueOf(fileId))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 可选:单次使用更安全
|
||||||
|
// stringRedisTemplate.delete(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeDecryptedToStream(Long fileId, OutputStream os) throws Exception {
|
||||||
|
FileDO fileDO = getActiveFileById(fileId);
|
||||||
|
if (fileDO == null) {
|
||||||
|
throw exception(FILE_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decrypted = getDecryptedBytes(fileId);
|
||||||
|
IoUtil.write(os, true, decrypted);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user