mirror of
https://gitee.com/bookshelfplus/bookshelfplus
synced 2025-09-01 22:53:29 +08:00
通过书本ID获取所有文件对象信息;前端书籍详情页面直链下载功能完成;后端COS部分功能完善;下载文件设置"Content-Disposition"为"attachment; filename="
This commit is contained in:
@@ -196,8 +196,8 @@
|
||||
// 由于多个请求之后都需要调用该方法,为避免多次压缩,使用计数器,等最后一个请求完成后执行一次
|
||||
var requestCount = 2;
|
||||
function doFontmin() {
|
||||
console.log("所有请求完毕,开始获取字体");
|
||||
if (--requestCount == 0) {
|
||||
console.log("开始获取字体");
|
||||
fontmin(getPageText());
|
||||
}
|
||||
}
|
||||
@@ -373,6 +373,7 @@
|
||||
</script>
|
||||
<!-- 获取文件下载链接信息 -->
|
||||
<script>
|
||||
var direckLinkInfo = {};
|
||||
// 获取文件信息
|
||||
function getFileInfo() {
|
||||
function stringifyFileSize(nBytes = 0) {
|
||||
@@ -384,6 +385,20 @@
|
||||
}
|
||||
return sOutput;
|
||||
}
|
||||
function getLinkDOM(fileObjectInfo) {
|
||||
// 获取文件下载链接DOM
|
||||
var div = document.createElement("div");
|
||||
div.className = "file-object-item";
|
||||
switch (fileObjectInfo.storageMediumType) {
|
||||
case "腾讯云对象存储":
|
||||
div.innerHTML = `<a style="cursor: pointer;" onclick="getDirectLink(${fileObjectInfo.id});">直链下载</a>`;
|
||||
break;
|
||||
default:
|
||||
div.innerHTML = `<a href="${fileObjectInfo.filePath}" target="_blank">${fileObjectInfo.storageMediumType}${fileShareCode == "" ? "" : "(提取码:" + fileShareCode + ")"}</a>`;
|
||||
break;
|
||||
}
|
||||
return div.outerHTML;
|
||||
}
|
||||
getRequest("/file/getFile", { bookId: bookId })
|
||||
.then(function (response) {
|
||||
var axiosData = response.data;
|
||||
@@ -392,10 +407,23 @@
|
||||
if (status === "success") {
|
||||
console.log(data);
|
||||
|
||||
// 先处理文件对象
|
||||
var fileObjectList = {};
|
||||
for (let i = 0; i < data.fileObject.length; i++) {
|
||||
const element = data.fileObject[i];
|
||||
if (!fileObjectList[element.fileId])
|
||||
fileObjectList[element.fileId] = [];
|
||||
fileObjectList[element.fileId].push(getLinkDOM(element));
|
||||
|
||||
direckLinkInfo[element.id] = element;
|
||||
}
|
||||
console.log(fileObjectList);
|
||||
|
||||
// 再处理文件
|
||||
var fileContainer = document.getElementById("file-container");
|
||||
var innerHTML = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const fileInfo = data[i];
|
||||
for (let i = 0; i < data.file.length; i++) {
|
||||
const fileInfo = data.file[i];
|
||||
innerHTML.push(`
|
||||
<div class="file-item">
|
||||
<div class="file-title">
|
||||
@@ -408,15 +436,7 @@
|
||||
<span class="file-copyright size14" style="margin-top: 12px; display: block;">下载地址:</span>
|
||||
</div>
|
||||
<div class="file-object">
|
||||
<div class="file-object-item">
|
||||
<a href="" target="_blank">百度网盘</a>
|
||||
</div>
|
||||
<div class="file-object-item">
|
||||
<a href="" target="_blank">阿里云盘</a>
|
||||
</div>
|
||||
<div class="file-object-item">
|
||||
<a href="" download="文件名.pdf">直链下载</a>
|
||||
</div>
|
||||
${fileObjectList[fileInfo.id] ? fileObjectList[fileInfo.id].join('') : ""}
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
@@ -442,5 +462,40 @@
|
||||
}
|
||||
getFileInfo();
|
||||
</script>
|
||||
<script>
|
||||
// 获取直链下载链接,并下载该文件
|
||||
function getDirectLink(fileObjectId) {
|
||||
var fileObject = direckLinkInfo[fileObjectId];
|
||||
console.log("fileObject", fileObject);
|
||||
|
||||
if(!localStorageUtils.getLoginStatus()) {
|
||||
alert("请先登录!");
|
||||
return;
|
||||
}
|
||||
|
||||
var fileNameForUser = `${fileObject.fileName}.${fileObject.fileType}`;
|
||||
var fileKeyForCos = fileObject.filePath;
|
||||
postRequest("/file/cos/get", { fileSha1: fileObject.fileSha1, fileNameAndExt: fileNameForUser, token: localStorageUtils.getToken(), expireMinute: 30 })
|
||||
.then(function (response) {
|
||||
var axiosData = response.data;
|
||||
var status = axiosData.status;
|
||||
var data = axiosData.data;
|
||||
if (status === "success") {
|
||||
console.log(data);
|
||||
// let guid = data.guid;
|
||||
var downloadLink = document.createElement("a");
|
||||
downloadLink.href = data.url;
|
||||
downloadLink.target = "_blank";
|
||||
console.log(downloadLink);
|
||||
downloadLink.click();
|
||||
} else {
|
||||
alert(`出错啦!${data.errMsg} (错误码: ${data.errCode}) `);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
console.log(error);
|
||||
alert("无法连接到服务器,请检查网络连接!");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@@ -2,10 +2,13 @@ package plus.bookshelf.Common.FileManager;
|
||||
|
||||
import com.qcloud.cos.COSClient;
|
||||
import com.qcloud.cos.ClientConfig;
|
||||
import com.qcloud.cos.Headers;
|
||||
import com.qcloud.cos.auth.BasicCOSCredentials;
|
||||
import com.qcloud.cos.auth.COSCredentials;
|
||||
import com.qcloud.cos.http.HttpMethodName;
|
||||
import com.qcloud.cos.http.HttpProtocol;
|
||||
import com.qcloud.cos.model.GeneratePresignedUrlRequest;
|
||||
import com.qcloud.cos.model.ResponseHeaderOverrides;
|
||||
import com.qcloud.cos.region.Region;
|
||||
import plus.bookshelf.Common.Error.BusinessErrorCode;
|
||||
import plus.bookshelf.Common.Error.BusinessException;
|
||||
@@ -86,7 +89,7 @@ public class QCloudCosUtils {
|
||||
// 存储桶的命名格式为 BucketName-APPID,此处填写的存储桶名称必须为此格式
|
||||
String bucketName = qCloudCosConfig.getBucketName();
|
||||
// 对象键(Key)是对象在存储桶中的唯一标识。详情请参见 [对象键](https://cloud.tencent.com/document/product/436/13324)
|
||||
String key = qCloudCosConfig.getKeyName() + objectKey;
|
||||
String key = qCloudCosConfig.getKeyName() + folder + objectKey;
|
||||
return cosClient.doesObjectExist(bucketName, key);
|
||||
}
|
||||
|
||||
@@ -104,13 +107,12 @@ public class QCloudCosUtils {
|
||||
public String generatePresignedUrl(Integer userId, HttpMethodName httpMethodName, String savePath, String objectKey, Integer expireMinute, String urlGUID) throws BusinessException {
|
||||
// 调用 COS 接口之前必须保证本进程存在一个 COSClient 实例,如果没有则创建
|
||||
// 详细代码参见本页:简单操作 -> 创建 COSClient
|
||||
// COSClient cosClient = createCOSClient();
|
||||
COSClient cosClient = createCOSClient();
|
||||
|
||||
// 存储桶的命名格式为 BucketName-APPID,此处填写的存储桶名称必须为此格式
|
||||
String bucketName = qCloudCosConfig.getBucketName();
|
||||
// 对象键(Key)是对象在存储桶中的唯一标识。详情请参见 [对象键](https://cloud.tencent.com/document/product/436/13324)
|
||||
String key = qCloudCosConfig.getKeyName() + objectKey;
|
||||
String key = qCloudCosConfig.getKeyName() + savePath + objectKey;
|
||||
|
||||
// 设置签名过期时间(可选), 若未进行设置则默认使用 ClientConfig 中的签名过期时间(1小时)
|
||||
// 这里设置签名在 expireMinute 分钟后过期
|
||||
@@ -142,6 +144,61 @@ public class QCloudCosUtils {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成预签名URL (下载使用,用于下载时指定下载的文件名)
|
||||
* <p>
|
||||
* refer: https://cloud.tencent.com/document/product/436/35217
|
||||
*
|
||||
* @param userId 当前登录用户的 id
|
||||
* @param savePath
|
||||
* @param objectKey 文件对象的 key
|
||||
* @param expireMinute 过期时间
|
||||
* @param urlGUID
|
||||
* @param fileNameForUser
|
||||
* @return
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public String generatePresignedUrlForGET(Integer userId, String savePath, String objectKey, Integer expireMinute, String urlGUID, String fileNameForUser) throws BusinessException {
|
||||
// 调用 COS 接口之前必须保证本进程存在一个 COSClient 实例,如果没有则创建
|
||||
// 详细代码参见本页:简单操作 -> 创建 COSClient
|
||||
COSClient cosClient = createCOSClient();
|
||||
|
||||
// 存储桶的命名格式为 BucketName-APPID,此处填写的存储桶名称必须为此格式
|
||||
String bucketName = qCloudCosConfig.getBucketName();
|
||||
// 对象键(Key)是对象在存储桶中的唯一标识。详情请参见 [对象键](https://cloud.tencent.com/document/product/436/13324)
|
||||
String key = qCloudCosConfig.getKeyName() + savePath + objectKey;
|
||||
|
||||
GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(bucketName, key, HttpMethodName.GET);
|
||||
|
||||
// 设置下载时返回的 http 头
|
||||
ResponseHeaderOverrides responseHeaders = new ResponseHeaderOverrides();
|
||||
responseHeaders.setContentDisposition("attachment; filename=\"" + fileNameForUser + "\""); // 作为附件下载;设置返回头部里包含文件名信息
|
||||
responseHeaders.setCacheControl("no-cache");
|
||||
req.setResponseHeaders(responseHeaders);
|
||||
|
||||
// 设置签名过期时间(可选),若未进行设置,则默认使用 ClientConfig 中的签名过期时间(1小时)
|
||||
// 这里设置签名在半个小时后过期
|
||||
Date expirationDate = new Date(System.currentTimeMillis() + 30L * 60L * 1000L);
|
||||
req.setExpiration(expirationDate);
|
||||
|
||||
// 填写本次请求的参数,需与实际请求相同,能够防止用户篡改此签名的 HTTP 请求的参数
|
||||
Map<String, String> params = new HashMap<>();
|
||||
req.addRequestParameter("by", "书栖网 bookshelf.plus");
|
||||
req.addRequestParameter("userId", String.valueOf(userId));
|
||||
req.addRequestParameter("guid", urlGUID);
|
||||
// 填写本次请求的头部
|
||||
// host 必填
|
||||
req.putCustomRequestHeader(Headers.HOST, cosClient.getClientConfig().getEndpointBuilder().buildGeneralApiEndpoint(bucketName));
|
||||
// req.putCustomRequestHeader("header1", "value1");
|
||||
|
||||
URL url = cosClient.generatePresignedUrl(req);
|
||||
|
||||
// 记录用户操作日志
|
||||
cosPresignedUrlGenerateLogService.log(userId, expireMinute, HttpMethodName.GET.name(), key, urlGUID);
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁 CosClient 对象方法
|
||||
*/
|
||||
|
@@ -53,7 +53,7 @@ public class FileController extends BaseController {
|
||||
@ApiOperation(value = "书籍下载页面获取文件提供的下载方式", notes = "")
|
||||
@RequestMapping(value = "getFile", method = {RequestMethod.GET})
|
||||
@ResponseBody
|
||||
public CommonReturnType getFile(@RequestParam(value = "bookId", required = false) Integer bookId) throws BusinessException {
|
||||
public CommonReturnType getFile(@RequestParam(value = "bookId", required = false) Integer bookId) throws BusinessException, InvocationTargetException, IllegalAccessException {
|
||||
|
||||
List<FileModel> fileModels = fileService.getFile(bookId);
|
||||
List<FileVO> fileVOS = new ArrayList<>();
|
||||
@@ -61,7 +61,19 @@ public class FileController extends BaseController {
|
||||
FileVO fileVO = convertFileVOFromModel(fileModel);
|
||||
fileVOS.add(fileVO);
|
||||
}
|
||||
return CommonReturnType.create(fileVOS);
|
||||
|
||||
List<FileObjectModel> fileObjectModels = fileObjectService.getFileObjectByBookId(bookId);
|
||||
List<FileObjectVO> fileObjectVOS = new ArrayList<>();
|
||||
for (FileObjectModel fileObjectModel : fileObjectModels) {
|
||||
FileObjectVO fileObjectVO = convertFileObjectVOFromModel(fileObjectModel);
|
||||
fileObjectVOS.add(fileObjectVO);
|
||||
}
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("file", fileVOS);
|
||||
map.put("fileObject", fileObjectVOS);
|
||||
|
||||
return CommonReturnType.create(map);
|
||||
}
|
||||
|
||||
@ApiOperation(value = "【管理员】查询文件列表", notes = "查询文件列表")
|
||||
@@ -142,16 +154,19 @@ public class FileController extends BaseController {
|
||||
@ResponseBody
|
||||
public CommonReturnType cos(@PathVariable(value = "httpMethod") String httpMethod,
|
||||
@RequestParam(value = "token") String token,
|
||||
@RequestParam(value = "fileName") String fileName, // 不含扩展名
|
||||
@RequestParam(value = "fileSha1") String fileSha1,
|
||||
@RequestParam(value = "expireMinute", required = false) Integer expireMinute,
|
||||
|
||||
// 以下为 PUT 请求必传参数
|
||||
@RequestParam(value = "fileName", required = false) String fileName, // 不含扩展名
|
||||
@RequestParam(value = "fileSize", required = false) Long fileSize,
|
||||
// @RequestParam(value = "fileType", required = false) String fileType,
|
||||
@RequestParam(value = "lastModified", required = false) Long lastModified,
|
||||
@RequestParam(value = "fileSha1", required = false) String fileSha1,
|
||||
@RequestParam(value = "fileExt", required = false) String fileExt,
|
||||
@RequestParam(value = "fileId", required = false) Integer fileId // 关联的文件ID,创建新文件则为0
|
||||
@RequestParam(value = "fileId", required = false) Integer fileId, // 关联的文件ID,创建新文件则为0
|
||||
|
||||
// 以下为 GET 请求必传参数
|
||||
@RequestParam(value = "fileNameAndExt", required = false) String fileNameAndExt
|
||||
) throws BusinessException, InvocationTargetException, IllegalAccessException {
|
||||
if (expireMinute == null) {
|
||||
expireMinute = 30;
|
||||
@@ -180,26 +195,28 @@ public class FileController extends BaseController {
|
||||
|
||||
// 判断对象是否存在
|
||||
Boolean isExist = qCloudCosUtils.doesObjectExist(bookSaveFolder, fileSha1);
|
||||
switch (httpMethodName) {
|
||||
String url = null;
|
||||
switch (httpMethodName) {
|
||||
case PUT:
|
||||
// 上传文件
|
||||
if (isExist) throw new BusinessException(BusinessErrorCode.PARAMETER_VALIDATION_ERROR, "文件已存在");
|
||||
|
||||
fileObjectService.uploadFile(fileId, fileName, bookSaveFolder + fileSha1, fileSize,
|
||||
fileSha1, fileExt, fileName, FileStorageMediumEnum.QCLOUD_COS, "", lastModified);
|
||||
url = qCloudCosUtils.generatePresignedUrl(userModel.getId(), httpMethodName, bookSaveFolder, fileSha1, expireMinute, urlGUID);
|
||||
break;
|
||||
case GET:
|
||||
if (!isExist) throw new BusinessException(BusinessErrorCode.PARAMETER_VALIDATION_ERROR, "文件不存在");
|
||||
url = qCloudCosUtils.generatePresignedUrlForGET(userModel.getId(), bookSaveFolder, fileSha1, expireMinute, urlGUID, fileNameAndExt);
|
||||
break;
|
||||
case DELETE:
|
||||
if (!isExist) throw new BusinessException(BusinessErrorCode.PARAMETER_VALIDATION_ERROR, "文件不存在");
|
||||
url = qCloudCosUtils.generatePresignedUrl(userModel.getId(), httpMethodName, bookSaveFolder, fileSha1, expireMinute, urlGUID);
|
||||
break;
|
||||
default:
|
||||
throw new BusinessException(BusinessErrorCode.PARAMETER_VALIDATION_ERROR, "httpMethod 参数暂不支持");
|
||||
}
|
||||
|
||||
String url = qCloudCosUtils.generatePresignedUrl(userModel.getId(), httpMethodName, bookSaveFolder, fileSha1, 30, urlGUID);
|
||||
|
||||
Map map = new HashMap();
|
||||
map.put("url", url);
|
||||
map.put("guid", urlGUID);
|
||||
|
@@ -67,4 +67,12 @@ public interface FileObjectDOMapper {
|
||||
* @return
|
||||
*/
|
||||
FileObjectDO selectByFilePath(String filePath);
|
||||
|
||||
/**
|
||||
* 通过书本Id获取关联文件,进而获取所有关联文件对应的文件对象
|
||||
*
|
||||
* @param bookId 书本Id
|
||||
* @return
|
||||
*/
|
||||
FileObjectDO[] selectFileObjectByBookId(Integer bookId);
|
||||
}
|
@@ -36,6 +36,25 @@ public class FileObjectServiceImpl implements FileObjectService {
|
||||
@Autowired
|
||||
FileServiceImpl fileService;
|
||||
|
||||
/**
|
||||
* 通过书本Id获取关联文件,进而获取所有关联文件对应的文件对象
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<FileObjectModel> getFileObjectByBookId(Integer bookId) throws InvocationTargetException, IllegalAccessException {
|
||||
|
||||
FileObjectDO[] fileObjectDOS = fileObjectDOMapper.selectFileObjectByBookId(bookId);
|
||||
|
||||
List<FileObjectModel> fileObjectModels = new ArrayList<>();
|
||||
for (FileObjectDO fileObjectDO : fileObjectDOS) {
|
||||
FileObjectModel fileObjectModel = convertFromDataObject(fileObjectDO);
|
||||
fileObjectModels.add(fileObjectModel);
|
||||
}
|
||||
|
||||
return fileObjectModels;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有文件对象
|
||||
*
|
||||
|
@@ -10,6 +10,16 @@ import java.util.List;
|
||||
|
||||
public interface FileObjectService {
|
||||
|
||||
/**
|
||||
* 通过书本Id获取关联文件,进而获取所有关联文件对应的文件对象
|
||||
*
|
||||
* @param bookId
|
||||
* @return
|
||||
* @throws InvocationTargetException
|
||||
* @throws IllegalAccessException
|
||||
*/
|
||||
List<FileObjectModel> getFileObjectByBookId(Integer bookId) throws InvocationTargetException, IllegalAccessException;
|
||||
|
||||
/**
|
||||
* 列出所有文件对象
|
||||
*
|
||||
|
@@ -228,4 +228,11 @@
|
||||
from file_object_info
|
||||
where file_path = #{filePath}
|
||||
</select>
|
||||
<select id="selectFileObjectByBookId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List" />
|
||||
FROM `file_object_info`
|
||||
WHERE file_id IN (SELECT id AS file_id FROM `file_info` WHERE book_id = 1)
|
||||
AND upload_status = 'SUCCESS'
|
||||
</select>
|
||||
</mapper>
|
Reference in New Issue
Block a user