Compare commits

...

48 Commits

Author SHA1 Message Date
xiongxiaoyang
0564871093 v5.2.5 发布 2025-08-14 22:36:37 +08:00
xiongxiaoyang
d6faab8ca1 fix: 删除废弃接口
可能导致 XSS 漏洞
2025-08-14 22:06:12 +08:00
xiongxiaoyang
1ef64edcda v5.2.4 发布 2025-07-25 20:09:50 +08:00
xiongxiaoyang
c24c68ecaf perf: 优化缓存模块
提升可读性 & 减小内存占用
2025-07-25 17:03:46 +08:00
xiongxiaoyang
7e27456a65 build:打包时复制最新模版文件 2025-07-25 13:00:00 +08:00
xiongxiaoyang
d4e8fb1cc7 模版更新 2025-07-25 12:58:18 +08:00
xiongxiaoyang
d4e1126873 perf: UI优化 2025-07-22 11:55:27 +08:00
xiongxiaoyang
84a90bbc34 v5.2.3 发布 2025-07-19 18:22:25 +08:00
xiongxiaoyang
b2d8fd8c66 feat(AI): 更新默认AI对话模型
- 原默认AI对话模型 deepseek-ai/DeepSeek-R1-Distill-Llama-8B 已从硅基流动模型广场下线。
- 此次更新将项目中使用的默认AI对话模型更改为新的可用模型 deepseek-ai/DeepSeek-R1-0528-Qwen3-8B。
2025-07-19 17:27:45 +08:00
xiongxiaoyang
11d9d6f6e8 refactor: 重构排序参数处理代码 2025-07-19 17:27:21 +08:00
xiongxiaoyang
8c7b891af2 build(AI): Spring AI 升级到 1.0.0 2025-07-18 21:24:24 +08:00
xiongxiaoyang
bb1a87e337 chore(sql): 内置海外专用源 2025-07-18 20:41:00 +08:00
xiongxiaoyang
1cd8a49fd4 perf: 优化排序参数校验 2025-07-18 16:21:35 +08:00
xiongxiaoyang
773ce159f7 v5.2.2 发布 2025-07-17 21:14:24 +08:00
xiongxiaoyang
91e7d2712b refactor: 重构sort和order参数校验功能 2025-07-17 20:53:41 +08:00
xiongxiaoyang
3db8828384 fix: 修复sort和order参数的SQL注入漏洞 2025-07-17 19:03:58 +08:00
xiongxiaoyang
54bd194b98 feat(novel-crawl): 增加爬虫源采集章节数量监控功能
可以监测到爬虫源在当前环境下是否可用
2025-07-16 19:52:07 +08:00
xiongxiaoyang
3d41cf3ebb perf(novel-crawl): 优化爬虫源列表排序
按照更新时间倒序
2025-07-15 18:53:31 +08:00
xiongxiaoyang
720711414c v5.2.1 发布 2025-07-14 22:00:34 +08:00
xiongxiaoyang
522bb7c739 fix(novel-front): 修复评论回复中的XSS漏洞 2025-07-14 21:02:13 +08:00
xiongxiaoyang
64e1686fd1 v5.2.0 发布 2025-07-12 19:54:23 +08:00
xiongxiaoyang
90009a57f4 模版更新 2025-07-12 18:40:05 +08:00
xiongxiaoyang
6452c1603f fix(templates): URI编码 2025-07-12 18:32:28 +08:00
xiongxiaoyang
d54eda2366 perf: 未登录自动跳转 2025-07-12 18:26:00 +08:00
xiongxiaoyang
972a49f1ba docs: 错别字修改 2025-07-12 18:05:06 +08:00
xiongxiaoyang
675b156094 perf(novel-front): 优化评论时间显示 2025-07-12 14:08:42 +08:00
xiongxiaoyang
3c409023e5 feat(novel-front): 增加评论点赞/点踩功能 2025-07-12 13:33:23 +08:00
xiongxiaoyang
02fb819120 feat(novel-front): 增加评论回复功能 2025-07-12 11:15:35 +08:00
xiongxiaoyang
8c572edb10 perf(novel-crawl): 优化单本采集任务进度显示功能 2025-07-11 22:55:36 +08:00
xiongxiaoyang
8c9013ad05 feat(novel-crawl): 增加单本采集任务进度显示功能 2025-07-11 22:09:28 +08:00
xiongxiaoyang
4693c7ffae feat(novel-front): 增加评论用户地理位置显示功能 2025-06-30 20:51:29 +08:00
xiongxiaoyang
efb136e3be v5.1.5 发布 2025-06-21 19:45:18 +08:00
xiongxiaoyang
7955db0e3c perf(novel-crawl): 去除小说简介前后空格 2025-06-21 19:06:08 +08:00
xiongxiaoyang
60dc28c5ed perf(novel-crawl): 去除小说简介末尾冗余的小说名 2025-06-21 18:12:37 +08:00
xiongxiaoyang
1534220f0c perf(novel-crawl): 增加小说简介过滤规则 2025-06-21 17:54:59 +08:00
xiongxiaoyang
0830f6ffeb Merge remote-tracking branch 'Gitee/develop_xxy' into develop_xxy 2025-06-21 12:33:02 +08:00
xiongxiaoyang
adc83db64e perf(novel-crawl): 去除小说内容末尾的所有换行 2025-06-21 12:32:11 +08:00
xiongxiaoyang
9c11f22816 v5.1.4 发布 2025-06-19 19:42:03 +08:00
xiongxiaoyang
24abe7714f chore: 增加在线演示网站 2025-06-19 19:38:22 +08:00
xiongxiaoyang
a9fc80eba1 fix: 修复 ConcurrentModificationException 2025-06-19 19:32:04 +08:00
xiongxiaoyang
32541a7cb6 v5.1.3发布 2025-05-13 11:16:08 +08:00
xiongxiaoyang
42bcecc304 fix(novel-crawl): 解决爬虫进程间的冲突问题,支持同时启动多个爬虫进程 2025-05-13 11:11:27 +08:00
xiongxiaoyang
a07643bde0 fix(novel-crawl): 解决多个爬虫进程间的爬虫源状态冲突问题 2025-05-13 10:45:38 +08:00
xiongxiaoyang
1f53b56bd6 fix(novel-crawl): 调整线程终止逻辑 2025-05-13 09:58:47 +08:00
xiongxiaoyang
2c86cb9a7d v5.1.2 发布 2025-05-12 18:33:59 +08:00
xiongxiaoyang
a4d6272a4f perf(novel-crawl): 减小失效爬虫的CPU占用 2025-05-12 18:18:48 +08:00
xiongxiaoyang
55d5deea74 fix(novel-crawl): 修复部分源无法停止的问题 2025-05-12 17:48:24 +08:00
xiongxiaoyang
4f474b91a8 Update README.md 2025-05-07 08:50:48 +08:00
149 changed files with 3095 additions and 752 deletions

View File

@@ -1,5 +1,4 @@
<p align="center">
<a href="https://www.swiftproxy.net/?code=T2WV1VT50"><img src="https://xxyopen.com/images/ad1.png" alt="AD" ></a>
<a href="https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=736e609d66e0ac4e57813316cec6fd0b&from=console"><img src="https://youdoc.github.io/img/tencent.jpg" alt="AD" ></a>
</p>
<p align="center">
@@ -10,7 +9,7 @@
</p>
<p align="center">
👉 <a href='https://novel.xxyopen.com'>官网</a> | 👉 <a href='https://www.bilibili.com/video/BV1Zo4y187Mi'>项目演示</a> | 👉 <a href='https://docs.xxyopen.com/course/novelplus/1.html'>安装教程</a>
👉 <a href='https://novel.xxyopen.com'>官网</a> | 👉 <a href='http://117.72.165.13:8888'>演示站点</a> | 👉 <a href='https://docs.xxyopen.com/course/novelplus/1.html'>安装教程</a>
</p>
## 项目介绍
@@ -22,8 +21,8 @@ TXT 文本存储)、阅读主题切换、多爬虫源自动采集和更新数
## 项目地址
- 学习版[GitHub](https://github.com/201206030/novel) [码云](https://gitee.com/novel_dev_team/novel)
[保姆级教程](https://docs.xxyopen.com)
- **应用版**[GitHub](https://github.com/201206030/novel-plus) [码云](https://gitee.com/novel_dev_team/novel-plus)
[保姆级教程](https://docs.xxyopen.com)
- **应用版**[GitHub](https://github.com/201206030/novel-plus) [码云](https://gitee.com/novel_dev_team/novel-plus) [演示站点](http://117.72.165.13:8888)
- 微服务版[GitHub](https://github.com/201206030/novel-cloud) [码云](https://gitee.com/novel_dev_team/novel-cloud)
## 项目结构
@@ -78,14 +77,11 @@ novel-plus 5.x 已集成 Spring 官方最新发布的 Spring AI 框架,并推
1. v5.0.0 版本在小说章节发布页面的文本编辑器中集成了多项智能编辑功能包括 AI 扩写缩写续写及文本润色等这些功能的设计灵感来源于百家号文章编辑器中的 AI 助手
2. v5.1.0 版本在小说发布页面新增 AI 生成封面图功能若作家未上传自定义封面图系统将根据小说信息自动生成封面图
目前AI 功能仍处于实验阶段仅实现了基础的核心功能我们非常重视用户的实际使用体验和反馈未来将根据用户需求和使用情况持续优化和调整该功能如果用户反馈积极我们计划进一步开发更高级的
AI 功能例如自动生成有声小说智能情节推荐等以全面提升 novel-plus 的创作能力和用户体验
目前AI 功能仍处于实验阶段仅实现了基础的核心功能我们非常重视用户的实际使用体验和反馈未来将根据用户需求和使用情况持续优化和调整该功能如果用户反馈积极我们计划进一步开发更高级的 AI 功能例如自动生成有声小说智能情节推荐等以全面提升 novel-plus 的创作能力和用户体验
我们将持续关注 AI 技术的发展并致力于将其与小说创作场景深度融合为用户带来更智能更便捷的创作工具
由于 DeepSeek 官方 API 目前不可用novel-plus 项目默认使用的是第三方[硅基流动](https://cloud.siliconflow.cn/i/DOgMRH9S)
提供的 API采用的 AI 模型有对话模型`deepseek-ai/DeepSeek-R1-Distill-Llama-8B`DeepSeek-R1 的蒸馏版本免费使用和生图模型`Kwai-Kolors/Kolors`快手 Kolors 团队开发的文本到图像生成模型免费使用只需注册一个硅基流动账号创建一个
API 密钥并将其添加到 novel-plus 项目 novel-front 模块的 yaml 配置文件中即可体验 novel-plus 项目的 AI 写作功能
novel-plus 项目默认使用的是第三方大模型服务平台[硅基流动](https://cloud.siliconflow.cn/i/DOgMRH9S)提供的 API兼容 OpenAI 的相关接口,可直接通过 Spring AI 框架调用),采用的 AI 模型有对话模型`deepseek-ai/DeepSeek-R1-0528-Qwen3-8B`DeepSeek-R1 的蒸馏版本,免费使用)和生图模型`Kwai-Kolors/Kolors`(快手 Kolors 团队开发的文本到图像生成模型,免费使用)。只需注册一个硅基流动账号,创建一个 API 密钥,并将其添加到 novel-plus 项目 novel-front 模块的 yaml 配置文件中,即可体验 novel-plus 项目的 AI 写作功能。
```yaml
spring:
@@ -102,10 +98,10 @@ spring:
base-url: https://api.siliconflow.cn
chat:
options:
model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B
model: deepseek-ai/DeepSeek-R1-0528-Qwen3-8B
```
> novel-plus 项目默认使用的都是免费 AI 模型生成效果有限如果对生成内容有更高的要求建议选用付费的 AI 模型
novel-plus 项目默认使用的都是免费 AI 模型生成效果有限如果对生成内容有更高的要求建议选用付费的 AI 模型
## 增值服务

3
doc/sql/20250630.sql Normal file
View File

@@ -0,0 +1,3 @@
alter table book_comment add column location varchar(50) DEFAULT NULL COMMENT '地理位置' after comment_content ;

3
doc/sql/20250711.sql Normal file
View File

@@ -0,0 +1,3 @@
alter table crawl_single_task add column crawl_chapters int DEFAULT 0 COMMENT '采集章节数量' after exc_count ;

13
doc/sql/20250712.sql Normal file
View File

@@ -0,0 +1,13 @@
DROP TABLE IF EXISTS `book_comment_reply`;
CREATE TABLE `book_comment_reply`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`comment_id` bigint(20) DEFAULT NULL COMMENT '评论ID',
`reply_content` varchar(512) DEFAULT NULL COMMENT '回复内容',
`location` varchar(50) DEFAULT NULL COMMENT '地理位置',
`audit_status` tinyint(1) DEFAULT '0' COMMENT '审核状态0待审核1审核通过2审核不通过',
`create_time` datetime DEFAULT NULL COMMENT '回复用户ID',
`create_user_id` bigint(20) DEFAULT NULL COMMENT '回复时间',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='小说评论回复表';

View File

@@ -3153,4 +3153,64 @@ where menu_id = 104;
delete
from sys_menu
where menu_id = 57;
where menu_id = 57;
alter table book_comment add column location varchar(50) DEFAULT NULL COMMENT '地理位置' after comment_content ;
alter table crawl_single_task add column crawl_chapters int DEFAULT 0 COMMENT '采集章节数量' after exc_count ;
DROP TABLE IF EXISTS `book_comment_reply`;
CREATE TABLE `book_comment_reply`
(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`comment_id` bigint(20) DEFAULT NULL COMMENT '评论ID',
`reply_content` varchar(512) DEFAULT NULL COMMENT '回复内容',
`location` varchar(50) DEFAULT NULL COMMENT '地理位置',
`audit_status` tinyint(1) DEFAULT '0' COMMENT '审核状态0待审核1审核通过2审核不通过',
`create_time` datetime DEFAULT NULL COMMENT '回复用户ID',
`create_user_id` bigint(20) DEFAULT NULL COMMENT '回复时间',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='小说评论回复表';
INSERT INTO crawl_source (source_name, crawl_rule, source_status, create_time
, update_time)
VALUES ('飘天文学网海外专用', '{
"bookListUrl": "https://www.piaotia.com/booksort{catId}/{page}.html",
"catIdRule": {
"catId1": "1/0",
"catId2": "2/0",
"catId3": "3/0",
"catId4": "4/0",
"catId5": "6/0",
"catId6": "5/0"
},
"bookIdPatten": "href=\\"https://www.piaotia.com/bookinfo/(\\\\d+/\\\\d+).html\\"",
"pagePatten": "<em\\\\s+id=\\"pagestats\\">(\\\\d+)/\\\\d+</em>",
"totalPagePatten": "<em\\\\s+id=\\"pagestats\\">\\\\d+/(\\\\d+)</em>",
"bookDetailUrl": "https://www.piaotia.com/bookinfo/{bookId}.html",
"bookNamePatten": "<h1>([^/]+)</h1>",
"authorNamePatten": "<td\\\\s+width=\\"\\\\d+%\\">作&nbsp;&nbsp;&nbsp; 者:([^/]+)<",
"picUrlPatten": "<img\\\\s+src=\\"(https://www.piaotia.com/files/article/image/[^\\"]+)\\"",
"statusPatten": "<td>文章状态:([^/]+)</td>",
"bookStatusRule": {
"连载中": 0,
"已完成": 1
},
"descStart": " <span class=\\"hottext\\">内容简介:</span><br />",
"descEnd": "</td>",
"filterDesc": "",
"bookIndexUrl": "https://www.piaotia.com/html/{bookId}/index.html",
"indexIdPatten": "<li><a href=\\"(\\\\d+).html\\">[^/]+</a></li>",
"indexNamePatten": "<li><a href=\\"\\\\d+.html\\">([^/]+)</a></li>",
"bookContentUrl": "https://www.piaotia.com/html/{bookId}/{indexId}.html",
"contentStart": "<br>",
"contentEnd": "</div>",
"filterContent": "",
"charset": "gbk"
}', 0, '2025-07-13 18:57:39'
, '2025-07-13 18:57:39');

View File

@@ -5,7 +5,7 @@
<groupId>com.java2nb</groupId>
<artifactId>novel-admin</artifactId>
<version>5.1.1</version>
<version>5.2.5</version>
<packaging>jar</packaging>
<name>novel-admin</name>

View File

@@ -0,0 +1,14 @@
package com.java2nb.common.annotation;
import java.lang.annotation.*;
/**
* 标记某个方法参数需要进行 Map 字段的清理和标准化处理。
*
* <p>通常用于 DAO 接口中 list 方法的 Map 参数,用于防止非法排序字段或排序顺序。</p>
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SanitizeMap {
}

View File

@@ -0,0 +1,69 @@
package com.java2nb.common.aspect;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.common.utils.SortWhitelistUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
/**
* 拦截所有 Mapper 接口的 list* 方法,对带有 @SanitizeMap 注解的 Map 参数进行排序字段和顺序的规范化处理。
*
* <p>主要防止 SQL 注入或非法排序字段、非法排序顺序的问题。
* 例如对 sort 和 order 字段进行白名单过滤和标准化处理。</p>
*/
@Aspect
@Component
@RequiredArgsConstructor
public class MapSortValidationAspect {
/**
* 拦截所有 Mapper 接口的 list* 方法(如 list(), listByPage 等)。
* 对带有 @SanitizeMap 注解的 Map 参数进行处理。
*
* <p>执行逻辑:</p>
* <ol>
* <li>获取方法参数及注解信息</li>
* <li>遍历所有参数,检查是否带有 @SanitizeMap 注解</li>
* <li>如果参数是 Map 类型且有注解,则进行字段清理</li>
* </ol>
*
* @param joinPoint 切点信息
* @return 方法执行结果
*/
@SneakyThrows
@Around("execution(* com.java2nb.*.dao.*Dao.list*(..))")
public Object sanitizeMapParameters(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
boolean hasAnnotation = Arrays.stream(parameterAnnotations[i])
.anyMatch(a -> a.annotationType().equals(SanitizeMap.class));
if (hasAnnotation && args[i] instanceof Map map) {
if (map.get("sort") instanceof String sortStr) {
map.put("sort", SortWhitelistUtil.sanitizeColumn(sortStr));
}
if (map.get("order") instanceof String orderStr) {
map.put("order", SortWhitelistUtil.sanitizeOrder(orderStr));
}
}
}
return joinPoint.proceed(args);
}
}

View File

@@ -1,5 +1,6 @@
package com.java2nb.common.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.common.domain.DictDO;
import java.util.List;
@@ -19,7 +20,7 @@ public interface DictDao {
DictDO get(Long id);
List<DictDO> list(Map<String, Object> map);
List<DictDO> list(@SanitizeMap Map<String, Object> map);
int count(Map<String, Object> map);

View File

@@ -1,5 +1,6 @@
package com.java2nb.common.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.common.domain.FileDO;
import java.util.List;
@@ -18,7 +19,7 @@ public interface FileDao {
FileDO get(Long id);
List<FileDO> list(Map<String,Object> map);
List<FileDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -3,6 +3,7 @@ package com.java2nb.common.dao;
import java.util.List;
import java.util.Map;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.common.domain.GenColumnsDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -19,7 +20,7 @@ public interface GenColumnsDao {
GenColumnsDO get(Long id);
List<GenColumnsDO> list(Map<String,Object> map);
List<GenColumnsDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,6 @@
package com.java2nb.common.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.common.domain.LogDO;
import java.util.List;
@@ -18,7 +19,7 @@ public interface LogDao {
LogDO get(Long id);
List<LogDO> list(Map<String,Object> map);
List<LogDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -0,0 +1,43 @@
package com.java2nb.common.utils;
import lombok.experimental.UtilityClass;
import java.util.Set;
/**
* 排序字段和排序顺序的白名单工具类。
*/
@UtilityClass
public class SortWhitelistUtil {
// 白名单字段
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "order_num");
// 白名单排序方式
private static final Set<String> ALLOWED_ORDERS = Set.of("asc", "desc");
/**
* 对排序字段进行白名单过滤和标准化。
*
* @param column 原始字段名
* @return 安全的字段名,若非法则返回 null
*/
public static String sanitizeColumn(String column) {
if (column == null) return null;
String lower = column.trim().toLowerCase();
return ALLOWED_COLUMNS.contains(lower) ? lower : null;
}
/**
* 对排序方式进行白名单过滤和标准化。
*
* @param order 原始排序方式
* @return 安全的排序方式("asc" 或 "desc"),若非法则返回 null
*/
public static String sanitizeOrder(String order) {
if (order == null) return null;
String lower = order.trim().toLowerCase();
return ALLOWED_ORDERS.contains(lower) ? lower : null;
}
}

View File

@@ -9,7 +9,7 @@ import com.java2nb.novel.service.FriendLinkService;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.annotation.Validated;
@@ -31,7 +31,7 @@ public class FriendLinkController {
@Autowired
private FriendLinkService friendLinkService;
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
private StringRedisTemplate redisTemplate;
@GetMapping()
@RequiresPermissions("novel:friendLink:friendLink")

View File

@@ -9,7 +9,7 @@ import com.java2nb.novel.service.NewsService;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@@ -32,7 +32,7 @@ public class NewsController {
@Autowired
private NewsService newsService;
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
private StringRedisTemplate redisTemplate;
@GetMapping()
@RequiresPermissions("novel:news:news")

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.AuthorCodeDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface AuthorCodeDao {
AuthorCodeDO get(Long id);
List<AuthorCodeDO> list(Map<String,Object> map);
List<AuthorCodeDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,6 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.AuthorDO;
import java.util.Date;
@@ -19,7 +20,7 @@ public interface AuthorDao {
AuthorDO get(Long id);
List<AuthorDO> list(Map<String,Object> map);
List<AuthorDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.BookCommentDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface BookCommentDao {
BookCommentDO get(Long id);
List<BookCommentDO> list(Map<String,Object> map);
List<BookCommentDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.BookContentDO;
import org.apache.ibatis.annotations.Mapper;
@@ -18,7 +20,7 @@ public interface BookContentDao {
BookContentDO get(Long id);
List<BookContentDO> list(Map<String, Object> map);
List<BookContentDO> list(@SanitizeMap Map<String, Object> map);
int count(Map<String, Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.BookDO;
import org.apache.ibatis.annotations.Mapper;
@@ -19,7 +21,7 @@ public interface BookDao {
BookDO get(Long id);
List<BookDO> list(Map<String, Object> map);
List<BookDO> list(@SanitizeMap Map<String, Object> map);
int count(Map<String, Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.BookIndexDO;
import org.apache.ibatis.annotations.Mapper;
@@ -18,7 +20,7 @@ public interface BookIndexDao {
BookIndexDO get(Long id);
List<BookIndexDO> list(Map<String, Object> map);
List<BookIndexDO> list(@SanitizeMap Map<String, Object> map);
int count(Map<String, Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.BookSettingDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface BookSettingDao {
BookSettingDO get(Long id);
List<BookSettingDO> list(Map<String,Object> map);
List<BookSettingDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.CategoryDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface CategoryDao {
CategoryDO get(Integer id);
List<CategoryDO> list(Map<String,Object> map);
List<CategoryDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.FriendLinkDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface FriendLinkDao {
FriendLinkDO get(Integer id);
List<FriendLinkDO> list(Map<String,Object> map);
List<FriendLinkDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.NewsDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface NewsDao {
NewsDO get(Long id);
List<NewsDO> list(Map<String,Object> map);
List<NewsDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.PayDO;
import java.util.Date;
@@ -19,7 +21,7 @@ public interface PayDao {
PayDO get(Long id);
List<PayDO> list(Map<String,Object> map);
List<PayDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.UserDO;
import org.apache.ibatis.annotations.Mapper;
@@ -17,7 +19,7 @@ public interface UserDao {
UserDO get(Long id);
List<UserDO> list(Map<String, Object> map);
List<UserDO> list(@SanitizeMap Map<String, Object> map);
int count(Map<String, Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.UserFeedbackDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface UserFeedbackDao {
UserFeedbackDO get(Long id);
List<UserFeedbackDO> list(Map<String,Object> map);
List<UserFeedbackDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.novel.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.novel.domain.WebsiteInfoDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface WebsiteInfoDao {
WebsiteInfoDO get(Long id);
List<WebsiteInfoDO> list(Map<String,Object> map);
List<WebsiteInfoDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.DataPermDO;
import java.util.List;
@@ -19,7 +21,7 @@ public interface DataPermDao {
DataPermDO get(Long id);
List<DataPermDO> list(Map<String,Object> map);
List<DataPermDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.DeptDO;
import java.util.List;
@@ -19,7 +21,7 @@ public interface DeptDao {
DeptDO get(Long deptId);
List<DeptDO> list(Map<String,Object> map);
List<DeptDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.MenuDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface MenuDao {
MenuDO get(Long menuId);
List<MenuDO> list(Map<String,Object> map);
List<MenuDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.RoleDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface RoleDao {
RoleDO get(Long roleId);
List<RoleDO> list(Map<String,Object> map);
List<RoleDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.RoleDataPermDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface RoleDataPermDao {
RoleDataPermDO get(Long id);
List<RoleDataPermDO> list(Map<String,Object> map);
List<RoleDataPermDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,5 +1,7 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.RoleMenuDO;
import java.util.List;
@@ -18,7 +20,7 @@ public interface RoleMenuDao {
RoleMenuDO get(Long id);
List<RoleMenuDO> list(Map<String,Object> map);
List<RoleMenuDO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -1,15 +1,15 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.UserDO;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
*
* @author xiongxy
* @email 1179705413@qq.com
* @date 2019-10-03 09:45:11
@@ -17,23 +17,23 @@ import org.apache.ibatis.annotations.Param;
@Mapper
public interface SysUserDao {
UserDO get(Long userId);
List<UserDO> list(Map<String,Object> map);
int count(Map<String,Object> map);
int save(UserDO user);
int update(UserDO user);
int remove(Long userId);
int batchRemove(Long[] userIds);
Long[] listAllDept();
UserDO get(Long userId);
List<UserDO> listByPerm(Map<String, Object> map);
List<UserDO> list(@SanitizeMap Map<String, Object> map);
int countByPerm(Map<String,Object> map);
int count(Map<String, Object> map);
int save(UserDO user);
int update(UserDO user);
int remove(Long userId);
int batchRemove(Long[] userIds);
Long[] listAllDept();
List<UserDO> listByPerm(@SanitizeMap Map<String, Object> map);
int countByPerm(Map<String, Object> map);
}

View File

@@ -1,5 +1,7 @@
package com.java2nb.system.dao;
import com.java2nb.common.annotation.SanitizeMap;
import com.java2nb.system.domain.UserRoleDO;
import java.util.List;
@@ -19,7 +21,7 @@ public interface UserRoleDao {
UserRoleDO get(Long id);
List<UserRoleDO> list(Map<String, Object> map);
List<UserRoleDO> list(@SanitizeMap Map<String, Object> map);
int count(Map<String, Object> map);

View File

@@ -18,7 +18,7 @@ public interface ${className}Dao {
${className}DO get(${pk.javaType} ${pk.attrname});
List<${className}DO> list(Map<String,Object> map);
List<${className}DO> list(@SanitizeMap Map<String,Object> map);
int count(Map<String,Object> map);

View File

@@ -44,7 +44,7 @@ public interface ${className}Mapper {
"limit #{offset}, #{limit}" +
"</if>"+
"</script>")
List<${className}DO> list(Map<String,Object> map);
List<${className}DO> list(@SanitizeMap Map<String,Object> map);
@Select("<script>" +
"select count(*) from ${tableName} " +

View File

@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8">
<title>后台管理-</title>
<title>后台管理-</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="Access-Control-Allow-Origin" content="*">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>novel</artifactId>
<groupId>com.java2nb</groupId>
<version>5.1.1</version>
<version>5.2.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -22,6 +22,12 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Aop 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>

View File

@@ -8,7 +8,7 @@ public interface CacheKey {
/**
* 首页小说设置
* */
String INDEX_BOOK_SETTINGS_KEY = "indexBookSettingsKey";
String INDEX_BOOK_SETTINGS_KEY = "indexBookSettingsKey:v2";
/**
* 首页新闻
@@ -41,11 +41,6 @@ public interface CacheKey {
* */
String TEMPLATE_DIR_KEY = "templateDirKey";;
/**
* 正在运行的爬虫线程存储KEY前缀
* */
String RUNNING_CRAWL_THREAD_KEY_PREFIX = "runningCrawlTreadDataKeyPrefix";
/**
* 上一次搜索引擎更新的时间
* */

View File

@@ -1,55 +1,61 @@
package com.java2nb.novel.core.cache;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.util.List;
/**
* @author 11797
*/
public interface CacheService {
/**
* 根据key获取缓存的String类型数据
*/
String get(String key);
/**
* 根据key获取缓存的String类型数据
*/
String get(String key);
/**
* 设置String类型的缓存
*/
void set(String key, String value);
/**
* 设置String类型的缓存
*/
void set(String key, String value);
/**
* 设置一个有过期时间的String类型的缓存,单位秒
*/
void set(String key, String value, long timeout);
/**
* 根据key获取缓存的Object类型数据
*/
Object getObject(String key);
/**
* 设置Object类型的缓存
*/
void setObject(String key, Object value);
/**
* 设置一个有过期时间的Object类型的缓存,单位秒
*/
/**
* 设置一个有过期时间的String类型的缓存,单位秒
*/
void set(String key, String value, long timeout);
/**
* 根据key获取缓存的Object类型数据
*/
<T> T getObject(String key, Class<T> clazz);
<T> List<T> getList(String key, Class<T> clazz);
/**
* 设置Object类型的缓存
*/
void setObject(String key, Object value);
/**
* 设置一个有过期时间的Object类型的缓存,单位秒
*/
void setObject(String key, Object value, long timeout);
/**
* 根据key删除缓存的数据
*/
void del(String key);
/**
* 根据key删除缓存的数据
*/
void del(String key);
/**
* 判断是否存在一个key
* */
boolean contains(String key);
/**
* 设置key过期时间
* */
void expire(String key, long timeout);
/**
* 判断是否存在一个key
*/
boolean contains(String key);
/**
* 设置key过期时间
*/
void expire(String key, long timeout);
}

View File

@@ -1,23 +1,35 @@
package com.java2nb.novel.core.cache.impl;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.java2nb.novel.core.cache.CacheService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author xxy
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class RedisServiceImpl implements CacheService {
private final StringRedisTemplate stringRedisTemplate;
private final RedisTemplate<Object, Object> redisTemplate;
private ObjectMapper objectMapper;
@PostConstruct
public void init() {
objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
@Override
@@ -37,34 +49,66 @@ public class RedisServiceImpl implements CacheService {
}
@Override
public Object getObject(String key) {
return redisTemplate.opsForValue().get(key);
public <T> T getObject(String key, Class<T> clazz) {
String result = get(key);
if (result != null) {
try {
return objectMapper.readValue(result, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return null;
}
@Override
public <T> List<T> getList(String key, Class<T> clazz) {
String result = get(key);
if (result != null) {
try {
return objectMapper.readValue(result,
objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return null;
}
@Override
public void setObject(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
if (value != null) {
try {
set(key, objectMapper.writeValueAsString(value));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void setObject(String key, Object value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
if (value != null) {
try {
set(key, objectMapper.writeValueAsString(value), timeout);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void del(String key) {
redisTemplate.delete(key);
stringRedisTemplate.delete(key);
}
@Override
public boolean contains(String key) {
return redisTemplate.hasKey(key) || stringRedisTemplate.hasKey(key);
return stringRedisTemplate.hasKey(key);
}
@Override
public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}

View File

@@ -18,7 +18,7 @@ public enum ResponseStatus implements IResultCode {
/**
* 用户相关错误
* */
NO_LOGIN(1001, "未登录或登失效!"),
NO_LOGIN(1001, "未登录或登失效!"),
VEL_CODE_ERROR(1002, "验证码错误!"),
USERNAME_EXIST(1003,"该手机号已注册!"),
USERNAME_PASS_ERROR(1004,"手机号或密码错误!"),

View File

@@ -106,6 +106,47 @@ public class DateUtil {
}
/**
* 将日期格式化成"多久之前"的格式
* */
public static String formatTimeAgo(Date date){
if (date == null) {
return null;
}
long now = new Date().getTime();
long then = date.getTime();
long diff = now - then;
if (diff < 0) {
// 未来时间
DateUtil.formatDate(date, DateUtil.DATE_TIME_PATTERN);
}
long seconds = diff / 1000;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
long months = days / 30;
long years = months / 12;
if (seconds < 60) {
return "刚刚";
} else if (minutes < 60) {
return minutes + "分钟前";
} else if (hours < 24) {
return hours + "小时前";
} else if (days < 30) {
return days + "天前";
} else if (months < 12) {
return months + "个月前";
} else {
return years + "年前";
}
}
public static void main(String[] args) {
System.out.println(formatDate(getYesterday(),DATE_TIME_PATTERN));
System.out.println(formatDate(getDateStartTime(getYesterday()),DATE_TIME_PATTERN));

View File

@@ -5,8 +5,8 @@ import org.springframework.http.*;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author Administrator
@@ -16,7 +16,7 @@ public class HttpUtil {
private static final String DEFAULT_CHARSET = "utf-8";
private static final Map<String, RestTemplate> REST_TEMPLATE_MAP = new HashMap<>();
private static final Map<String, RestTemplate> REST_TEMPLATE_MAP = new ConcurrentHashMap<>();
public static String getByHttpClientWithChrome(String url, String charset) {
log.debug("Get url{}", url);

View File

@@ -1,11 +1,21 @@
package com.java2nb.novel.core.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
@Slf4j
public class IpUtil {
/**
* 获取真实IP
*
* @param request 请求体
* @return 真实IP
*/
@@ -31,4 +41,27 @@ public class IpUtil {
}
return ip;
}
/**
* 获取本机公网IP
*/
public static String getPublicIP() {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/ip"))
.GET()
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return new ObjectMapper().readTree(response.body()).get("origin").asText();
}
} catch (Exception e) {
log.error("获取本机公网IP异常", e);
}
return null;
}
}

View File

@@ -14,6 +14,9 @@ public class BookComment {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private String commentContent;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private String location;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Integer replyCount;
@@ -56,6 +59,16 @@ public class BookComment {
this.commentContent = commentContent == null ? null : commentContent.trim();
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public String getLocation() {
return location;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setLocation(String location) {
this.location = location == null ? null : location.trim();
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Integer getReplyCount() {
return replyCount;

View File

@@ -0,0 +1,97 @@
package com.java2nb.novel.entity;
import java.util.Date;
import javax.annotation.Generated;
public class BookCommentReply {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Long id;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Long commentId;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private String replyContent;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private String location;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Byte auditStatus;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Date createTime;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Long createUserId;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Long getId() {
return id;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setId(Long id) {
this.id = id;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Long getCommentId() {
return commentId;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setCommentId(Long commentId) {
this.commentId = commentId;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public String getReplyContent() {
return replyContent;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setReplyContent(String replyContent) {
this.replyContent = replyContent == null ? null : replyContent.trim();
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public String getLocation() {
return location;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setLocation(String location) {
this.location = location == null ? null : location.trim();
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Byte getAuditStatus() {
return auditStatus;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setAuditStatus(Byte auditStatus) {
this.auditStatus = auditStatus;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Date getCreateTime() {
return createTime;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Long getCreateUserId() {
return createUserId;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setCreateUserId(Long createUserId) {
this.createUserId = createUserId;
}
}

View File

@@ -31,6 +31,9 @@ public class CrawlSingleTask {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Byte excCount;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Integer crawlChapters;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
private Date createTime;
@@ -124,6 +127,16 @@ public class CrawlSingleTask {
this.excCount = excCount;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Integer getCrawlChapters() {
return crawlChapters;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public void setCrawlChapters(Integer crawlChapters) {
this.crawlChapters = crawlChapters;
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public Date getCreateTime() {
return createTime;

View File

@@ -19,6 +19,9 @@ public final class BookCommentDynamicSqlSupport {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<String> commentContent = bookComment.commentContent;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<String> location = bookComment.location;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Integer> replyCount = bookComment.replyCount;
@@ -39,6 +42,8 @@ public final class BookCommentDynamicSqlSupport {
public final SqlColumn<String> commentContent = column("comment_content", JDBCType.VARCHAR);
public final SqlColumn<String> location = column("location", JDBCType.VARCHAR);
public final SqlColumn<Integer> replyCount = column("reply_count", JDBCType.INTEGER);
public final SqlColumn<Byte> auditStatus = column("audit_status", JDBCType.TINYINT);

View File

@@ -29,7 +29,7 @@ import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
@Mapper
public interface BookCommentMapper {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
BasicColumn[] selectList = BasicColumn.columnList(id, bookId, commentContent, replyCount, auditStatus, createTime, createUserId);
BasicColumn[] selectList = BasicColumn.columnList(id, bookId, commentContent, location, replyCount, auditStatus, createTime, createUserId);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@@ -58,6 +58,7 @@ public interface BookCommentMapper {
@Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true),
@Result(column="book_id", property="bookId", jdbcType=JdbcType.BIGINT),
@Result(column="comment_content", property="commentContent", jdbcType=JdbcType.VARCHAR),
@Result(column="location", property="location", jdbcType=JdbcType.VARCHAR),
@Result(column="reply_count", property="replyCount", jdbcType=JdbcType.INTEGER),
@Result(column="audit_status", property="auditStatus", jdbcType=JdbcType.TINYINT),
@Result(column="create_time", property="createTime", jdbcType=JdbcType.TIMESTAMP),
@@ -92,6 +93,7 @@ public interface BookCommentMapper {
c.map(id).toProperty("id")
.map(bookId).toProperty("bookId")
.map(commentContent).toProperty("commentContent")
.map(location).toProperty("location")
.map(replyCount).toProperty("replyCount")
.map(auditStatus).toProperty("auditStatus")
.map(createTime).toProperty("createTime")
@@ -105,6 +107,7 @@ public interface BookCommentMapper {
c.map(id).toProperty("id")
.map(bookId).toProperty("bookId")
.map(commentContent).toProperty("commentContent")
.map(location).toProperty("location")
.map(replyCount).toProperty("replyCount")
.map(auditStatus).toProperty("auditStatus")
.map(createTime).toProperty("createTime")
@@ -118,6 +121,7 @@ public interface BookCommentMapper {
c.map(id).toPropertyWhenPresent("id", record::getId)
.map(bookId).toPropertyWhenPresent("bookId", record::getBookId)
.map(commentContent).toPropertyWhenPresent("commentContent", record::getCommentContent)
.map(location).toPropertyWhenPresent("location", record::getLocation)
.map(replyCount).toPropertyWhenPresent("replyCount", record::getReplyCount)
.map(auditStatus).toPropertyWhenPresent("auditStatus", record::getAuditStatus)
.map(createTime).toPropertyWhenPresent("createTime", record::getCreateTime)
@@ -157,6 +161,7 @@ public interface BookCommentMapper {
return dsl.set(id).equalTo(record::getId)
.set(bookId).equalTo(record::getBookId)
.set(commentContent).equalTo(record::getCommentContent)
.set(location).equalTo(record::getLocation)
.set(replyCount).equalTo(record::getReplyCount)
.set(auditStatus).equalTo(record::getAuditStatus)
.set(createTime).equalTo(record::getCreateTime)
@@ -168,6 +173,7 @@ public interface BookCommentMapper {
return dsl.set(id).equalToWhenPresent(record::getId)
.set(bookId).equalToWhenPresent(record::getBookId)
.set(commentContent).equalToWhenPresent(record::getCommentContent)
.set(location).equalToWhenPresent(record::getLocation)
.set(replyCount).equalToWhenPresent(record::getReplyCount)
.set(auditStatus).equalToWhenPresent(record::getAuditStatus)
.set(createTime).equalToWhenPresent(record::getCreateTime)
@@ -179,6 +185,7 @@ public interface BookCommentMapper {
return update(c ->
c.set(bookId).equalTo(record::getBookId)
.set(commentContent).equalTo(record::getCommentContent)
.set(location).equalTo(record::getLocation)
.set(replyCount).equalTo(record::getReplyCount)
.set(auditStatus).equalTo(record::getAuditStatus)
.set(createTime).equalTo(record::getCreateTime)
@@ -192,6 +199,7 @@ public interface BookCommentMapper {
return update(c ->
c.set(bookId).equalToWhenPresent(record::getBookId)
.set(commentContent).equalToWhenPresent(record::getCommentContent)
.set(location).equalToWhenPresent(record::getLocation)
.set(replyCount).equalToWhenPresent(record::getReplyCount)
.set(auditStatus).equalToWhenPresent(record::getAuditStatus)
.set(createTime).equalToWhenPresent(record::getCreateTime)

View File

@@ -0,0 +1,54 @@
package com.java2nb.novel.mapper;
import java.sql.JDBCType;
import java.util.Date;
import javax.annotation.Generated;
import org.mybatis.dynamic.sql.SqlColumn;
import org.mybatis.dynamic.sql.SqlTable;
public final class BookCommentReplyDynamicSqlSupport {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final BookCommentReply bookCommentReply = new BookCommentReply();
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Long> id = bookCommentReply.id;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Long> commentId = bookCommentReply.commentId;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<String> replyContent = bookCommentReply.replyContent;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<String> location = bookCommentReply.location;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Byte> auditStatus = bookCommentReply.auditStatus;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Date> createTime = bookCommentReply.createTime;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Long> createUserId = bookCommentReply.createUserId;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final class BookCommentReply extends SqlTable {
public final SqlColumn<Long> id = column("id", JDBCType.BIGINT);
public final SqlColumn<Long> commentId = column("comment_id", JDBCType.BIGINT);
public final SqlColumn<String> replyContent = column("reply_content", JDBCType.VARCHAR);
public final SqlColumn<String> location = column("location", JDBCType.VARCHAR);
public final SqlColumn<Byte> auditStatus = column("audit_status", JDBCType.TINYINT);
public final SqlColumn<Date> createTime = column("create_time", JDBCType.TIMESTAMP);
public final SqlColumn<Long> createUserId = column("create_user_id", JDBCType.BIGINT);
public BookCommentReply() {
super("book_comment_reply");
}
}
}

View File

@@ -0,0 +1,208 @@
package com.java2nb.novel.mapper;
import static com.java2nb.novel.mapper.BookCommentReplyDynamicSqlSupport.*;
import static org.mybatis.dynamic.sql.SqlBuilder.*;
import com.java2nb.novel.entity.BookCommentReply;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import javax.annotation.Generated;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.dynamic.sql.BasicColumn;
import org.mybatis.dynamic.sql.delete.DeleteDSLCompleter;
import org.mybatis.dynamic.sql.delete.render.DeleteStatementProvider;
import org.mybatis.dynamic.sql.insert.render.InsertStatementProvider;
import org.mybatis.dynamic.sql.insert.render.MultiRowInsertStatementProvider;
import org.mybatis.dynamic.sql.select.CountDSLCompleter;
import org.mybatis.dynamic.sql.select.SelectDSLCompleter;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.mybatis.dynamic.sql.update.UpdateDSL;
import org.mybatis.dynamic.sql.update.UpdateDSLCompleter;
import org.mybatis.dynamic.sql.update.UpdateModel;
import org.mybatis.dynamic.sql.update.render.UpdateStatementProvider;
import org.mybatis.dynamic.sql.util.SqlProviderAdapter;
import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
@Mapper
public interface BookCommentReplyMapper {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
BasicColumn[] selectList = BasicColumn.columnList(id, commentId, replyContent, location, auditStatus, createTime, createUserId);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
long count(SelectStatementProvider selectStatement);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@DeleteProvider(type=SqlProviderAdapter.class, method="delete")
int delete(DeleteStatementProvider deleteStatement);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@InsertProvider(type=SqlProviderAdapter.class, method="insert")
int insert(InsertStatementProvider<BookCommentReply> insertStatement);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@InsertProvider(type=SqlProviderAdapter.class, method="insertMultiple")
int insertMultiple(MultiRowInsertStatementProvider<BookCommentReply> multipleInsertStatement);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@ResultMap("BookCommentReplyResult")
Optional<BookCommentReply> selectOne(SelectStatementProvider selectStatement);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@Results(id="BookCommentReplyResult", value = {
@Result(column="id", property="id", jdbcType=JdbcType.BIGINT, id=true),
@Result(column="comment_id", property="commentId", jdbcType=JdbcType.BIGINT),
@Result(column="reply_content", property="replyContent", jdbcType=JdbcType.VARCHAR),
@Result(column="location", property="location", jdbcType=JdbcType.VARCHAR),
@Result(column="audit_status", property="auditStatus", jdbcType=JdbcType.TINYINT),
@Result(column="create_time", property="createTime", jdbcType=JdbcType.TIMESTAMP),
@Result(column="create_user_id", property="createUserId", jdbcType=JdbcType.BIGINT)
})
List<BookCommentReply> selectMany(SelectStatementProvider selectStatement);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@UpdateProvider(type=SqlProviderAdapter.class, method="update")
int update(UpdateStatementProvider updateStatement);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default long count(CountDSLCompleter completer) {
return MyBatis3Utils.countFrom(this::count, bookCommentReply, completer);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int delete(DeleteDSLCompleter completer) {
return MyBatis3Utils.deleteFrom(this::delete, bookCommentReply, completer);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int deleteByPrimaryKey(Long id_) {
return delete(c ->
c.where(id, isEqualTo(id_))
);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int insert(BookCommentReply record) {
return MyBatis3Utils.insert(this::insert, record, bookCommentReply, c ->
c.map(id).toProperty("id")
.map(commentId).toProperty("commentId")
.map(replyContent).toProperty("replyContent")
.map(location).toProperty("location")
.map(auditStatus).toProperty("auditStatus")
.map(createTime).toProperty("createTime")
.map(createUserId).toProperty("createUserId")
);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int insertMultiple(Collection<BookCommentReply> records) {
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, bookCommentReply, c ->
c.map(id).toProperty("id")
.map(commentId).toProperty("commentId")
.map(replyContent).toProperty("replyContent")
.map(location).toProperty("location")
.map(auditStatus).toProperty("auditStatus")
.map(createTime).toProperty("createTime")
.map(createUserId).toProperty("createUserId")
);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int insertSelective(BookCommentReply record) {
return MyBatis3Utils.insert(this::insert, record, bookCommentReply, c ->
c.map(id).toPropertyWhenPresent("id", record::getId)
.map(commentId).toPropertyWhenPresent("commentId", record::getCommentId)
.map(replyContent).toPropertyWhenPresent("replyContent", record::getReplyContent)
.map(location).toPropertyWhenPresent("location", record::getLocation)
.map(auditStatus).toPropertyWhenPresent("auditStatus", record::getAuditStatus)
.map(createTime).toPropertyWhenPresent("createTime", record::getCreateTime)
.map(createUserId).toPropertyWhenPresent("createUserId", record::getCreateUserId)
);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default Optional<BookCommentReply> selectOne(SelectDSLCompleter completer) {
return MyBatis3Utils.selectOne(this::selectOne, selectList, bookCommentReply, completer);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default List<BookCommentReply> select(SelectDSLCompleter completer) {
return MyBatis3Utils.selectList(this::selectMany, selectList, bookCommentReply, completer);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default List<BookCommentReply> selectDistinct(SelectDSLCompleter completer) {
return MyBatis3Utils.selectDistinct(this::selectMany, selectList, bookCommentReply, completer);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default Optional<BookCommentReply> selectByPrimaryKey(Long id_) {
return selectOne(c ->
c.where(id, isEqualTo(id_))
);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int update(UpdateDSLCompleter completer) {
return MyBatis3Utils.update(this::update, bookCommentReply, completer);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
static UpdateDSL<UpdateModel> updateAllColumns(BookCommentReply record, UpdateDSL<UpdateModel> dsl) {
return dsl.set(id).equalTo(record::getId)
.set(commentId).equalTo(record::getCommentId)
.set(replyContent).equalTo(record::getReplyContent)
.set(location).equalTo(record::getLocation)
.set(auditStatus).equalTo(record::getAuditStatus)
.set(createTime).equalTo(record::getCreateTime)
.set(createUserId).equalTo(record::getCreateUserId);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
static UpdateDSL<UpdateModel> updateSelectiveColumns(BookCommentReply record, UpdateDSL<UpdateModel> dsl) {
return dsl.set(id).equalToWhenPresent(record::getId)
.set(commentId).equalToWhenPresent(record::getCommentId)
.set(replyContent).equalToWhenPresent(record::getReplyContent)
.set(location).equalToWhenPresent(record::getLocation)
.set(auditStatus).equalToWhenPresent(record::getAuditStatus)
.set(createTime).equalToWhenPresent(record::getCreateTime)
.set(createUserId).equalToWhenPresent(record::getCreateUserId);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int updateByPrimaryKey(BookCommentReply record) {
return update(c ->
c.set(commentId).equalTo(record::getCommentId)
.set(replyContent).equalTo(record::getReplyContent)
.set(location).equalTo(record::getLocation)
.set(auditStatus).equalTo(record::getAuditStatus)
.set(createTime).equalTo(record::getCreateTime)
.set(createUserId).equalTo(record::getCreateUserId)
.where(id, isEqualTo(record::getId))
);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int updateByPrimaryKeySelective(BookCommentReply record) {
return update(c ->
c.set(commentId).equalToWhenPresent(record::getCommentId)
.set(replyContent).equalToWhenPresent(record::getReplyContent)
.set(location).equalToWhenPresent(record::getLocation)
.set(auditStatus).equalToWhenPresent(record::getAuditStatus)
.set(createTime).equalToWhenPresent(record::getCreateTime)
.set(createUserId).equalToWhenPresent(record::getCreateUserId)
.where(id, isEqualTo(record::getId))
);
}
}

View File

@@ -37,6 +37,9 @@ public final class CrawlSingleTaskDynamicSqlSupport {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Byte> excCount = crawlSingleTask.excCount;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Integer> crawlChapters = crawlSingleTask.crawlChapters;
@Generated("org.mybatis.generator.api.MyBatisGenerator")
public static final SqlColumn<Date> createTime = crawlSingleTask.createTime;
@@ -60,6 +63,8 @@ public final class CrawlSingleTaskDynamicSqlSupport {
public final SqlColumn<Byte> excCount = column("exc_count", JDBCType.TINYINT);
public final SqlColumn<Integer> crawlChapters = column("crawl_chapters", JDBCType.INTEGER);
public final SqlColumn<Date> createTime = column("create_time", JDBCType.TIMESTAMP);
public CrawlSingleTask() {

View File

@@ -35,7 +35,7 @@ import org.mybatis.dynamic.sql.util.mybatis3.MyBatis3Utils;
@Mapper
public interface CrawlSingleTaskMapper {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
BasicColumn[] selectList = BasicColumn.columnList(id, sourceId, sourceName, sourceBookId, catId, bookName, authorName, taskStatus, excCount, createTime);
BasicColumn[] selectList = BasicColumn.columnList(id, sourceId, sourceName, sourceBookId, catId, bookName, authorName, taskStatus, excCount, crawlChapters, createTime);
@Generated("org.mybatis.generator.api.MyBatisGenerator")
@SelectProvider(type=SqlProviderAdapter.class, method="select")
@@ -70,6 +70,7 @@ public interface CrawlSingleTaskMapper {
@Result(column="author_name", property="authorName", jdbcType=JdbcType.VARCHAR),
@Result(column="task_status", property="taskStatus", jdbcType=JdbcType.TINYINT),
@Result(column="exc_count", property="excCount", jdbcType=JdbcType.TINYINT),
@Result(column="crawl_chapters", property="crawlChapters", jdbcType=JdbcType.INTEGER),
@Result(column="create_time", property="createTime", jdbcType=JdbcType.TIMESTAMP)
})
List<CrawlSingleTask> selectMany(SelectStatementProvider selectStatement);
@@ -90,7 +91,7 @@ public interface CrawlSingleTaskMapper {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int deleteByPrimaryKey(Long id_) {
return delete(c ->
return delete(c ->
c.where(id, isEqualTo(id_))
);
}
@@ -99,15 +100,16 @@ public interface CrawlSingleTaskMapper {
default int insert(CrawlSingleTask record) {
return MyBatis3Utils.insert(this::insert, record, crawlSingleTask, c ->
c.map(id).toProperty("id")
.map(sourceId).toProperty("sourceId")
.map(sourceName).toProperty("sourceName")
.map(sourceBookId).toProperty("sourceBookId")
.map(catId).toProperty("catId")
.map(bookName).toProperty("bookName")
.map(authorName).toProperty("authorName")
.map(taskStatus).toProperty("taskStatus")
.map(excCount).toProperty("excCount")
.map(createTime).toProperty("createTime")
.map(sourceId).toProperty("sourceId")
.map(sourceName).toProperty("sourceName")
.map(sourceBookId).toProperty("sourceBookId")
.map(catId).toProperty("catId")
.map(bookName).toProperty("bookName")
.map(authorName).toProperty("authorName")
.map(taskStatus).toProperty("taskStatus")
.map(excCount).toProperty("excCount")
.map(crawlChapters).toProperty("crawlChapters")
.map(createTime).toProperty("createTime")
);
}
@@ -115,15 +117,16 @@ public interface CrawlSingleTaskMapper {
default int insertMultiple(Collection<CrawlSingleTask> records) {
return MyBatis3Utils.insertMultiple(this::insertMultiple, records, crawlSingleTask, c ->
c.map(id).toProperty("id")
.map(sourceId).toProperty("sourceId")
.map(sourceName).toProperty("sourceName")
.map(sourceBookId).toProperty("sourceBookId")
.map(catId).toProperty("catId")
.map(bookName).toProperty("bookName")
.map(authorName).toProperty("authorName")
.map(taskStatus).toProperty("taskStatus")
.map(excCount).toProperty("excCount")
.map(createTime).toProperty("createTime")
.map(sourceId).toProperty("sourceId")
.map(sourceName).toProperty("sourceName")
.map(sourceBookId).toProperty("sourceBookId")
.map(catId).toProperty("catId")
.map(bookName).toProperty("bookName")
.map(authorName).toProperty("authorName")
.map(taskStatus).toProperty("taskStatus")
.map(excCount).toProperty("excCount")
.map(crawlChapters).toProperty("crawlChapters")
.map(createTime).toProperty("createTime")
);
}
@@ -131,15 +134,16 @@ public interface CrawlSingleTaskMapper {
default int insertSelective(CrawlSingleTask record) {
return MyBatis3Utils.insert(this::insert, record, crawlSingleTask, c ->
c.map(id).toPropertyWhenPresent("id", record::getId)
.map(sourceId).toPropertyWhenPresent("sourceId", record::getSourceId)
.map(sourceName).toPropertyWhenPresent("sourceName", record::getSourceName)
.map(sourceBookId).toPropertyWhenPresent("sourceBookId", record::getSourceBookId)
.map(catId).toPropertyWhenPresent("catId", record::getCatId)
.map(bookName).toPropertyWhenPresent("bookName", record::getBookName)
.map(authorName).toPropertyWhenPresent("authorName", record::getAuthorName)
.map(taskStatus).toPropertyWhenPresent("taskStatus", record::getTaskStatus)
.map(excCount).toPropertyWhenPresent("excCount", record::getExcCount)
.map(createTime).toPropertyWhenPresent("createTime", record::getCreateTime)
.map(sourceId).toPropertyWhenPresent("sourceId", record::getSourceId)
.map(sourceName).toPropertyWhenPresent("sourceName", record::getSourceName)
.map(sourceBookId).toPropertyWhenPresent("sourceBookId", record::getSourceBookId)
.map(catId).toPropertyWhenPresent("catId", record::getCatId)
.map(bookName).toPropertyWhenPresent("bookName", record::getBookName)
.map(authorName).toPropertyWhenPresent("authorName", record::getAuthorName)
.map(taskStatus).toPropertyWhenPresent("taskStatus", record::getTaskStatus)
.map(excCount).toPropertyWhenPresent("excCount", record::getExcCount)
.map(crawlChapters).toPropertyWhenPresent("crawlChapters", record::getCrawlChapters)
.map(createTime).toPropertyWhenPresent("createTime", record::getCreateTime)
);
}
@@ -173,35 +177,7 @@ public interface CrawlSingleTaskMapper {
@Generated("org.mybatis.generator.api.MyBatisGenerator")
static UpdateDSL<UpdateModel> updateAllColumns(CrawlSingleTask record, UpdateDSL<UpdateModel> dsl) {
return dsl.set(id).equalTo(record::getId)
.set(sourceId).equalTo(record::getSourceId)
.set(sourceName).equalTo(record::getSourceName)
.set(sourceBookId).equalTo(record::getSourceBookId)
.set(catId).equalTo(record::getCatId)
.set(bookName).equalTo(record::getBookName)
.set(authorName).equalTo(record::getAuthorName)
.set(taskStatus).equalTo(record::getTaskStatus)
.set(excCount).equalTo(record::getExcCount)
.set(createTime).equalTo(record::getCreateTime);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
static UpdateDSL<UpdateModel> updateSelectiveColumns(CrawlSingleTask record, UpdateDSL<UpdateModel> dsl) {
return dsl.set(id).equalToWhenPresent(record::getId)
.set(sourceId).equalToWhenPresent(record::getSourceId)
.set(sourceName).equalToWhenPresent(record::getSourceName)
.set(sourceBookId).equalToWhenPresent(record::getSourceBookId)
.set(catId).equalToWhenPresent(record::getCatId)
.set(bookName).equalToWhenPresent(record::getBookName)
.set(authorName).equalToWhenPresent(record::getAuthorName)
.set(taskStatus).equalToWhenPresent(record::getTaskStatus)
.set(excCount).equalToWhenPresent(record::getExcCount)
.set(createTime).equalToWhenPresent(record::getCreateTime);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int updateByPrimaryKey(CrawlSingleTask record) {
return update(c ->
c.set(sourceId).equalTo(record::getSourceId)
.set(sourceId).equalTo(record::getSourceId)
.set(sourceName).equalTo(record::getSourceName)
.set(sourceBookId).equalTo(record::getSourceBookId)
.set(catId).equalTo(record::getCatId)
@@ -209,15 +185,14 @@ public interface CrawlSingleTaskMapper {
.set(authorName).equalTo(record::getAuthorName)
.set(taskStatus).equalTo(record::getTaskStatus)
.set(excCount).equalTo(record::getExcCount)
.set(createTime).equalTo(record::getCreateTime)
.where(id, isEqualTo(record::getId))
);
.set(crawlChapters).equalTo(record::getCrawlChapters)
.set(createTime).equalTo(record::getCreateTime);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int updateByPrimaryKeySelective(CrawlSingleTask record) {
return update(c ->
c.set(sourceId).equalToWhenPresent(record::getSourceId)
static UpdateDSL<UpdateModel> updateSelectiveColumns(CrawlSingleTask record, UpdateDSL<UpdateModel> dsl) {
return dsl.set(id).equalToWhenPresent(record::getId)
.set(sourceId).equalToWhenPresent(record::getSourceId)
.set(sourceName).equalToWhenPresent(record::getSourceName)
.set(sourceBookId).equalToWhenPresent(record::getSourceBookId)
.set(catId).equalToWhenPresent(record::getCatId)
@@ -225,8 +200,41 @@ public interface CrawlSingleTaskMapper {
.set(authorName).equalToWhenPresent(record::getAuthorName)
.set(taskStatus).equalToWhenPresent(record::getTaskStatus)
.set(excCount).equalToWhenPresent(record::getExcCount)
.set(createTime).equalToWhenPresent(record::getCreateTime)
.where(id, isEqualTo(record::getId))
.set(crawlChapters).equalToWhenPresent(record::getCrawlChapters)
.set(createTime).equalToWhenPresent(record::getCreateTime);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int updateByPrimaryKey(CrawlSingleTask record) {
return update(c ->
c.set(sourceId).equalTo(record::getSourceId)
.set(sourceName).equalTo(record::getSourceName)
.set(sourceBookId).equalTo(record::getSourceBookId)
.set(catId).equalTo(record::getCatId)
.set(bookName).equalTo(record::getBookName)
.set(authorName).equalTo(record::getAuthorName)
.set(taskStatus).equalTo(record::getTaskStatus)
.set(excCount).equalTo(record::getExcCount)
.set(crawlChapters).equalTo(record::getCrawlChapters)
.set(createTime).equalTo(record::getCreateTime)
.where(id, isEqualTo(record::getId))
);
}
@Generated("org.mybatis.generator.api.MyBatisGenerator")
default int updateByPrimaryKeySelective(CrawlSingleTask record) {
return update(c ->
c.set(sourceId).equalToWhenPresent(record::getSourceId)
.set(sourceName).equalToWhenPresent(record::getSourceName)
.set(sourceBookId).equalToWhenPresent(record::getSourceBookId)
.set(catId).equalToWhenPresent(record::getCatId)
.set(bookName).equalToWhenPresent(record::getBookName)
.set(authorName).equalToWhenPresent(record::getAuthorName)
.set(taskStatus).equalToWhenPresent(record::getTaskStatus)
.set(excCount).equalToWhenPresent(record::getExcCount)
.set(crawlChapters).equalToWhenPresent(record::getCrawlChapters)
.set(createTime).equalToWhenPresent(record::getCreateTime)
.where(id, isEqualTo(record::getId))
);
}
}

View File

@@ -25,10 +25,10 @@ http:
# 是否开启 HTTP 代理true-开启false-不开启
enabled: false
# 代理 IP
ip: us.swiftproxy.net
ip: proxy.bestproxy.com
# 代理端口号
port: 7878
port: 2312
# 代理用户名
username: swiftproxy_u
username: bestproxy_u
# 代理密码
password: swiftproxy_p
password: bestproxy_p

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>novel</artifactId>
<groupId>com.java2nb</groupId>
<version>5.1.1</version>
<version>5.2.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -38,10 +38,10 @@ http:
# 是否开启 HTTP 代理true-开启false-不开启
enabled: false
# 代理 IP
ip: us.swiftproxy.net
ip: proxy.bestproxy.com
# 代理端口号
port: 7878
port: 2312
# 代理用户名
username: swiftproxy_u
username: bestproxy_u
# 代理密码
password: swiftproxy_p
password: bestproxy_p

View File

@@ -72,17 +72,15 @@ public class CrawlController {
if(url.startsWith("https://")||url.startsWith("http://")){
String refreshCache="1";
if(!refreshCache.equals(isRefresh)) {
Object cache = cacheService.getObject(CacheKey.BOOK_TEST_PARSE + url);
if (cache == null) {
html = cacheService.get(CacheKey.BOOK_TEST_PARSE + url);
if (html == null) {
isRefresh="1";
}else {
html = (String) cache;
}
}
if(refreshCache.equals(isRefresh)){
html = HttpUtil.getByHttpClientWithChrome(url);
if (html != null) {
cacheService.setObject(CacheKey.BOOK_TEST_PARSE + url, html, 60 * 10);
cacheService.set(CacheKey.BOOK_TEST_PARSE + url, html, 60 * 10);
}else{
resultMap.put("msg","html is null");
return RestResult.ok(resultMap);
@@ -153,6 +151,14 @@ public class CrawlController {
return RestResult.ok();
}
/**
* 采集任务进度查询
* */
@GetMapping("getTaskProgress/{id}")
public RestResult<Integer> getTaskProgress(@PathVariable("id") Long id){
return RestResult.ok(crawlService.getTaskProgress(id));
}

View File

@@ -7,6 +7,6 @@ import com.java2nb.novel.entity.Book;
* */
public interface CrawlBookHandler {
void handle(Book book);
void handle(Book book) throws InterruptedException;
}

View File

@@ -5,19 +5,19 @@ import com.java2nb.novel.core.utils.StringUtil;
import com.java2nb.novel.entity.Book;
import com.java2nb.novel.entity.BookContent;
import com.java2nb.novel.entity.BookIndex;
import com.java2nb.novel.entity.CrawlSingleTask;
import com.java2nb.novel.utils.Constants;
import com.java2nb.novel.utils.CrawlHttpClient;
import io.github.xxyopen.util.IdWorker;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -26,6 +26,7 @@ import java.util.regex.Pattern;
*
* @author Administrator
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CrawlParser {
@@ -34,8 +35,48 @@ public class CrawlParser {
private final CrawlHttpClient crawlHttpClient;
@SneakyThrows
public void parseBook(RuleBean ruleBean, String bookId, CrawlBookHandler handler) {
private final StringRedisTemplate stringRedisTemplate;
/**
* 爬虫源采集章节数量缓存key
*/
private static final String CRAWL_SOURCE_CHAPTER_COUNT_CACHE_KEY = "crawlSource:chapterCount:";
/**
* 爬虫任务进度
*/
private final Map<Long, Integer> crawlTaskProgress = new HashMap<>();
/**
* 获取爬虫任务进度
*/
public Integer getCrawlTaskProgress(Long taskId) {
return crawlTaskProgress.get(taskId);
}
/**
* 移除爬虫任务进度
*/
public void removeCrawlTaskProgress(Long taskId) {
crawlTaskProgress.remove(taskId);
}
/**
* 获取爬虫源采集的章节数量
*/
public Long getCrawlSourceChapterCount(Integer sourceId) {
return Optional.ofNullable(
stringRedisTemplate.opsForValue().get(CRAWL_SOURCE_CHAPTER_COUNT_CACHE_KEY + sourceId)).map(v -> {
try {
return Long.parseLong(v);
} catch (NumberFormatException e) {
return 0L;
}
}).orElse(0L);
}
public void parseBook(RuleBean ruleBean, String bookId, CrawlBookHandler handler)
throws InterruptedException {
Book book = new Book();
String bookDetailUrl = ruleBean.getBookDetailUrl().replace("{bookId}", bookId);
String bookDetailHtml = crawlHttpClient.get(bookDetailUrl, ruleBean.getCharset());
@@ -97,6 +138,22 @@ public class CrawlParser {
.replaceAll("<p>\\s*</p>", "")
.replaceAll("<p>", "")
.replaceAll("</p>", "<br/>");
// 小说简介过滤
String filterDesc = ruleBean.getFilterDesc();
if (StringUtils.isNotBlank(filterDesc)) {
String[] filterRules = filterDesc.replace("\r\n", "\n").split("\n");
for (String filterRule : filterRules) {
if (StringUtils.isNotBlank(filterRule)) {
desc = desc.replaceAll(filterRule, "");
}
}
}
// 去除小说简介前后空格
desc = desc.trim();
// 去除小说简介末尾冗余的小说名
if (desc.endsWith(bookName)) {
desc = desc.substring(0, desc.length() - bookName.length());
}
//设置书籍简介
book.setBookDesc(desc);
if (StringUtils.isNotBlank(ruleBean.getStatusPatten())) {
@@ -120,8 +177,12 @@ public class CrawlParser {
if (isFindUpdateTime) {
String updateTime = updateTimeMatch.group(1);
//设置更新时间
book.setLastIndexUpdateTime(
new SimpleDateFormat(ruleBean.getUpadateTimeFormatPatten()).parse(updateTime));
try {
book.setLastIndexUpdateTime(
new SimpleDateFormat(ruleBean.getUpadateTimeFormatPatten()).parse(updateTime));
} catch (ParseException e) {
log.error("解析最新章节更新时间出错", e);
}
}
}
@@ -133,7 +194,7 @@ public class CrawlParser {
} else if (book.getVisitCount() != null && book.getScore() == null) {
//随机根据访问次数生成评分
book.setScore(RandomBookInfoUtil.getScoreByVisitCount(book.getVisitCount()));
} else if (book.getVisitCount() == null && book.getScore() == null) {
} else if (book.getVisitCount() == null) {
//都没有,设置成固定值
book.setVisitCount(Constants.VISIT_COUNT_DEFAULT);
book.setScore(6.5f);
@@ -143,8 +204,14 @@ public class CrawlParser {
handler.handle(book);
}
public boolean parseBookIndexAndContent(String sourceBookId, Book book, RuleBean ruleBean,
Map<Integer, BookIndex> existBookIndexMap, CrawlBookChapterHandler handler) {
public boolean parseBookIndexAndContent(String sourceBookId, Book book, RuleBean ruleBean, Integer sourceId,
Map<Integer, BookIndex> existBookIndexMap, CrawlBookChapterHandler handler, CrawlSingleTask task)
throws InterruptedException {
if (task != null) {
// 开始采集
crawlTaskProgress.put(task.getId(), 0);
}
Date currentDate = new Date();
@@ -202,7 +269,7 @@ public class CrawlParser {
calResult = sourceIndexId.substring(0, sourceBookId.length() - y);
}
if (calResult.length() == 0) {
if (calResult.isEmpty()) {
calResult = "0";
}
@@ -231,6 +298,8 @@ public class CrawlParser {
}
}
}
// 去除小说内容末尾的所有换行
content = removeTrailingBrTags(content);
//插入章节目录和章节内容
BookIndex bookIndex = new BookIndex();
bookIndex.setIndexName(indexName);
@@ -266,6 +335,13 @@ public class CrawlParser {
}
bookIndex.setUpdateTime(currentDate);
if (task != null) {
// 更新单本任务采集进度
crawlTaskProgress.put(task.getId(), indexList.size());
}
// 更新爬虫源采集章节数量
stringRedisTemplate.opsForValue().increment(CRAWL_SOURCE_CHAPTER_COUNT_CACHE_KEY + sourceId);
}
@@ -275,10 +351,10 @@ public class CrawlParser {
isFindIndex = indexIdMatch.find() & indexNameMatch.find();
}
if (indexList.size() > 0) {
if (!indexList.isEmpty()) {
//如果有爬到最新章节,则设置小说主表的最新章节信息
//获取爬取到的最新章节
BookIndex lastIndex = indexList.get(indexList.size() - 1);
BookIndex lastIndex = indexList.getLast();
book.setLastIndexId(lastIndex.getId());
book.setLastIndexName(lastIndex.getIndexName());
book.setLastIndexUpdateTime(currentDate);
@@ -287,7 +363,7 @@ public class CrawlParser {
book.setWordCount(totalWordCount);
book.setUpdateTime(currentDate);
if (indexList.size() == contentList.size() && indexList.size() > 0) {
if (indexList.size() == contentList.size() && !indexList.isEmpty()) {
handler.handle(new ChapterBean() {{
setBookIndexList(indexList);
@@ -307,4 +383,12 @@ public class CrawlParser {
return false;
}
/**
* 删除字符串末尾的所有 <br> 类似标签(允许各种空格)
*/
public static String removeTrailingBrTags(String str) {
return str.replaceAll("(?i)(?:\\s*<\\s*br\\s*/?\\s*>)++(?:\\s|\\u3000)*$", "");
}
}

View File

@@ -45,6 +45,7 @@ public class RuleBean {
private String visitCountPatten;
private String descStart;
private String descEnd;
private String filterDesc;
private String upadateTimePatten;
private String upadateTimeFormatPatten;
private String bookIndexUrl;

View File

@@ -74,10 +74,10 @@ public class StarterListener implements ServletContextInitializer {
needUpdateBook.getId());
//解析章节目录
crawlParser.parseBookIndexAndContent(needUpdateBook.getCrawlBookId(), book,
ruleBean, existBookIndexMap, chapter -> {
bookService.updateBookAndIndexAndContent(book, chapter.getBookIndexList(),
chapter.getBookContentList(), existBookIndexMap);
});
ruleBean, needUpdateBook.getCrawlSourceId(), existBookIndexMap,
chapter -> bookService.updateBookAndIndexAndContent(book,
chapter.getBookIndexList(),
chapter.getBookContentList(), existBookIndexMap), null);
});
} catch (Exception e) {
log.error(e.getMessage(), e);
@@ -109,9 +109,8 @@ public class StarterListener implements ServletContextInitializer {
//查询爬虫规则
CrawlSource source = crawlService.queryCrawlSource(task.getSourceId());
RuleBean ruleBean = new ObjectMapper().readValue(source.getCrawlRule(), RuleBean.class);
if (crawlService.parseBookAndSave(task.getCatId(), ruleBean, task.getSourceId(),
task.getSourceBookId())) {
task.getSourceBookId(), task)) {
//采集成功
crawlStatus = 1;
}
@@ -124,6 +123,7 @@ public class StarterListener implements ServletContextInitializer {
} catch (Exception e) {
log.error(e.getMessage(), e);
}
if (task != null) {
crawlService.updateCrawlSingleTask(task, crawlStatus);
}

View File

@@ -1,61 +0,0 @@
package com.java2nb.novel.core.schedule;
import com.java2nb.novel.core.cache.CacheKey;
import com.java2nb.novel.core.cache.CacheService;
import com.java2nb.novel.entity.CrawlSource;
import com.java2nb.novel.service.CrawlService;
import io.github.xxyopen.util.ThreadUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
/**
* 爬虫线程监控器,监控执行完成的爬虫源,并修改状态
*
* @author Administrator
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class CrawlThreadMonitor {
private final CacheService cacheService;
private final CrawlService crawlService;
@Scheduled(fixedRate = 1000 * 60 * 5)
public void monitor() {
//查询需要监控的正在运行的爬虫源
List<CrawlSource> sources = crawlService.queryCrawlSourceByStatus((byte) 1);
for (CrawlSource source : sources) {
Set<Long> runningCrawlThreadIds = (Set<Long>) cacheService.getObject(CacheKey.RUNNING_CRAWL_THREAD_KEY_PREFIX + source.getId());
boolean sourceStop = true;
if (runningCrawlThreadIds != null) {
for (Long threadId : runningCrawlThreadIds) {
Thread thread = ThreadUtil.findThread(threadId);
if (thread != null && thread.isAlive()) {
//有活跃线程,说明该爬虫源正在运行,数据库中状态正确,不需要修改
sourceStop = false;
}
}
}
if (sourceStop) {
crawlService.updateCrawlSourceStatus(source.getId(), (byte) 0);
}
}
}
}

View File

@@ -47,13 +47,15 @@ public interface CrawlService {
/**
* 采集并保存小说
* @param catId 分类ID
* @param bookId 小说ID
* @param sourceId 源ID
*
* @param catId 分类ID
* @param ruleBean 采集规则\
* @param sourceId 源ID
* @param bookId 小说ID
* @param task
* @return true:成功false:失败
* */
boolean parseBookAndSave(int catId, RuleBean ruleBean, Integer sourceId, String bookId);
*/
boolean parseBookAndSave(int catId, RuleBean ruleBean, Integer sourceId, String bookId, CrawlSingleTask task) throws InterruptedException;
/**
* 根据爬虫状态查询爬虫源集合
@@ -117,4 +119,9 @@ public interface CrawlService {
* @return
*/
CrawlSource getCrawlSource(Integer id);
/**
* 采集任务进度查询
* */
Integer getTaskProgress(Long taskId);
}

View File

@@ -2,12 +2,10 @@ package com.java2nb.novel.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.pagehelper.PageHelper;
import com.java2nb.novel.core.cache.CacheKey;
import com.java2nb.novel.core.cache.CacheService;
import com.java2nb.novel.core.crawl.CrawlParser;
import com.java2nb.novel.core.crawl.RuleBean;
import com.java2nb.novel.core.enums.ResponseStatus;
import com.java2nb.novel.core.utils.SpringUtil;
import com.java2nb.novel.entity.Book;
import com.java2nb.novel.entity.CrawlSingleTask;
import com.java2nb.novel.entity.CrawlSource;
@@ -34,6 +32,7 @@ import org.mybatis.dynamic.sql.render.RenderingStrategies;
import org.mybatis.dynamic.sql.select.render.SelectStatementProvider;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
@@ -59,12 +58,14 @@ public class CrawlServiceImpl implements CrawlService {
private final BookService bookService;
private final CacheService cacheService;
private final IdWorker idWorker = IdWorker.INSTANCE;
private final CrawlHttpClient crawlHttpClient;
private final Map<Integer, Byte> crawlSourceStatusMap = new HashMap<>();
private final Map<Integer, Set<Long>> runningCrawlThread = new HashMap<>();
@Override
public void addCrawlSource(CrawlSource source) {
@@ -99,12 +100,19 @@ public class CrawlServiceImpl implements CrawlService {
PageHelper.startPage(page, pageSize);
SelectStatementProvider render = select(id, sourceName, sourceStatus, createTime, updateTime)
.from(crawlSource)
.orderBy(updateTime)
.orderBy(updateTime.descending())
.build()
.render(RenderingStrategies.MYBATIS3);
List<CrawlSource> crawlSources = crawlSourceMapper.selectMany(render);
PageBean<CrawlSource> pageBean = PageBuilder.build(crawlSources);
pageBean.setList(BeanUtil.copyList(crawlSources, CrawlSourceVO.class));
List<CrawlSourceVO> crawlSourceVOS = BeanUtil.copyList(crawlSources, CrawlSourceVO.class);
crawlSourceVOS.forEach(crawlSource -> {
crawlSource.setSourceStatus(
Optional.ofNullable(crawlSourceStatusMap.get(crawlSource.getId())).orElse((byte) 0));
crawlSource.setChapterCount(crawlParser.getCrawlSourceChapterCount(crawlSource.getId()));
}
);
pageBean.setList(crawlSourceVOS);
return pageBean;
}
@@ -112,14 +120,13 @@ public class CrawlServiceImpl implements CrawlService {
@Override
public void openOrCloseCrawl(Integer sourceId, Byte sourceStatus) {
//判断是开启还是关闭,如果是关闭,则修改数据库状态后获取该爬虫正在运行的线程集合并全部停止
//如果是开启,先查询数据库中状态,判断该爬虫源是否还在运行,如果在运行,则忽略,
// 如果没有则修改数据库状态并启动线程爬取小说数据加入到runningCrawlThread中
// 判断是开启还是关闭,如果是关闭,则获取该爬虫正在运行的线程集合并全部中断
// 如果是开启,先判断该爬虫源是否还在运行,如果在运行,则忽略,如果没有运行则启动线程爬取小说数据并加入到runningCrawlThread中
// 最后,保存爬虫源状态
if (sourceStatus == (byte) 0) {
//关闭,直接修改数据库状态,并直接修改数据库状态后获取该爬虫正在运行的线程集合全部停止
SpringUtil.getBean(CrawlService.class).updateCrawlSourceStatus(sourceId, sourceStatus);
Set<Long> runningCrawlThreadId = (Set<Long>) cacheService.getObject(
CacheKey.RUNNING_CRAWL_THREAD_KEY_PREFIX + sourceId);
// 关闭
// 将该爬虫源正在运行的线程集合全部停止
Set<Long> runningCrawlThreadId = runningCrawlThread.get(sourceId);
if (runningCrawlThreadId != null) {
for (Long ThreadId : runningCrawlThreadId) {
Thread thread = ThreadUtil.findThread(ThreadId);
@@ -131,16 +138,13 @@ public class CrawlServiceImpl implements CrawlService {
} else {
//开启
//查询爬虫源状态和规则
CrawlSource source = queryCrawlSource(sourceId);
Byte realSourceStatus = source.getSourceStatus();
// 开启
Byte realSourceStatus = Optional.ofNullable(crawlSourceStatusMap.get(sourceId)).orElse((byte) 0);
if (realSourceStatus == (byte) 0) {
//该爬虫源已经停止运行了,修改数据库状态并启动线程爬取小说数据加入到runningCrawlThread中
SpringUtil.getBean(CrawlService.class).updateCrawlSourceStatus(sourceId, sourceStatus);
// 查询爬虫源规则
CrawlSource source = queryCrawlSource(sourceId);
//该爬虫源已经停止运行了,启动线程爬取小说数据并将线程加入到runningCrawlThread中
RuleBean ruleBean = new ObjectMapper().readValue(source.getCrawlRule(), RuleBean.class);
Set<Long> threadIds = new HashSet<>();
//按分类开始爬虫解析任务
for (int i = 1; i < 8; i++) {
@@ -149,16 +153,15 @@ public class CrawlServiceImpl implements CrawlService {
thread.start();
//thread加入到监控缓存中
threadIds.add(thread.getId());
}
cacheService.setObject(CacheKey.RUNNING_CRAWL_THREAD_KEY_PREFIX + sourceId, threadIds);
runningCrawlThread.put(sourceId, threadIds);
}
}
// 保存爬虫源状态
crawlSourceStatusMap.put(sourceId, sourceStatus);
}
@Override
@@ -196,6 +199,16 @@ public class CrawlServiceImpl implements CrawlService {
List<CrawlSingleTask> crawlSingleTasks = crawlSingleTaskMapper.selectMany(render);
PageBean<CrawlSingleTask> pageBean = PageBuilder.build(crawlSingleTasks);
pageBean.setList(BeanUtil.copyList(crawlSingleTasks, CrawlSingleTaskVO.class));
for (CrawlSingleTask crawlSingleTask : pageBean.getList()) {
if (crawlSingleTask.getTaskStatus() == 2
&& crawlParser.getCrawlTaskProgress(crawlSingleTask.getId()) != null) {
// 如果排队中的任务有任务进度,将排队中的任务状态修改成采集中并设置任务进度
crawlSingleTask.setTaskStatus((byte) 3);
crawlSingleTask.setCrawlChapters(crawlParser.getCrawlTaskProgress(crawlSingleTask.getId()));
// 只会有一个任务在采集中
break;
}
}
return pageBean;
}
@@ -225,21 +238,27 @@ public class CrawlServiceImpl implements CrawlService {
excCount += 1;
task.setExcCount(excCount);
if (status == 1 || excCount == 5) {
//当采集成功或者采集次数等于5则更新采集最终状态并停止采集
// 当采集成功或者采集次数等于5则更新采集最终状态并停止采集
task.setTaskStatus(status);
}
if (status == 1) {
// 当采集成功,保存采集的章节数量
task.setCrawlChapters(crawlParser.getCrawlTaskProgress(task.getId()));
}
crawlSingleTaskMapper.updateByPrimaryKeySelective(task);
// 删除任务进度
crawlParser.removeCrawlTaskProgress(task.getId());
}
@Override
public CrawlSource getCrawlSource(Integer id) {
Optional<CrawlSource> opt = crawlSourceMapper.selectByPrimaryKey(id);
if (opt.isPresent()) {
CrawlSource crawlSource = opt.get();
return crawlSource;
}
return null;
return crawlSourceMapper.selectByPrimaryKey(id).orElse(null);
}
@Override
public Integer getTaskProgress(Long taskId) {
return Optional.ofNullable(crawlParser.getCrawlTaskProgress(taskId)).orElse(0);
}
/**
@@ -248,6 +267,11 @@ public class CrawlServiceImpl implements CrawlService {
@Override
public void parseBookList(int catId, RuleBean ruleBean, Integer sourceId) {
String catIdRule = ruleBean.getCatIdRule().get("catId" + catId);
if (StringUtils.isBlank(catIdRule)) {
return;
}
//当前页码1
int page = 1;
int totalPage = page;
@@ -255,73 +279,91 @@ public class CrawlServiceImpl implements CrawlService {
while (page <= totalPage) {
try {
String catIdRule = ruleBean.getCatIdRule().get("catId" + catId);
if (StringUtils.isNotBlank(catIdRule)) {
String catBookListUrl = "";
if (StringUtils.isNotBlank(ruleBean.getBookListUrl())) {
// 兼容老规则
// 拼接分类URL
catBookListUrl = ruleBean.getBookListUrl()
.replace("{catId}", catIdRule)
.replace("{page}", page + "");
} else {
// 新规则
// 拼接分类URL
catBookListUrl = catIdRule.replace("{page}", page + "");
}
log.info("catBookListUrl{}", catBookListUrl);
String catBookListUrl;
if (StringUtils.isNotBlank(ruleBean.getBookListUrl())) {
// 兼容老规则
// 拼接分类URL
catBookListUrl = ruleBean.getBookListUrl()
.replace("{catId}", catIdRule)
.replace("{page}", page + "");
} else {
// 新规则
// 拼接分类URL
catBookListUrl = catIdRule.replace("{page}", page + "");
}
log.info("catBookListUrl{}", catBookListUrl);
String bookListHtml = crawlHttpClient.get(catBookListUrl, ruleBean.getCharset());
if (bookListHtml != null) {
Pattern bookIdPatten = Pattern.compile(ruleBean.getBookIdPatten());
Matcher bookIdMatcher = bookIdPatten.matcher(bookListHtml);
boolean isFindBookId = bookIdMatcher.find();
while (isFindBookId) {
try {
//1.阻塞过程(使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时)
//捕获中断异常InterruptedException来退出线程。
//2.非阻塞过程中通过判断中断标志来退出线程。
if (Thread.currentThread().isInterrupted()) {
return;
}
String bookId = bookIdMatcher.group(1);
parseBookAndSave(catId, ruleBean, sourceId, bookId);
} catch (Exception e) {
log.error(e.getMessage(), e);
String bookListHtml = crawlHttpClient.get(catBookListUrl, ruleBean.getCharset());
if (bookListHtml != null) {
Pattern bookIdPatten = Pattern.compile(ruleBean.getBookIdPatten());
Matcher bookIdMatcher = bookIdPatten.matcher(bookListHtml);
boolean isFindBookId = bookIdMatcher.find();
while (isFindBookId) {
try {
//1.阻塞过程(使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时)
//捕获中断异常InterruptedException来退出线程。
//2.非阻塞过程中通过判断中断标志来退出线程。
if (Thread.currentThread().isInterrupted()) {
return;
}
isFindBookId = bookIdMatcher.find();
String bookId = bookIdMatcher.group(1);
parseBookAndSave(catId, ruleBean, sourceId, bookId, null);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
//1.阻塞过程(使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时)
//捕获中断异常InterruptedException来退出线程。
//2.非阻塞过程中通过判断中断标志来退出线程。
return;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
Pattern totalPagePatten = Pattern.compile(ruleBean.getTotalPagePatten());
Matcher totalPageMatcher = totalPagePatten.matcher(bookListHtml);
boolean isFindTotalPage = totalPageMatcher.find();
if (isFindTotalPage) {
isFindBookId = bookIdMatcher.find();
}
totalPage = Integer.parseInt(totalPageMatcher.group(1));
}
Pattern totalPagePatten = Pattern.compile(ruleBean.getTotalPagePatten());
Matcher totalPageMatcher = totalPagePatten.matcher(bookListHtml);
boolean isFindTotalPage = totalPageMatcher.find();
if (isFindTotalPage) {
totalPage = Integer.parseInt(totalPageMatcher.group(1));
}
}
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
//1.阻塞过程(使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时)
//捕获中断异常InterruptedException来退出线程。
//2.非阻塞过程中通过判断中断标志来退出线程。
return;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
if (page == totalPage) {
if (page >= totalPage) {
// 第一遍采集完成,翻到第一页,继续第二次采集,适用于分页数比较少的最近更新列表
page = 0;
page = 1;
try {
// 第一遍采集完成休眠1分钟
Thread.sleep(Duration.ofMinutes(1));
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
//1.阻塞过程(使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时)
//捕获中断异常InterruptedException来退出线程。
//2.非阻塞过程中通过判断中断标志来退出线程。
return;
}
} else {
page += 1;
}
page += 1;
}
}
@Override
public boolean parseBookAndSave(int catId, RuleBean ruleBean, Integer sourceId, String bookId) {
public boolean parseBookAndSave(int catId, RuleBean ruleBean, Integer sourceId, String bookId, CrawlSingleTask task)
throws InterruptedException {
final AtomicBoolean parseResult = new AtomicBoolean(false);
@@ -349,11 +391,11 @@ public class CrawlServiceImpl implements CrawlService {
book.setCrawlLastTime(new Date());
book.setId(idWorker.nextId());
//解析章节目录
boolean parseIndexContentResult = crawlParser.parseBookIndexAndContent(bookId, book, ruleBean,
boolean parseIndexContentResult = crawlParser.parseBookIndexAndContent(bookId, book, ruleBean, sourceId,
new HashMap<>(0), chapter -> {
bookService.saveBookAndIndexAndContent(book, chapter.getBookIndexList(),
chapter.getBookContentList());
});
}, task);
parseResult.set(parseIndexContentResult);
} else {
@@ -385,4 +427,5 @@ public class CrawlServiceImpl implements CrawlService {
.render(RenderingStrategies.MYBATIS3);
return crawlSourceMapper.selectMany(render);
}
}

View File

@@ -25,13 +25,9 @@ public class CrawlHttpClient {
private static final ThreadLocal<Integer> RETRY_COUNT = new ThreadLocal<>();
public String get(String url, String charset) {
public String get(String url, String charset) throws InterruptedException {
if (Objects.nonNull(intervalMin) && Objects.nonNull(intervalMax) && intervalMax > intervalMin) {
try {
Thread.sleep(random.nextInt(intervalMax - intervalMin + 1) + intervalMin);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
Thread.sleep(random.nextInt(intervalMax - intervalMin + 1) + intervalMin);
}
String body = HttpUtil.getByHttpClientWithChrome(url, charset);
if (Objects.isNull(body) || body.length() < Constants.INVALID_HTML_LENGTH) {
@@ -41,7 +37,7 @@ public class CrawlHttpClient {
return body;
}
private String processErrorHttpResult(String url, String charset) {
private String processErrorHttpResult(String url, String charset) throws InterruptedException{
Integer count = RETRY_COUNT.get();
if (count == null) {
count = 0;

View File

@@ -20,7 +20,7 @@ public class CrawlSourceVO extends CrawlSource{
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm")
private Date updateTime;
private Long chapterCount;
@Override
public String toString() {

View File

@@ -110,7 +110,7 @@
} else if (data.code == 1001) {
//未登录
location.href = '/user/login.html?originUrl=' + decodeURIComponent(location.href);
location.href = '/user/login.html?originUrl=' + encodeURIComponent(location.href);
} else {
layer.alert(data.msg);

View File

@@ -48,6 +48,9 @@
<th class="name">
采集小说作者名
</th>
<th class="goread">
采集进度
</th>
<th class="goread">
采集次数
</th>
@@ -113,9 +116,15 @@
<script src="/javascript/header.js" type="text/javascript"></script>
<script src="/javascript/user.js" type="text/javascript"></script>
<script language="javascript" type="text/javascript">
search(1, 10);
let curr = 1;
let limit = 10;
function search(curr, limit) {
search();
setInterval(function(){
search();
}, 10000);
function search() {
$.ajax({
type: "get",
@@ -140,10 +149,13 @@
" " + crawlSource.authorName + "\n" +
" </td>\n" +
" <td class=\"goread\">\n" +
" " + crawlSource.crawlChapters + "\n" + "" +
" </td>\n" +
" <td class=\"goread\">\n" +
" " + crawlSource.excCount + "\n" +
" </td>\n" +
" <td class=\"goread\">\n" +
" " + (crawlSource.taskStatus == 0 ? '采集失败' : (crawlSource.taskStatus == 1 ? '采集成功' : (crawlSource.excCount > 0 ? '采集中' : '排队中'))) + "\n" +
" " + (crawlSource.taskStatus == 0 ? '采集失败' : (crawlSource.taskStatus == 1 ? '采集成功' : (crawlSource.taskStatus == 3 || crawlSource.excCount > 0 ? '采集中' : '排队中'))) + "\n" +
" </td>\n" +
" <td class=\"name\" valsc=\"291|2037554|1\">"
+ crawlSource.createTime + "</td>\n" +
@@ -171,7 +183,9 @@
//首次不执行
if (!first) {
search(obj.curr, obj.limit);
curr = obj.curr;
limit = obj.limit;
search();
} else {
}

View File

@@ -118,6 +118,9 @@
示例:<b>&lt;/p&gt;</b>
<li><input type="text" id="descEnd" class="s_input icon_key" placeholder="小说简介结束截取字符串">
</li>
示例:<b>&lt;span\s+class="allshow"&gt;([^/]+)&lt;/span&gt;</b>
<li><textarea id="filterDesc"
placeholder="过滤简介多个内容换行" rows="5" cols="52"></textarea></li>
示例:<b>更新:(\d+-\d+-\d+\s\d+:\d+:\d+)&lt;/a&gt;</b>
<li><input type="text" id="upadateTimePatten" class="s_input icon_key"
placeholder="小说更新时间的正则表达式"></li>
@@ -338,6 +341,9 @@
crawlRule.descEnd = descEnd;
var filterDesc = $("#filterDesc").val();
crawlRule.filterDesc = filterDesc;
var upadateTimePatten = $("#upadateTimePatten").val();
if (upadateTimePatten.length > 0) {

View File

@@ -43,7 +43,7 @@
<th class="style">
序号
</th>
<th class="chapter">
<th class="name">
爬虫源
</th>
<th class="name">
@@ -52,6 +52,9 @@
<th class="name">
更新时间
</th>
<th class="goread">
采集数量
</th>
<th class="goread">
状态
</th>
@@ -111,11 +114,17 @@
<script src="/javascript/header.js" type="text/javascript"></script>
<script src="/javascript/user.js" type="text/javascript"></script>
<script language="javascript" type="text/javascript">
search(1, 10);
let curr = 1;
let limit = 10;
search();
setInterval(function(){
search();
}, 10000);
var pageCrawlSourceList = null;
function search(curr, limit) {
function search() {
$.ajax({
type: "get",
@@ -134,13 +143,15 @@
" <td class=\"style bookclass\">\n" +
" [" + (i + 1) + "]\n" +
" </td>\n" +
" <td class=\"chapter\">\n" +
" <td class=\"name\">\n" +
" " + crawlSource.sourceName + "</td>\n" +
" <td class=\"name\" valsc=\"291|2037554|1\">"
+ crawlSource.createTime + "</td>\n" +
" <td class=\"name\">\n" +
" " + crawlSource.updateTime + "\n" +
" </td>\n" +
" <td class=\"goread\">\n" +
" " + crawlSource.chapterCount + "章</td>\n" +
" <td class=\"goread\" id='sourceStatus" + crawlSource.id + "'>" + (crawlSource.sourceStatus == 0 ? '停止运行' : '正在运行') +
" </td>\n" +
@@ -169,7 +180,9 @@
//首次不执行
if (!first) {
search(obj.curr, obj.limit);
curr = obj.curr;
limit = obj.limit;
search();
} else {
}
@@ -182,7 +195,7 @@
} else if (data.code == 1001) {
//未登录
location.href = '/user/login.html?originUrl=' + decodeURIComponent(location.href);
location.href = '/user/login.html?originUrl=' + encodeURIComponent(location.href);
} else {
layer.alert(data.msg);
@@ -216,17 +229,17 @@
if (status == 0) {
//开启
$("#sourceStatus" + sourceId).html("正在运行");
$("#opt" + sourceId).html("<a href='javascript:openOrStopCrawl(" + sourceId + "," + 1 + ")'>关闭</a>");
$("#opt" + sourceId).html("<a href='javascript:openOrStopCrawl(" + sourceId + "," + 1 + ")'>关闭 </a>"+"<a href='javascript:updateCrawlSource(" + sourceId + ")'>修改 </a>");
} else {
//关闭
$("#sourceStatus" + sourceId).html("停止运行");
$("#opt" + sourceId).html("<a href='javascript:openOrStopCrawl(" + sourceId + "," + 0 + ")'>开启</a>");
$("#opt" + sourceId).html("<a href='javascript:openOrStopCrawl(" + sourceId + "," + 0 + ")'>开启 </a>"+"<a href='javascript:updateCrawlSource(" + sourceId + ")'>修改 </a>");
}
} else if (data.code == 1001) {
//未登录
location.href = '/user/login.html?originUrl=' + decodeURIComponent(location.href);
location.href = '/user/login.html?originUrl=' + encodeURIComponent(location.href);
} else {
layer.alert(data.msg);

View File

@@ -119,6 +119,9 @@
示例:<b>&lt;/p&gt;</b>
<li><input type="text" id="descEnd" class="s_input icon_key" placeholder="小说简介结束截取字符串">
</li>
示例:<b>&lt;span\s+class="allshow"&gt;([^/]+)&lt;/span&gt;</b>
<li><textarea id="filterDesc"
placeholder="过滤简介多个内容换行" rows="5" cols="52"></textarea></li>
示例:<b>更新:(\d+-\d+-\d+\s\d+:\d+:\d+)&lt;/a&gt;</b>
<li><input type="text" id="upadateTimePatten" class="s_input icon_key"
placeholder="小说更新时间的正则表达式"></li>
@@ -214,7 +217,7 @@
loadPage(data.data);
} else if (data.code == 1001) {
//未登录
location.href = '/user/login.html?originUrl=' + decodeURIComponent(location.href);
location.href = '/user/login.html?originUrl=' + encodeURIComponent(location.href);
} else {
layer.alert(data.msg);
@@ -266,6 +269,7 @@
$("#visitCountPatten").val(crawlRule.visitCountPatten);
$("#descStart").val(crawlRule.descStart);
$("#descEnd").val(crawlRule.descEnd);
$("#filterDesc").val(crawlRule.filterDesc);
$("#upadateTimePatten").val(crawlRule.upadateTimePatten);
$("#upadateTimeFormatPatten").val(crawlRule.upadateTimeFormatPatten);
$("#bookIndexUrl").val(crawlRule.bookIndexUrl);
@@ -424,6 +428,9 @@
crawlRule.descEnd = descEnd;
var filterDesc = $("#filterDesc").val();
crawlRule.filterDesc = filterDesc;
var upadateTimePatten = $("#upadateTimePatten").val();
if (upadateTimePatten.length > 0) {

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>novel</artifactId>
<groupId>com.java2nb</groupId>
<version>5.1.1</version>
<version>5.2.5</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -59,7 +59,13 @@
<!-- AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
@@ -90,6 +96,16 @@
<include name="application-website.yml"/>
</fileset>
</copy>
<copy todir="${basedir}/../templates/green/html" overwrite="true">
<fileset dir="${basedir}/src/main/resources/templates">
<include name="**/*.*"/>
</fileset>
</copy>
<copy todir="${basedir}/../templates/green/static" overwrite="true">
<fileset dir="${basedir}/src/main/resources/static">
<include name="**/*.*"/>
</fileset>
</copy>
<copy todir="${project.build.directory}/build/templates" overwrite="true">
<fileset dir="${basedir}/../templates">
<include name="**/*.*"/>

View File

@@ -41,13 +41,13 @@ http:
# 是否开启 HTTP 代理true-开启false-不开启
enabled: false
# 代理 IP
ip: us.swiftproxy.net
ip: proxy.bestproxy.com
# 代理端口号
port: 7878
port: 2312
# 代理用户名
username: swiftproxy_u
username: bestproxy_u
# 代理密码
password: swiftproxy_p
password: bestproxy_p
--- #--------------------- Spring AI 配置----------------------

View File

@@ -110,20 +110,6 @@ public class AuthorController extends BaseController {
return RestResult.ok();
}
/**
* 更新章节名
*/
@PostMapping("updateIndexName")
public RestResult<Void> updateIndexName(Long indexId, String indexName, HttpServletRequest request) {
Author author = checkAuthor(request);
//更新章节名
bookService.updateIndexName(indexId, indexName, author.getId());
return RestResult.ok();
}
/**
* 发布章节内容

View File

@@ -2,22 +2,20 @@ package com.java2nb.novel.controller;
import com.java2nb.novel.core.bean.UserDetails;
import com.java2nb.novel.core.enums.ResponseStatus;
import com.java2nb.novel.entity.Book;
import com.java2nb.novel.entity.BookCategory;
import com.java2nb.novel.entity.BookComment;
import com.java2nb.novel.entity.BookIndex;
import com.java2nb.novel.core.utils.IpUtil;
import com.java2nb.novel.entity.*;
import com.java2nb.novel.service.BookContentService;
import com.java2nb.novel.service.BookService;
import com.java2nb.novel.vo.BookCommentVO;
import com.java2nb.novel.vo.BookSettingVO;
import com.java2nb.novel.vo.BookSpVO;
import com.java2nb.novel.vo.BookVO;
import com.java2nb.novel.service.IpLocationService;
import com.java2nb.novel.service.LikeService;
import com.java2nb.novel.vo.*;
import io.github.xxyopen.model.page.PageBean;
import io.github.xxyopen.model.page.builder.pagehelper.PageBuilder;
import io.github.xxyopen.model.resp.RestResult;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
@@ -37,11 +35,15 @@ public class BookController extends BaseController {
private final Map<String, BookContentService> bookContentServiceMap;
private final IpLocationService ipLocationService;
private final LikeService likeService;
/**
* 查询首页小说设置列表数据
*/
@GetMapping("listBookSetting")
public RestResult<Map<Byte, List<BookSettingVO>>> listBookSetting() {
public RestResult<Map<String, List<BookSettingVO>>> listBookSetting() {
return RestResult.ok(bookService.listBookSettingVO());
}
@@ -81,7 +83,7 @@ public class BookController extends BaseController {
* 分页搜索
*/
@GetMapping("searchByPage")
public RestResult<?> searchByPage(BookSpVO bookSP, @RequestParam(value = "curr", defaultValue = "1") int page,
public RestResult<?> searchByPage(@Validated BookSpVO bookSP, @RequestParam(value = "curr", defaultValue = "1") int page,
@RequestParam(value = "limit", defaultValue = "20") int pageSize) {
return RestResult.ok(bookService.searchByPage(bookSP, page, pageSize));
}
@@ -149,6 +151,16 @@ public class BookController extends BaseController {
return RestResult.ok(bookService.listCommentByPage(null, bookId, page, pageSize));
}
/**
* 分页查询评论回复列表
*/
@GetMapping("listCommentReplyByPage")
public RestResult<PageBean<BookCommentReplyVO>> listCommentReplyByPage(@RequestParam("commentId") Long commentId,
@RequestParam(value = "curr", defaultValue = "1") int page,
@RequestParam(value = "limit", defaultValue = "5") int pageSize) {
return RestResult.ok(bookService.listCommentReplyByPage(null, commentId, page, pageSize));
}
/**
* 新增评价
*/
@@ -158,10 +170,73 @@ public class BookController extends BaseController {
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
comment.setLocation(ipLocationService.getLocation(IpUtil.getRealIp(request)));
bookService.addBookComment(userDetails.getId(), comment);
return RestResult.ok();
}
/**
* 评价点赞/取消点赞
*/
@PostMapping("toggleCommentLike")
public RestResult<?> toggleCommentLike(Long commentId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleCommentLike(commentId, userDetails.getId()));
}
/**
* 评价点踩/取消点踩
*/
@PostMapping("toggleCommentUnLike")
public RestResult<?> toggleCommentUnLike(Long commentId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleCommentUnLike(commentId, userDetails.getId()));
}
/**
* 新增回复
*/
@PostMapping("addCommentReply")
public RestResult<?> addCommentReply(BookCommentReply commentReply, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
commentReply.setLocation(ipLocationService.getLocation(IpUtil.getRealIp(request)));
bookService.addBookCommentReply(userDetails.getId(), commentReply);
return RestResult.ok();
}
/**
* 回复点赞/取消点赞
*/
@PostMapping("toggleReplyLike")
public RestResult<?> toggleReplyLike(Long replyId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleReplyLike(replyId, userDetails.getId()));
}
/**
* 回复点赞/取消点赞
*/
@PostMapping("toggleReplyUnLike")
public RestResult<?> toggleReplyUnLike(Long replyId, HttpServletRequest request) {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
return RestResult.fail(ResponseStatus.NO_LOGIN);
}
return RestResult.ok(likeService.toggleReplyUnLike(replyId, userDetails.getId()));
}
/**
* 根据小说ID查询小说前十条最新更新目录集合
*/

View File

@@ -50,7 +50,7 @@ public class PayController extends BaseController {
UserDetails userDetails = getUserDetails(request);
if (userDetails == null) {
//未登录,跳转到登页面
//未登录,跳转到登页面
httpResponse.sendRedirect("/user/login.html?originUrl=/pay/index.html");
} else {
//创建充值订单

View File

@@ -40,12 +40,12 @@ public class UserController extends BaseController {
private final BookService bookService;
/**
* 登
* 登
*/
@PostMapping("login")
public RestResult<Map<String, Object>> login(User user) {
//登
//登
UserDetails userDetails = userService.login(user);
Map<String, Object> data = new HashMap<>(1);

View File

@@ -82,7 +82,7 @@ public class PageController extends BaseController {
@RequestMapping(path = {"/", "/index", "/index.html"})
public String index(Model model) {
//加载小说首页小说基本信息线程
CompletableFuture<Map<Byte, List<BookSettingVO>>> bookCompletableFuture = CompletableFuture.supplyAsync(
CompletableFuture<Map<String, List<BookSettingVO>>> bookCompletableFuture = CompletableFuture.supplyAsync(
bookService::listBookSettingVO, threadPoolExecutor);
//加载首页新闻线程
CompletableFuture<List<News>> newsCompletableFuture = CompletableFuture.supplyAsync(newsService::listIndexNews,
@@ -313,6 +313,16 @@ public class PageController extends BaseController {
return "book/book_comment";
}
/**
* 评论回复页面
*/
@RequestMapping("/book/reply-{commentId}.html")
public String commentReplyList(@PathVariable("commentId") Long commentId, Model model) {
model.addAttribute("commentId", commentId);
model.addAttribute("commentContent", bookService.getBookComment(commentId).getCommentContent());
return "book/book_comment_reply";
}
/**
* 新闻内容页面
*/

View File

@@ -0,0 +1,55 @@
package com.java2nb.novel.core.config;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* IP 地址定位配置类
*
* @author xiongxiaoyang
* @date 2025/6/30
*/
@Slf4j
@Configuration
public class IpLocationConfig {
/**
* 使用 {@link Searcher} 实现高效的本地 IP 查询服务, 创建基于内存的 IP 地址查询对象,支持并发访问且仅需初始化一次。
*
* <p>该方法会将 ip2region.xdb 数据库文件加载到内存中,
* 并构建一个线程安全的 {@link Searcher} 实例,可用于高效、并发的 IP 地址定位查询。</p>
*
* <p>{@link Searcher} 实例是线程安全的,可以作为全局单例在整个应用中跨线程使用。</p>
*
* <p>通过配置 destroyMethod="close",确保在 Spring 容器关闭时自动释放底层资源。</p>
*/
@Bean(destroyMethod = "close")
public Searcher searcher() throws IOException {
// 1、从 classpath 加载整个 xdb 到内存。
try (InputStream inputStream = new ClassPathResource("ip2region.xdb").getInputStream()) {
File tempDbFile = File.createTempFile("ip2region", ".xdb");
try (FileOutputStream outputStream = new FileOutputStream(tempDbFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
// 确保程序退出时删除临时文件
tempDbFile.deleteOnExit();
byte[] cBuff = Searcher.loadContentFromFile(tempDbFile.getPath());
// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
return Searcher.newWithBuffer(cBuff);
}
}
}

View File

@@ -0,0 +1,23 @@
package com.java2nb.novel.core.serialize;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.java2nb.novel.core.utils.DateUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.Date;
public class TimeAgoFormatSerialize extends JsonSerializer<Date> {
@Override
public void serialize(Date s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (s != null) {
jsonGenerator.writeString(DateUtil.formatTimeAgo(s));
}
}
}

View File

@@ -12,4 +12,5 @@ public interface FrontBookCommentMapper extends BookCommentMapper {
List<BookCommentVO> listCommentByPage(@Param("userId") Long userId, @Param("bookId") Long bookId);
void addReplyCount(@Param("commentId") Long commentId);
}

View File

@@ -0,0 +1,16 @@
package com.java2nb.novel.mapper;
import com.java2nb.novel.vo.BookCommentReplyVO;
import com.java2nb.novel.vo.BookCommentVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author Administrator
*/
public interface FrontBookCommentReplyMapper extends BookCommentReplyMapper {
List<BookCommentReplyVO> listCommentReplyByPage(@Param("userId") Long userId, @Param("commentId") Long commentId);
}

View File

@@ -1,12 +1,7 @@
package com.java2nb.novel.mapper;
import com.java2nb.novel.entity.Book;
import com.java2nb.novel.vo.BookSpVO;
import com.java2nb.novel.vo.BookVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author Administrator
*/

View File

@@ -1,12 +1,9 @@
package com.java2nb.novel.service;
import com.java2nb.novel.vo.*;
import io.github.xxyopen.model.page.PageBean;
import com.java2nb.novel.entity.*;
import com.java2nb.novel.vo.BookCommentVO;
import com.java2nb.novel.vo.BookSettingVO;
import com.java2nb.novel.vo.BookSpVO;
import com.java2nb.novel.vo.BookVO;
import java.util.Date;
import java.util.List;
@@ -19,9 +16,10 @@ public interface BookService {
/**
* 查询首页小说设置列表数据
*
* @return
* */
Map<Byte, List<BookSettingVO>> listBookSettingVO();
*/
Map<String, List<BookSettingVO>> listBookSettingVO();
/**
* 查询首页点击榜单数据
@@ -295,4 +293,15 @@ public interface BookService {
* 查询AI生成图片
*/
String queryAiGenPic(Long bookId);
/**
* 新增回复
* @param userId 用户ID
* @param commentReply 回复内容
* */
void addBookCommentReply(Long userId, BookCommentReply commentReply);
PageBean<BookCommentReplyVO> listCommentReplyByPage(Long userId, Long commentId, int page, int pageSize);
BookComment getBookComment(Long commentId);
}

View File

@@ -0,0 +1,24 @@
package com.java2nb.novel.service;
/**
* IP 地址定位服务类
*
* <p>该服务用于实现 IP 地址到地理位置的查询功能,
* 包括国家、省份、城市等信息。</p>
*
* <p>此类设计为 Spring 管理的 Service Bean支持在 Controller 或其他 Service 中注入使用。</p>
*
* @author xiongxiaoyang
* @date 2025/6/30
*/
public interface IpLocationService {
/**
* 根据 IP 地址查询地理位置信息
*
* @param ip 待查询的 IP 地址IPv4
* @return 如果是中国 IP返回省份否则返回国家
*/
String getLocation(String ip);
}

View File

@@ -0,0 +1,69 @@
package com.java2nb.novel.service;
/**
* @author 11797
*/
public interface LikeService {
/**
* 评论点赞或取消点赞
* @param commentId 被点赞的评论ID
* @param userId 用户ID
* @return 返回点赞数量
*/
public Long toggleCommentLike(Long commentId, Long userId);
/**
* 评论点踩或取消点踩
* @param commentId 被点踩的评论ID
* @param userId 用户ID
* @return 返回点踩数量
*/
public Long toggleCommentUnLike(Long commentId, Long userId);
/**
* 获取评论的点赞数量
* @param commentId 评论ID
* @return 点赞数
*/
public Long getCommentLikesCount(Long commentId);
/**
* 获取评论的点踩赞数量
* @param commentId 评论ID
* @return 点踩数
*/
public Long getCommentUnLikesCount(Long commentId);
/**
* 回复点赞或取消点赞
* @param replyId 被点赞的回复ID
* @param userId 用户ID
* @return 返回点赞数量
*/
public Long toggleReplyLike(Long replyId, Long userId);
/**
* 回复点踩或取消点踩
* @param replyId 被点踩的回复ID
* @param userId 用户ID
* @return 返回点踩数量
*/
public Long toggleReplyUnLike(Long replyId, Long userId);
/**
* 获取回复的点赞数量
* @param replyId 回复ID
* @return 点赞数
*/
public Long getReplyLikesCount(Long replyId);
/**
* 获取回复的点踩数量
* @param replyId 回复ID
* @return 点踩数
*/
public Long getReplyUnLikesCount(Long replyId);
}

View File

@@ -26,8 +26,8 @@ public interface UserService {
UserDetails register(User user);
/**
* 用户登
* @param user 用户登信息类
* 用户登
* @param user 用户登信息类
* @return jwt载体信息类
* */
UserDetails login(User user);

View File

@@ -1,6 +1,5 @@
package com.java2nb.novel.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.pagehelper.PageHelper;
import com.java2nb.novel.core.cache.CacheKey;
import com.java2nb.novel.core.cache.CacheService;
@@ -15,10 +14,8 @@ import com.java2nb.novel.mapper.*;
import com.java2nb.novel.service.AuthorService;
import com.java2nb.novel.service.BookService;
import com.java2nb.novel.service.FileService;
import com.java2nb.novel.vo.BookCommentVO;
import com.java2nb.novel.vo.BookSettingVO;
import com.java2nb.novel.vo.BookSpVO;
import com.java2nb.novel.vo.BookVO;
import com.java2nb.novel.service.LikeService;
import com.java2nb.novel.vo.*;
import io.github.xxyopen.model.page.PageBean;
import io.github.xxyopen.model.page.builder.pagehelper.PageBuilder;
import io.github.xxyopen.util.IdWorker;
@@ -27,7 +24,6 @@ import io.github.xxyopen.web.util.BeanUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.DateUtils;
import org.mybatis.dynamic.sql.SortSpecification;
import org.mybatis.dynamic.sql.render.RenderingStrategies;
@@ -50,7 +46,6 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
import static com.java2nb.novel.mapper.BookCategoryDynamicSqlSupport.bookCategory;
import static com.java2nb.novel.mapper.BookCategoryDynamicSqlSupport.sort;
import static com.java2nb.novel.mapper.BookCommentDynamicSqlSupport.bookComment;
import static com.java2nb.novel.mapper.BookContentDynamicSqlSupport.bookContent;
import static com.java2nb.novel.mapper.BookContentDynamicSqlSupport.content;
@@ -87,6 +82,8 @@ public class BookServiceImpl implements BookService {
private final FrontBookCommentMapper bookCommentMapper;
private final FrontBookCommentReplyMapper bookCommentReplyMapper;
private final BookAuthorMapper bookAuthorMapper;
private final CacheService cacheService;
@@ -95,6 +92,8 @@ public class BookServiceImpl implements BookService {
private final FileService fileService;
private final LikeService likeService;
private final BookPriceProperties bookPriceConfig;
private final OpenAiImageModel openAiImageModel;
@@ -106,19 +105,19 @@ public class BookServiceImpl implements BookService {
@SneakyThrows
@Override
public Map<Byte, List<BookSettingVO>> listBookSettingVO() {
String result = cacheService.get(CacheKey.INDEX_BOOK_SETTINGS_KEY);
if (result == null || result.length() < Constants.OBJECT_JSON_CACHE_EXIST_LENGTH) {
List<BookSettingVO> list = bookSettingMapper.listVO();
if (list.size() == 0) {
public Map<String, List<BookSettingVO>> listBookSettingVO() {
List<BookSettingVO> list = cacheService.getList(CacheKey.INDEX_BOOK_SETTINGS_KEY, BookSettingVO.class);
if (list == null || list.isEmpty()) {
list = bookSettingMapper.listVO();
if (list.isEmpty()) {
//如果首页小说没有被设置,则初始化首页小说设置
list = initIndexBookSetting();
}
result = new ObjectMapper().writeValueAsString(
list.stream().collect(Collectors.groupingBy(BookSettingVO::getType)));
cacheService.set(CacheKey.INDEX_BOOK_SETTINGS_KEY, result, 3600 * 24);
cacheService.setObject(CacheKey.INDEX_BOOK_SETTINGS_KEY, list, 3600 * 24);
}
return new ObjectMapper().readValue(result, Map.class);
return list.stream().collect(
Collectors.groupingBy(book -> book.getType().toString())
);
}
@@ -168,11 +167,10 @@ public class BookServiceImpl implements BookService {
return new ArrayList<>(0);
}
@Override
public List<Book> listClickRank() {
List<Book> result = (List<Book>) cacheService.getObject(CacheKey.INDEX_CLICK_BANK_BOOK_KEY);
if (result == null || result.size() == 0) {
List<Book> result = cacheService.getList(CacheKey.INDEX_CLICK_BANK_BOOK_KEY, Book.class);
if (result == null || result.isEmpty()) {
result = listRank((byte) 0, 10);
cacheService.setObject(CacheKey.INDEX_CLICK_BANK_BOOK_KEY, result, 5000);
}
@@ -181,8 +179,8 @@ public class BookServiceImpl implements BookService {
@Override
public List<Book> listNewRank() {
List<Book> result = (List<Book>) cacheService.getObject(CacheKey.INDEX_NEW_BOOK_KEY);
if (result == null || result.size() == 0) {
List<Book> result = cacheService.getList(CacheKey.INDEX_NEW_BOOK_KEY, Book.class);
if (result == null || result.isEmpty()) {
result = listRank((byte) 1, 10);
cacheService.setObject(CacheKey.INDEX_NEW_BOOK_KEY, result, 3600);
}
@@ -191,8 +189,8 @@ public class BookServiceImpl implements BookService {
@Override
public List<BookVO> listUpdateRank() {
List<BookVO> result = (List<BookVO>) cacheService.getObject(CacheKey.INDEX_UPDATE_BOOK_KEY);
if (result == null || result.size() == 0) {
List<BookVO> result = cacheService.getList(CacheKey.INDEX_UPDATE_BOOK_KEY, BookVO.class);
if (result == null || result.isEmpty()) {
List<Book> bookPOList = listRank((byte) 2, 23);
result = BeanUtil.copyList(bookPOList, BookVO.class);
cacheService.setObject(CacheKey.INDEX_UPDATE_BOOK_KEY, result, 60 * 10);
@@ -391,7 +389,12 @@ public class BookServiceImpl implements BookService {
@Override
public PageBean<BookCommentVO> listCommentByPage(Long userId, Long bookId, int page, int pageSize) {
PageHelper.startPage(page, pageSize);
return PageBuilder.build(bookCommentMapper.listCommentByPage(userId, bookId));
PageBean<BookCommentVO> pageBean = PageBuilder.build(bookCommentMapper.listCommentByPage(userId, bookId));
for (BookCommentVO bookCommentVO : pageBean.getList()) {
bookCommentVO.setLikesCount(likeService.getCommentLikesCount(bookCommentVO.getId()));
bookCommentVO.setUnLikesCount(likeService.getCommentUnLikesCount(bookCommentVO.getId()));
}
return pageBean;
}
@Transactional(rollbackFor = Exception.class)
@@ -888,5 +891,33 @@ public class BookServiceImpl implements BookService {
return cacheService.get(CacheKey.AI_GEN_PIC + bookId);
}
@Transactional(rollbackFor = Exception.class)
@Override
public void addBookCommentReply(Long userId, BookCommentReply commentReply) {
//增加回复
commentReply.setCreateUserId(userId);
commentReply.setCreateTime(new Date());
bookCommentReplyMapper.insertSelective(commentReply);
//增加评论回复数
bookCommentMapper.addReplyCount(commentReply.getCommentId());
}
@Override
public PageBean<BookCommentReplyVO> listCommentReplyByPage(Long userId, Long commentId, int page, int pageSize) {
PageHelper.startPage(page, pageSize);
PageBean<BookCommentReplyVO> pageBean = PageBuilder.build(
bookCommentReplyMapper.listCommentReplyByPage(userId, commentId));
pageBean.getList().forEach(commentReply -> {
commentReply.setLikesCount(likeService.getReplyLikesCount(commentReply.getId()));
commentReply.setUnLikesCount(likeService.getReplyUnLikesCount(commentReply.getId()));
});
return pageBean;
}
@Override
public BookComment getBookComment(Long commentId) {
return bookCommentMapper.selectByPrimaryKey(commentId).orElse(null);
}
}

View File

@@ -1,6 +1,5 @@
package com.java2nb.novel.service.impl;
import io.github.xxyopen.web.util.BeanUtil;
import com.java2nb.novel.service.FriendLinkService;
import com.java2nb.novel.core.cache.CacheKey;
import com.java2nb.novel.core.cache.CacheService;
@@ -31,16 +30,16 @@ public class FriendLinkServiceImpl implements FriendLinkService {
@Override
public List<FriendLink> listIndexLink() {
List<FriendLink> result = (List<FriendLink>) cacheService.getObject(CacheKey.INDEX_LINK_KEY);
if(result == null || result.size() == 0) {
SelectStatementProvider selectStatement = select(linkName,linkUrl)
.from(friendLink)
.where(isOpen,isEqualTo((byte)1))
.orderBy(sort)
.build()
.render(RenderingStrategies.MYBATIS3);
result = friendLinkMapper.selectMany(selectStatement);
cacheService.setObject(CacheKey.INDEX_LINK_KEY,result,60 * 60 * 24);
List<FriendLink> result = cacheService.getList(CacheKey.INDEX_LINK_KEY, FriendLink.class);
if (result == null || result.isEmpty()) {
SelectStatementProvider selectStatement = select(linkName, linkUrl)
.from(friendLink)
.where(isOpen, isEqualTo((byte) 1))
.orderBy(sort)
.build()
.render(RenderingStrategies.MYBATIS3);
result = friendLinkMapper.selectMany(selectStatement);
cacheService.setObject(CacheKey.INDEX_LINK_KEY, result, 60 * 60 * 24);
}
return result;
}

View File

@@ -0,0 +1,56 @@
package com.java2nb.novel.service.impl;
import com.java2nb.novel.core.utils.IpUtil;
import com.java2nb.novel.service.IpLocationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* IpLocationService 实现类
*
* @author xiongxiaoyang
* @date 2025/6/30
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class IpLocationServiceImpl implements IpLocationService {
private final Searcher searcher;
@Override
public String getLocation(String ip) {
try {
// 示例返回:"中国|0|湖北省|武汉市|电信"
String region = searcher.search(ip);
String[] regions = region.split("\\|");
if (regions.length > 0) {
// 国家
String country = regions[0];
if ("0".equals(country)) {
// 内网IP直接获取本机公网IP
String publicIp = IpUtil.getPublicIP();
log.info("内网IP{}本机公网IP{}", ip, publicIp);
if (StringUtils.hasText(publicIp)) {
return getLocation(publicIp);
}
} else if ("中国".equals(country)) {
// 是中国,则返回省份(第三个字段)
String province = regions.length > 2 ? regions[2] : "未知地区";
// 去掉最后一个“省”字
return province.endsWith("") ? province.substring(0, province.length() - 1) : province;
} else {
// 非中国,返回国家名
return country;
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return "未知地区";
}
}

View File

@@ -0,0 +1,94 @@
package com.java2nb.novel.service.impl;
import com.java2nb.novel.service.LikeService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;
import org.springframework.stereotype.Service;
import java.util.Collections;
/**
* @author xiongxiaoyang
* @date 2025/7/12
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class LikeServiceImpl implements LikeService {
private final StringRedisTemplate redisTemplate;
private DefaultRedisScript<Long> toggleLikeScript;
private static final String COMMENT_LIKE_KEY_PREFIX = "like:comment:";
private static final String COMMENT_REPLY_LIKE_KEY_PREFIX = "like:comment:reply:";
private static final String COMMENT_UN_LIKE_KEY_PREFIX = "unlike:comment:";
private static final String COMMENT_REPLY_UN_LIKE_KEY_PREFIX = "unlike:comment:reply:";
@PostConstruct
public void init() {
// Lua 脚本保证原子性操作
String script = """
local key = KEYS[1]
local userId = ARGV[1]
local isLiked = redis.call('SISMEMBER', key, userId)
if isLiked == 1 then
redis.call('SREM', key, userId)
else
redis.call('SADD', key, userId)
end
return redis.call('SCARD', key)
""";
toggleLikeScript = new DefaultRedisScript<>();
toggleLikeScript.setScriptSource(new StaticScriptSource(script));
toggleLikeScript.setResultType(Long.class);
}
public Long toggleCommentLike(Long commentId, Long userId) {
return executeToggle(COMMENT_LIKE_KEY_PREFIX + commentId, userId);
}
@Override
public Long toggleCommentUnLike(Long commentId, Long userId) {
return executeToggle(COMMENT_UN_LIKE_KEY_PREFIX + commentId, userId);
}
public Long getCommentLikesCount(Long commentId) {
return redisTemplate.opsForSet().size(COMMENT_LIKE_KEY_PREFIX + commentId);
}
@Override
public Long getCommentUnLikesCount(Long commentId) {
return redisTemplate.opsForSet().size(COMMENT_UN_LIKE_KEY_PREFIX + commentId);
}
public Long toggleReplyLike(Long replyId, Long userId) {
return executeToggle(COMMENT_REPLY_LIKE_KEY_PREFIX + replyId, userId);
}
@Override
public Long toggleReplyUnLike(Long replyId, Long userId) {
return executeToggle(COMMENT_REPLY_UN_LIKE_KEY_PREFIX + replyId, userId);
}
public Long getReplyLikesCount(Long replyId) {
return redisTemplate.opsForSet().size(COMMENT_REPLY_LIKE_KEY_PREFIX + replyId);
}
@Override
public Long getReplyUnLikesCount(Long replyId) {
return redisTemplate.opsForSet().size(COMMENT_REPLY_UN_LIKE_KEY_PREFIX + replyId);
}
private Long executeToggle(String key, Long userId) {
return redisTemplate.execute(toggleLikeScript, Collections.singletonList(key), String.valueOf(userId));
}
}

View File

@@ -36,16 +36,16 @@ public class NewsServiceImpl implements NewsService {
@Override
public List<News> listIndexNews() {
List<News> result = (List<News>) cacheService.getObject(CacheKey.INDEX_NEWS_KEY);
if(result == null || result.size() == 0) {
SelectStatementProvider selectStatement = select(id, catName, catId, title,createTime)
.from(news)
.orderBy(createTime.descending())
.limit(2)
.build()
.render(RenderingStrategies.MYBATIS3);
List<News> result = cacheService.getList(CacheKey.INDEX_NEWS_KEY, News.class);
if (result == null || result.isEmpty()) {
SelectStatementProvider selectStatement = select(id, catName, catId, title, createTime)
.from(news)
.orderBy(createTime.descending())
.limit(2)
.build()
.render(RenderingStrategies.MYBATIS3);
result = newsMapper.selectMany(selectStatement);
cacheService.setObject(CacheKey.INDEX_NEWS_KEY,result,60 * 60 * 12);
cacheService.setObject(CacheKey.INDEX_NEWS_KEY, result, 60 * 60 * 12);
}
return result;
}
@@ -53,25 +53,25 @@ public class NewsServiceImpl implements NewsService {
@Override
public News queryNewsInfo(Long newsId) {
SelectStatementProvider selectStatement = select(news.allColumns())
.from(news)
.where(id,isEqualTo(newsId))
.build()
.render(RenderingStrategies.MYBATIS3);
.from(news)
.where(id, isEqualTo(newsId))
.build()
.render(RenderingStrategies.MYBATIS3);
return newsMapper.selectMany(selectStatement).get(0);
}
@Override
public PageBean<News> listByPage(int page, int pageSize) {
PageHelper.startPage(page,pageSize);
SelectStatementProvider selectStatement = select(id, catName, catId, title,createTime)
.from(news)
.orderBy(createTime.descending())
.build()
.render(RenderingStrategies.MYBATIS3);
PageHelper.startPage(page, pageSize);
SelectStatementProvider selectStatement = select(id, catName, catId, title, createTime)
.from(news)
.orderBy(createTime.descending())
.build()
.render(RenderingStrategies.MYBATIS3);
List<News> news = newsMapper.selectMany(selectStatement);
PageBean<News> pageBean = PageBuilder.build(news);
pageBean.setList(BeanUtil.copyList(news,NewsVO.class));
pageBean.setList(BeanUtil.copyList(news, NewsVO.class));
return pageBean;
}

View File

@@ -0,0 +1,33 @@
package com.java2nb.novel.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.java2nb.novel.core.serialize.CommentUserNameSerialize;
import com.java2nb.novel.core.serialize.TimeAgoFormatSerialize;
import com.java2nb.novel.entity.BookCommentReply;
import lombok.Data;
import java.util.Date;
/**
* @author 11797
*/
@Data
public class BookCommentReplyVO extends BookCommentReply {
@JsonSerialize(using = CommentUserNameSerialize.class)
private String createUserName;
private String createUserPhoto;
@JsonSerialize(using = TimeAgoFormatSerialize.class)
private Date createTime;
private Long likesCount;
private Long unLikesCount;
@Override
public String toString() {
return super.toString();
}
}

View File

@@ -1,8 +1,9 @@
package com.java2nb.novel.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.java2nb.novel.core.serialize.CommentUserNameSerialize;
import com.java2nb.novel.core.serialize.TimeAgoFormatSerialize;
import com.java2nb.novel.core.utils.DateUtil;
import com.java2nb.novel.entity.BookComment;
import lombok.Data;
@@ -19,9 +20,17 @@ public class BookCommentVO extends BookComment {
private String createUserPhoto;
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@JsonSerialize(using = TimeAgoFormatSerialize.class)
private Date createTime;
private Long likesCount;
private Long unLikesCount;
public String getCreateTimeFormat() {
return DateUtil.formatTimeAgo(getCreateTime());
}
@Override
public String toString() {
return super.toString();

View File

@@ -1,5 +1,6 @@
package com.java2nb.novel.vo;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.util.Date;
@@ -29,9 +30,8 @@ public class BookSpVO {
private Long updatePeriod;
@Pattern(regexp = "^(last_index_update_time|word_count|visit_count)$")
private String sort;
}

View File

@@ -23,7 +23,7 @@ xss:
# 排除链接多个用逗号分隔
excludes: /system/notice/*
# 匹配链接 多个用逗号分隔
urlPatterns: /book/addBookComment,/user/addFeedBack,/author/addBook,/author/addBookContent,/author/updateBookContent,/author/register.html
urlPatterns: /book/addCommentReply,/book/addBookComment,/user/addFeedBack,/author/addBook,/author/addBookContent,/author/updateBookContent,/author/register.html
author:
@@ -59,6 +59,6 @@ spring:
base-url: https://api.siliconflow.cn
chat:
options:
model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B
model: deepseek-ai/DeepSeek-R1-0528-Qwen3-8B

Some files were not shown because too many files have changed in this diff Show More