mirror of
https://gitee.com/novel_dev_team/novel-plus
synced 2026-07-20 17:54:59 +08:00
feat(novel-admin): 新增小说下载功能
为满足部分用户将小说下载至手机阅读的需求,新增管理后台小说下载功能。 该功能未在前台开放,主要基于以下考量: 1. 防止用户流失,保障网站留存率及广告收入 2. 避免大量下载请求带来额外的服务器流量与带宽压力 通过后台受限访问的方式,在满足特定需求的同时,兼顾系统稳定性与商业可持续性。
This commit is contained in:
@@ -15,7 +15,7 @@ public class Constant {
|
|||||||
//通知公告阅读状态-已读
|
//通知公告阅读状态-已读
|
||||||
public static int OA_NOTIFY_READ_YES = 1;
|
public static int OA_NOTIFY_READ_YES = 1;
|
||||||
//部门根节点id
|
//部门根节点id
|
||||||
public static Long DEPT_ROOT_ID = 0l;
|
public static Long DEPT_ROOT_ID = 0L;
|
||||||
//缓存方式
|
//缓存方式
|
||||||
public static String CACHE_TYPE_REDIS = "redis";
|
public static String CACHE_TYPE_REDIS = "redis";
|
||||||
|
|
||||||
@@ -23,5 +23,6 @@ public class Constant {
|
|||||||
|
|
||||||
public static final String UPLOAD_FILES_PREFIX = "/files/";
|
public static final String UPLOAD_FILES_PREFIX = "/files/";
|
||||||
|
|
||||||
|
public static final String BOOK_IS_DOWNLOADING_KEY = "bookIsDownloading:";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import java.util.Set;
|
|||||||
public class SortWhitelistUtil {
|
public class SortWhitelistUtil {
|
||||||
|
|
||||||
// 白名单字段
|
// 白名单字段
|
||||||
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "order_num");
|
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "order_num","index_num");
|
||||||
|
|
||||||
// 白名单排序方式
|
// 白名单排序方式
|
||||||
private static final Set<String> ALLOWED_ORDERS = Set.of("asc", "desc");
|
private static final Set<String> ALLOWED_ORDERS = Set.of("asc", "desc");
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
package com.java2nb.novel.controller;
|
package com.java2nb.novel.controller;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.java2nb.common.config.Constant;
|
||||||
|
import com.java2nb.novel.domain.BookContentDO;
|
||||||
|
import com.java2nb.novel.domain.BookIndexDO;
|
||||||
|
import com.java2nb.novel.service.BookContentService;
|
||||||
|
import com.java2nb.novel.service.BookIndexService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -22,6 +36,9 @@ import com.java2nb.common.utils.PageBean;
|
|||||||
import com.java2nb.common.utils.Query;
|
import com.java2nb.common.utils.Query;
|
||||||
import com.java2nb.common.utils.R;
|
import com.java2nb.common.utils.R;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 小说表
|
* 小说表
|
||||||
*
|
*
|
||||||
@@ -30,12 +47,34 @@ import com.java2nb.common.utils.R;
|
|||||||
* @date 2020-12-01 03:49:46
|
* @date 2020-12-01 03:49:46
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/novel/book")
|
@RequestMapping("/novel/book")
|
||||||
public class BookController {
|
public class BookController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private BookService bookService;
|
private BookService bookService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BookIndexService bookIndexService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BookContentService bookContentService;
|
||||||
|
|
||||||
|
|
||||||
|
private final Pattern PATTERN_BR = Pattern.compile("<br\\s*/?>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_NBSP = Pattern.compile(" ", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_ANCHOR_OPEN = Pattern.compile("<a[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_ANCHOR_CLOSE = Pattern.compile("</a>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_DIV_OPEN = Pattern.compile("<div[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_DIV_CLOSE = Pattern.compile("</div>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_EMPTY_PARAGRAPH_WITH_LINK = Pattern.compile("<p[^>]*>[^<]*<a[^>]*>[^<]*</a>\\s*</p>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_P_OPEN = Pattern.compile("<p[^>]*>", Pattern.CASE_INSENSITIVE);
|
||||||
|
private final Pattern PATTERN_P_CLOSE = Pattern.compile("</p>", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
@GetMapping()
|
@GetMapping()
|
||||||
@RequiresPermissions("novel:book:book")
|
@RequiresPermissions("novel:book:book")
|
||||||
String Book() {
|
String Book() {
|
||||||
@@ -66,7 +105,7 @@ public class BookController {
|
|||||||
@GetMapping("/edit/{id}")
|
@GetMapping("/edit/{id}")
|
||||||
@RequiresPermissions("novel:book:edit")
|
@RequiresPermissions("novel:book:edit")
|
||||||
String edit(@PathVariable("id") Long id, Model model) {
|
String edit(@PathVariable("id") Long id, Model model) {
|
||||||
BookDO book = bookService.get(id);
|
BookDO book = bookService.get(id);
|
||||||
model.addAttribute("book", book);
|
model.addAttribute("book", book);
|
||||||
return "novel/book/edit";
|
return "novel/book/edit";
|
||||||
}
|
}
|
||||||
@@ -75,7 +114,7 @@ public class BookController {
|
|||||||
@GetMapping("/detail/{id}")
|
@GetMapping("/detail/{id}")
|
||||||
@RequiresPermissions("novel:book:detail")
|
@RequiresPermissions("novel:book:detail")
|
||||||
String detail(@PathVariable("id") Long id, Model model) {
|
String detail(@PathVariable("id") Long id, Model model) {
|
||||||
BookDO book = bookService.get(id);
|
BookDO book = bookService.get(id);
|
||||||
model.addAttribute("book", book);
|
model.addAttribute("book", book);
|
||||||
return "novel/book/detail";
|
return "novel/book/detail";
|
||||||
}
|
}
|
||||||
@@ -87,7 +126,7 @@ public class BookController {
|
|||||||
@ResponseBody
|
@ResponseBody
|
||||||
@PostMapping("/save")
|
@PostMapping("/save")
|
||||||
@RequiresPermissions("novel:book:add")
|
@RequiresPermissions("novel:book:add")
|
||||||
public R save( BookDO book) {
|
public R save(BookDO book) {
|
||||||
if (bookService.save(book) > 0) {
|
if (bookService.save(book) > 0) {
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
@@ -101,8 +140,8 @@ public class BookController {
|
|||||||
@ResponseBody
|
@ResponseBody
|
||||||
@RequestMapping("/update")
|
@RequestMapping("/update")
|
||||||
@RequiresPermissions("novel:book:edit")
|
@RequiresPermissions("novel:book:edit")
|
||||||
public R update( BookDO book) {
|
public R update(BookDO book) {
|
||||||
bookService.update(book);
|
bookService.update(book);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +152,7 @@ public class BookController {
|
|||||||
@PostMapping("/remove")
|
@PostMapping("/remove")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@RequiresPermissions("novel:book:remove")
|
@RequiresPermissions("novel:book:remove")
|
||||||
public R remove( Long id) {
|
public R remove(Long id) {
|
||||||
if (bookService.remove(id) > 0) {
|
if (bookService.remove(id) > 0) {
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
@@ -128,8 +167,83 @@ public class BookController {
|
|||||||
@ResponseBody
|
@ResponseBody
|
||||||
@RequiresPermissions("novel:book:batchRemove")
|
@RequiresPermissions("novel:book:batchRemove")
|
||||||
public R remove(@RequestParam("ids[]") Long[] ids) {
|
public R remove(@RequestParam("ids[]") Long[] ids) {
|
||||||
bookService.batchRemove(ids);
|
bookService.batchRemove(ids);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小说下载
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/download")
|
||||||
|
public void download(@RequestParam("bookId") Long bookId, @RequestParam("bookName") String bookName,
|
||||||
|
HttpServletResponse resp) {
|
||||||
|
try {
|
||||||
|
OutputStream out = resp.getOutputStream();
|
||||||
|
Boolean success = redisTemplate
|
||||||
|
.opsForValue()
|
||||||
|
.setIfAbsent(Constant.BOOK_IS_DOWNLOADING_KEY + bookId, "1", 10, TimeUnit.MINUTES);
|
||||||
|
if (Boolean.FALSE.equals(success)) {
|
||||||
|
resp.setContentType("text/html;charset=UTF-8");
|
||||||
|
out.write("该小说正在下载中,请稍后重试!".getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//设置响应头,对文件进行url编码
|
||||||
|
bookName = URLEncoder.encode(bookName, StandardCharsets.UTF_8);
|
||||||
|
//解决手机端不能下载附件的问题
|
||||||
|
resp.setContentType("application/octet-stream");
|
||||||
|
resp.setHeader("Content-Disposition", "attachment;filename=" + bookName + ".txt");
|
||||||
|
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("bookId", bookId);
|
||||||
|
params.put("sort", "index_num");
|
||||||
|
params.put("order", "asc");
|
||||||
|
params.put("limit", 9999);
|
||||||
|
params.put("offset", 0);
|
||||||
|
List<BookIndexDO> bookIndexBigList = bookIndexService.list(params);
|
||||||
|
if (!bookIndexBigList.isEmpty()) {
|
||||||
|
List<List<BookIndexDO>> bookIndexSmallList = bookIndexBigList.stream().collect(
|
||||||
|
Collectors.groupingBy(item -> bookIndexBigList.indexOf(item) / 100)).values().stream().toList();
|
||||||
|
for (List<BookIndexDO> bookIndexList : bookIndexSmallList) {
|
||||||
|
// 获取集合中所有的ID
|
||||||
|
List<Long> bookIndexIds = bookIndexList.stream().map(BookIndexDO::getId).toList();
|
||||||
|
List<BookContentDO> bookContentList = bookContentService.listByIndexIds(bookIndexIds);
|
||||||
|
Map<Long, String> bookContentMap = bookContentList.stream()
|
||||||
|
.collect(Collectors.toMap(BookContentDO::getIndexId, BookContentDO::getContent));
|
||||||
|
for (BookIndexDO bookIndex : bookIndexList) {
|
||||||
|
String indexName = bookIndex.getIndexName();
|
||||||
|
if (indexName != null) {
|
||||||
|
String content = bookContentMap.get(bookIndex.getId());
|
||||||
|
out.write(indexName.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
content = PATTERN_BR.matcher(content).replaceAll("\r\n");
|
||||||
|
content = PATTERN_NBSP.matcher(content).replaceAll(" ");
|
||||||
|
content = PATTERN_ANCHOR_OPEN.matcher(content).replaceAll("");
|
||||||
|
content = PATTERN_ANCHOR_CLOSE.matcher(content).replaceAll("");
|
||||||
|
content = PATTERN_DIV_OPEN.matcher(content).replaceAll("");
|
||||||
|
content = PATTERN_DIV_CLOSE.matcher(content).replaceAll("");
|
||||||
|
content = PATTERN_EMPTY_PARAGRAPH_WITH_LINK.matcher(content).replaceAll("");
|
||||||
|
content = PATTERN_P_OPEN.matcher(content).replaceAll("");
|
||||||
|
content = PATTERN_P_CLOSE.matcher(content).replaceAll("\r\n");
|
||||||
|
out.write(content.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
out.close();
|
||||||
|
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
redisTemplate.delete(Constant.BOOK_IS_DOWNLOADING_KEY + bookId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.java2nb.common.annotation.SanitizeMap;
|
|||||||
|
|
||||||
import com.java2nb.novel.domain.BookContentDO;
|
import com.java2nb.novel.domain.BookContentDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -33,4 +34,6 @@ public interface BookContentDao {
|
|||||||
int batchRemove(Long[] ids);
|
int batchRemove(Long[] ids);
|
||||||
|
|
||||||
int removeByIndexIds(Long[] indexIds);
|
int removeByIndexIds(Long[] indexIds);
|
||||||
|
|
||||||
|
List<BookContentDO> listByIndexIds(@Param("indexIds") List<Long> indexIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,4 +27,6 @@ public interface BookContentService {
|
|||||||
int remove(Long id);
|
int remove(Long id);
|
||||||
|
|
||||||
int batchRemove(Long[] ids);
|
int batchRemove(Long[] ids);
|
||||||
|
|
||||||
|
List<BookContentDO> listByIndexIds(List<Long> indexIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.java2nb.novel.service.impl;
|
package com.java2nb.novel.service.impl;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -51,5 +52,10 @@ public class BookContentServiceImpl implements BookContentService {
|
|||||||
public int batchRemove(Long[] ids){
|
public int batchRemove(Long[] ids){
|
||||||
return bookContentDao.batchRemove(ids);
|
return bookContentDao.batchRemove(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<BookContentDO> listByIndexIds(List<Long> indexIds) {
|
||||||
|
return bookContentDao.listByIndexIds(indexIds);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,17 @@
|
|||||||
</where>
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="listByIndexIds" resultType="com.java2nb.novel.domain.BookContentDO">
|
||||||
|
select `id`,`index_id`,`content` from book_content
|
||||||
|
where index_id in (
|
||||||
|
<foreach item="item" index="index" collection="indexIds" separator=",">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="save" parameterType="com.java2nb.novel.domain.BookContentDO">
|
<insert id="save" parameterType="com.java2nb.novel.domain.BookContentDO">
|
||||||
insert into book_content
|
insert into book_content
|
||||||
(`id`,
|
(`id`,
|
||||||
|
|||||||
@@ -143,10 +143,14 @@ function load() {
|
|||||||
field: 'id',
|
field: 'id',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
formatter: function (value, row, index) {
|
formatter: function (value, row, index) {
|
||||||
|
// 增加下载按钮
|
||||||
|
var d = '<a class="btn btn-primary btn-sm" href="#" mce_href="#" title="下载TXT" onclick="downloadBook(\''
|
||||||
|
+ row.id
|
||||||
|
+ '\',\'' + row.bookName + '\')"><i class="fa fa-cloud-download"></i></a><br><br> ';
|
||||||
var r = '<a class="btn btn-warning btn-sm ' + s_remove_h + '" href="#" title="删除" mce_href="#" onclick="remove(\''
|
var r = '<a class="btn btn-warning btn-sm ' + s_remove_h + '" href="#" title="删除" mce_href="#" onclick="remove(\''
|
||||||
+ row.id
|
+ row.id
|
||||||
+ '\')"><i class="fa fa-remove"></i></a> ';
|
+ '\')"><i class="fa fa-remove"></i></a> ';
|
||||||
return r;
|
return d + r;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,4 +253,9 @@ function batchRemove() {
|
|||||||
}, function () {
|
}, function () {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadBook(bookId, bookName) {
|
||||||
|
window.open(prefix + '/download?bookId='+bookId+'&bookName='+bookName);
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user