Ver código fonte

1217 整合redis

Qing 1 ano atrás
pai
commit
57eb0c6ee3
26 arquivos alterados com 2035 adições e 1 exclusões
  1. 27 0
      novel-demo/pom.xml
  2. 201 0
      novel-demo/src/main/java/com/sf/common/constant/DatabaseConsts.java
  3. 152 0
      novel-demo/src/main/java/com/sf/common/constant/ErrorCodeEnum.java
  4. 45 0
      novel-demo/src/main/java/com/sf/common/constant/SystemConfigConsts.java
  5. 87 0
      novel-demo/src/main/java/com/sf/common/resp/RestResp.java
  6. 79 0
      novel-demo/src/main/java/com/sf/config/CacheConfig.java
  7. 57 0
      novel-demo/src/main/java/com/sf/config/CorsConfig.java
  8. 34 0
      novel-demo/src/main/java/com/sf/config/CorsProperties.java
  9. 27 0
      novel-demo/src/main/java/com/sf/constant/AmqpConsts.java
  10. 175 0
      novel-demo/src/main/java/com/sf/constant/CacheConsts.java
  11. 201 0
      novel-demo/src/main/java/com/sf/constant/DatabaseConsts.java
  12. 45 0
      novel-demo/src/main/java/com/sf/constant/SystemConfigConsts.java
  13. 63 0
      novel-demo/src/main/java/com/sf/controller/BookInfoController.java
  14. 41 0
      novel-demo/src/main/java/com/sf/dto/req/ChapterAddReqDto.java
  15. 29 0
      novel-demo/src/main/java/com/sf/dto/resp/BookCategoryRespDto.java
  16. 31 0
      novel-demo/src/main/java/com/sf/dto/resp/BookChapterAboutRespDto.java
  17. 78 0
      novel-demo/src/main/java/com/sf/dto/resp/BookChapterRespDto.java
  18. 54 0
      novel-demo/src/main/java/com/sf/dto/resp/BookCommentRespDto.java
  19. 35 0
      novel-demo/src/main/java/com/sf/dto/resp/BookContentAboutRespDto.java
  20. 122 0
      novel-demo/src/main/java/com/sf/dto/resp/BookInfoRespDto.java
  21. 84 0
      novel-demo/src/main/java/com/sf/dto/resp/BookRankRespDto.java
  22. 74 0
      novel-demo/src/main/java/com/sf/manager/cache/BookCacheManager.java
  23. 49 0
      novel-demo/src/main/java/com/sf/service/IBookInfoService.java
  24. 222 0
      novel-demo/src/main/java/com/sf/service/impl/BookInfoServiceImpl.java
  25. 1 1
      novel-demo/src/main/resources/application.properties
  26. 22 0
      novel-demo/src/main/resources/application.yml

+ 27 - 0
novel-demo/pom.xml

@@ -96,6 +96,33 @@
             <version>3.2.0</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
+            <version>2.0.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+            <version>2.0.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+            <version>2.15.3</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 201 - 0
novel-demo/src/main/java/com/sf/common/constant/DatabaseConsts.java

@@ -0,0 +1,201 @@
+package com.sf.common.constant;
+
+import lombok.Getter;
+
+/**
+ * 数据库 常量
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/12
+ */
+public class DatabaseConsts {
+
+    /**
+     * 用户信息表
+     */
+    public static class UserInfoTable {
+
+        private UserInfoTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USERNAME = "username";
+
+    }
+
+    /**
+     * 用户反馈表
+     */
+    public static class UserFeedBackTable {
+
+        private UserFeedBackTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+    }
+
+    /**
+     * 用户书架表
+     */
+    public static class UserBookshelfTable {
+
+        private UserBookshelfTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+        public static final String COLUMN_BOOK_ID = "book_id";
+
+    }
+
+    /**
+     * 作家信息表
+     */
+    public static class AuthorInfoTable {
+
+        private AuthorInfoTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+    }
+
+    /**
+     * 小说类别表
+     */
+    public static class BookCategoryTable {
+
+        private BookCategoryTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_WORK_DIRECTION = "work_direction";
+
+    }
+
+    /**
+     * 小说表
+     */
+    public static class BookTable {
+
+        private BookTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_CATEGORY_ID = "category_id";
+
+        public static final String COLUMN_BOOK_NAME = "book_name";
+
+        public static final String AUTHOR_ID = "author_id";
+
+        public static final String COLUMN_VISIT_COUNT = "visit_count";
+
+        public static final String COLUMN_WORD_COUNT = "word_count";
+
+        public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
+
+    }
+
+    /**
+     * 小说章节表
+     */
+    public static class BookChapterTable {
+
+        private BookChapterTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_BOOK_ID = "book_id";
+
+        public static final String COLUMN_CHAPTER_NUM = "chapter_num";
+
+        public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
+
+    }
+
+    /**
+     * 小说内容表
+     */
+    public static class BookContentTable {
+
+        private BookContentTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_CHAPTER_ID = "chapter_id";
+
+    }
+
+    /**
+     * 小说评论表
+     */
+    public static class BookCommentTable {
+
+        private BookCommentTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_BOOK_ID = "book_id";
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+    }
+
+    /**
+     * 新闻内容表
+     */
+    public static class NewsContentTable {
+
+        private NewsContentTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_NEWS_ID = "news_id";
+
+    }
+
+    /**
+     * 通用列枚举类
+     */
+    @Getter
+    public enum CommonColumnEnum {
+
+        ID("id"),
+        SORT("sort"),
+        CREATE_TIME("create_time"),
+        UPDATE_TIME("update_time");
+
+        private String name;
+
+        CommonColumnEnum(String name) {
+            this.name = name;
+        }
+
+    }
+
+
+    /**
+     * SQL语句枚举类
+     */
+    @Getter
+    public enum SqlEnum {
+
+        LIMIT_1("limit 1"),
+        LIMIT_2("limit 2"),
+        LIMIT_5("limit 5"),
+        LIMIT_30("limit 30"),
+        LIMIT_500("limit 500");
+
+        private String sql;
+
+        SqlEnum(String sql) {
+            this.sql = sql;
+        }
+
+    }
+
+}

+ 152 - 0
novel-demo/src/main/java/com/sf/common/constant/ErrorCodeEnum.java

@@ -0,0 +1,152 @@
+package com.sf.common.constant;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 错误码枚举类。
+ * <p>
+ * 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。 错误产生来源分为 A/B/C, A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题; B
+ * 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题; C 表示错误来源 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的
+ * 步长间距预留 100。
+ * <p>
+ * 错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码。
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/11
+ */
+@Getter
+@AllArgsConstructor
+public enum ErrorCodeEnum {
+
+    /**
+     * 正确执行后的返回
+     */
+    OK("00000", "一切 ok"),
+
+    /**
+     * 一级宏观错误码,用户端错误
+     */
+    USER_ERROR("A0001", "用户端错误"),
+
+    /**
+     * 二级宏观错误码,用户注册错误
+     */
+    USER_REGISTER_ERROR("A0100", "用户注册错误"),
+
+    /**
+     * 用户未同意隐私协议
+     */
+    USER_NO_AGREE_PRIVATE_ERROR("A0101", "用户未同意隐私协议"),
+
+    /**
+     * 注册国家或地区受限
+     */
+    USER_REGISTER_AREA_LIMIT_ERROR("A0102", "注册国家或地区受限"),
+
+    /**
+     * 用户验证码错误
+     */
+    USER_VERIFY_CODE_ERROR("A0240", "用户验证码错误"),
+
+    /**
+     * 用户名已存在
+     */
+    USER_NAME_EXIST("A0111", "用户名已存在"),
+
+    /**
+     * 用户账号不存在
+     */
+    USER_ACCOUNT_NOT_EXIST("A0201", "用户账号不存在"),
+
+    /**
+     * 用户密码错误
+     */
+    USER_PASSWORD_ERROR("A0210", "用户密码错误"),
+
+    /**
+     * 二级宏观错误码,用户请求参数错误
+     */
+    USER_REQUEST_PARAM_ERROR("A0400", "用户请求参数错误"),
+
+    /**
+     * 用户登录已过期
+     */
+    USER_LOGIN_EXPIRED("A0230", "用户登录已过期"),
+
+    /**
+     * 访问未授权
+     */
+    USER_UN_AUTH("A0301", "访问未授权"),
+
+    /**
+     * 用户请求服务异常
+     */
+    USER_REQ_EXCEPTION("A0500", "用户请求服务异常"),
+
+    /**
+     * 请求超出限制
+     */
+    USER_REQ_MANY("A0501", "请求超出限制"),
+
+    /**
+     * 用户评论异常
+     */
+    USER_COMMENT("A2000", "用户评论异常"),
+
+    /**
+     * 用户评论异常
+     */
+    USER_COMMENTED("A2001", "用户已发表评论"),
+
+    /**
+     * 作家发布异常
+     */
+    AUTHOR_PUBLISH("A3000", "作家发布异常"),
+
+    /**
+     * 小说名已存在
+     */
+    AUTHOR_BOOK_NAME_EXIST("A3001", "小说名已存在"),
+
+    /**
+     * 用户上传文件异常
+     */
+    USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"),
+
+    /**
+     * 用户上传文件类型不匹配
+     */
+    USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"),
+
+    /**
+     * 一级宏观错误码,系统执行出错
+     */
+    SYSTEM_ERROR("B0001", "系统执行出错"),
+
+    /**
+     * 二级宏观错误码,系统执行超时
+     */
+    SYSTEM_TIMEOUT_ERROR("B0100", "系统执行超时"),
+
+    /**
+     * 一级宏观错误码,调用第三方服务出错
+     */
+    THIRD_SERVICE_ERROR("C0001", "调用第三方服务出错"),
+
+    /**
+     * 一级宏观错误码,中间件服务出错
+     */
+    MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错");
+
+    /**
+     * 错误码
+     */
+    private final String code;
+
+    /**
+     * 中文描述
+     */
+    private final String message;
+
+}

+ 45 - 0
novel-demo/src/main/java/com/sf/common/constant/SystemConfigConsts.java

@@ -0,0 +1,45 @@
+package com.sf.common.constant;
+
+/**
+ * 系统配置相关常量
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/12
+ */
+public class SystemConfigConsts {
+
+    private SystemConfigConsts() {
+        throw new IllegalStateException(CONST_INSTANCE_EXCEPTION_MSG);
+    }
+
+    /**
+     * Http 请求认证 Header
+     */
+    public static final String HTTP_AUTH_HEADER_NAME = "Authorization";
+
+    /**
+     * 前台门户系统标识
+     */
+    public static final String NOVEL_FRONT_KEY = "front";
+
+    /**
+     * 作家管理系统标识
+     */
+    public static final String NOVEL_AUTHOR_KEY = "author";
+
+    /**
+     * 后台管理系统标识
+     */
+    public static final String NOVEL_ADMIN_KEY = "admin";
+
+    /**
+     * 图片上传目录
+     */
+    public static final String IMAGE_UPLOAD_DIRECTORY = "/image/";
+
+    /**
+     * 常量类实例化异常信息
+     */
+    public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class";
+
+}

+ 87 - 0
novel-demo/src/main/java/com/sf/common/resp/RestResp.java

@@ -0,0 +1,87 @@
+package com.sf.common.resp;
+
+import com.sf.common.constant.ErrorCodeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * Http Rest 响应工具及数据格式封装
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/11
+ */
+@Getter
+public class RestResp<T> {
+
+    /**
+     * 响应码
+     */
+    @Schema(description = "错误码,00000-没有错误")
+    private String code;
+
+    /**
+     * 响应消息
+     */
+    @Schema(description = "响应消息")
+    private String message;
+
+    /**
+     * 响应数据
+     */
+    @Schema(description = "响应数据")
+    private T data;
+
+    private RestResp() {
+        this.code = ErrorCodeEnum.OK.getCode();
+        this.message = ErrorCodeEnum.OK.getMessage();
+    }
+
+    private RestResp(ErrorCodeEnum errorCode) {
+        this.code = errorCode.getCode();
+        this.message = errorCode.getMessage();
+    }
+
+    private RestResp(T data) {
+        this();
+        this.data = data;
+    }
+
+    /**
+     * 业务处理成功,无数据返回
+     */
+    public static RestResp<Void> ok() {
+        return new RestResp<>();
+    }
+
+    /**
+     * 业务处理成功,有数据返回
+     */
+    public static <T> RestResp<T> ok(T data) {
+        return new RestResp<>(data);
+    }
+
+    /**
+     * 业务处理失败
+     */
+    public static RestResp<Void> fail(ErrorCodeEnum errorCode) {
+        return new RestResp<>(errorCode);
+    }
+
+
+    /**
+     * 系统错误
+     */
+    public static RestResp<Void> error() {
+        return new RestResp<>(ErrorCodeEnum.SYSTEM_ERROR);
+    }
+
+    /**
+     * 判断是否成功
+     */
+    public boolean isOk() {
+        return Objects.equals(this.code, ErrorCodeEnum.OK.getCode());
+    }
+
+}

+ 79 - 0
novel-demo/src/main/java/com/sf/config/CacheConfig.java

@@ -0,0 +1,79 @@
+package com.sf.config;
+
+import com.sf.constant.CacheConsts;
+import org.springframework.cache.CacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.cache.RedisCacheWriter;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+
+import java.time.Duration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 缓存配置类
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/12
+ */
+@Configuration
+public class CacheConfig {
+
+    /**
+     * Redis 缓存管理器
+     */
+    @Bean
+    public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
+
+        //设置CacheManager的值序列化方式为 fastJsonRedisSerializer,但其实RedisCacheConfiguration默认使用StringRedisSerializer序列化key,
+        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(
+            connectionFactory);
+        // 设置使用redis的一系列参数
+        // RedisConnectionFactory 是创建redis连接的工厂  我们是redis的客户端(jedis -> redission)
+        //  序列化方式 (决定数据以什么格式存储到redis中 以及取数据后 如何解析)
+        //  默认会使用jdk的序列化方式  但实际开发会设置为其他  如 Jackson2JsonRedisSerializer
+        //     创建一个序列化的对象  然后将对象放入redis的序列化容器中
+        //  项目在redis存储时的前缀是什么  所有存储的key都是前缀加上key的名字  通过RedisCacheConfiguration设置
+        //     将要使用的key统一管理  设置key所对应的失效时间  放到枚举类中
+
+        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
+
+        RedisSerializationContext.SerializationPair<Object> pair =
+                RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer);
+
+        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
+                .serializeValuesWith(pair)
+                .disableCachingNullValues()
+                .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX);
+
+        Map<String, RedisCacheConfiguration> cacheMap = new LinkedHashMap<>(
+            CacheConsts.CacheEnum.values().length);
+        // 类型推断 var 非常适合 for 循环,JDK 10 引入,JDK 11 改进
+        for (var c : CacheConsts.CacheEnum.values()) {
+            if (c.isRemote()) {
+                if (c.getTtl() > 0) {
+                    cacheMap.put(c.getName(),
+                        RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
+                            .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX)
+                            .entryTtl(Duration.ofSeconds(c.getTtl())));
+                } else {
+                    cacheMap.put(c.getName(),
+                        RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
+                            .prefixCacheNameWith(CacheConsts.REDIS_CACHE_PREFIX));
+                }
+            }
+        }
+
+        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
+            defaultCacheConfig, cacheMap);
+        redisCacheManager.setTransactionAware(true);
+        redisCacheManager.initializeCaches();
+        return redisCacheManager;
+    }
+
+}

+ 57 - 0
novel-demo/src/main/java/com/sf/config/CorsConfig.java

@@ -0,0 +1,57 @@
+package com.sf.config;
+
+import jakarta.annotation.Resource;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 跨域配置
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/13
+ */
+@Configuration
+@EnableConfigurationProperties(CorsProperties.class)
+//@RequiredArgsConstructor
+public class CorsConfig {
+
+    // 另一种注入方式  @RequiredArgsConstructor
+//    private final CorsProperties corsProperties;
+
+    @Resource
+    private CorsProperties corsProperties;
+
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        // 允许的域,不要写*,否则cookie就无法使用了
+//        List<String> list = new ArrayList<>();
+//        list.add("http://localhost:1024");
+        for (String allowOrigin : corsProperties.allowOrigins()) {
+            config.addAllowedOrigin(allowOrigin);
+        }
+        // 允许的头信息
+        config.addAllowedHeader("*");
+        // 允许的请求方式
+        config.addAllowedMethod("*");
+        // 是否允许携带Cookie信息
+        config.setAllowCredentials(true);
+
+        // url基础跨域配置
+        UrlBasedCorsConfigurationSource configurationSource =
+                new UrlBasedCorsConfigurationSource();
+        // 添加映射路径,拦截一切响应  然后在响应头增加跨域设置
+        configurationSource.registerCorsConfiguration("/**", config);
+        return new CorsFilter(configurationSource);
+    }
+
+}

+ 34 - 0
novel-demo/src/main/java/com/sf/config/CorsProperties.java

@@ -0,0 +1,34 @@
+package com.sf.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+/**
+ * 跨域配置属性
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/17
+ */
+@ConfigurationProperties(prefix = "novel.cors")
+// record 一条记录  一个简单的类
+public record CorsProperties(List<String> allowOrigins) {
+
+}
+/* public class CorsProperties(){
+       List<String> allowOrigins;
+       get/set
+}*/
+
+/*
+@ConfigurationProperties 帮忙去找配置文件的数据  查找的路径是 "novel.cors"
+List<String> allowOrigins  接收找到的字符串数组
+
+novel:
+  # 跨域配置
+  cors:
+    # 允许跨域的域名
+    allow-origins:
+      - http://localhost:1024
+      - http://localhost:8080
+ */

+ 27 - 0
novel-demo/src/main/java/com/sf/constant/AmqpConsts.java

@@ -0,0 +1,27 @@
+package com.sf.constant;
+
+public class AmqpConsts {
+
+    /**
+     * 小说信息改变 MQ
+     */
+    public static class BookChangeMq {
+
+        /**
+         * 小说信息改变交换机
+         */
+        public static final String EXCHANGE_NAME = "EXCHANGE-BOOK-CHANGE";
+
+        /**
+         * Elasticsearch book 索引更新的队列
+         */
+        public static final String QUEUE_ES_UPDATE = "QUEUE-ES-BOOK-UPDATE";
+
+        /**
+         * Redis book 缓存更新的队列
+         */
+        public static final String QUEUE_REDIS_UPDATE = "QUEUE-REDIS-BOOK-UPDATE";
+
+    }
+
+}

+ 175 - 0
novel-demo/src/main/java/com/sf/constant/CacheConsts.java

@@ -0,0 +1,175 @@
+package com.sf.constant;
+
+/**
+ * 缓存相关常量
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/12
+ */
+public class CacheConsts {
+
+    /**
+     * 本项目 Redis 缓存前缀
+     */
+    public static final String REDIS_CACHE_PREFIX = "Cache::NovelDemo::";
+
+
+    /**
+     * Caffeine 缓存管理器
+     */
+    public static final String CAFFEINE_CACHE_MANAGER = "caffeineCacheManager";
+
+    /**
+     * Redis 缓存管理器
+     */
+    public static final String REDIS_CACHE_MANAGER = "redisCacheManager";
+
+    /**
+     * 首页小说推荐缓存
+     */
+    public static final String HOME_BOOK_CACHE_NAME = "homeBookCache";
+
+    /**
+     * 最新新闻缓存
+     */
+    public static final String LATEST_NEWS_CACHE_NAME = "latestNewsCache";
+
+    /**
+     * 小说点击榜缓存
+     */
+    public static final String BOOK_VISIT_RANK_CACHE_NAME = "bookVisitRankCache";
+
+    /**
+     * 小说新书榜缓存
+     */
+    public static final String BOOK_NEWEST_RANK_CACHE_NAME = "bookNewestRankCache";
+
+    /**
+     * 小说更新榜缓存
+     */
+    public static final String BOOK_UPDATE_RANK_CACHE_NAME = "bookUpdateRankCache";
+
+    /**
+     * 首页友情链接缓存
+     */
+    public static final String HOME_FRIEND_LINK_CACHE_NAME = "homeFriendLinkCache";
+
+    /**
+     * 小说分类列表缓存
+     */
+    public static final String BOOK_CATEGORY_LIST_CACHE_NAME = "bookCategoryListCache";
+
+    /**
+     * 小说信息缓存
+     */
+    public static final String BOOK_INFO_CACHE_NAME = "bookInfoCache";
+
+    /**
+     * 小说章节缓存
+     */
+    public static final String BOOK_CHAPTER_CACHE_NAME = "bookChapterCache";
+
+    /**
+     * 小说内容缓存
+     */
+    public static final String BOOK_CONTENT_CACHE_NAME = "bookContentCache";
+
+    /**
+     * 最近更新小说ID列表缓存
+     */
+    public static final String LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME = "lastUpdateBookIdListCache";
+
+    /**
+     * 图片验证码缓存 KEY
+     */
+    public static final String IMG_VERIFY_CODE_CACHE_KEY =
+        REDIS_CACHE_PREFIX + "imgVerifyCodeCache::";
+
+    /**
+     * 用户信息缓存
+     */
+    public static final String USER_INFO_CACHE_NAME = "userInfoCache";
+
+    /**
+     * 作家信息缓存
+     */
+    public static final String AUTHOR_INFO_CACHE_NAME = "authorInfoCache";
+
+    /**
+     * 缓存配置常量
+     */
+    public enum CacheEnum {
+
+        HOME_BOOK_CACHE(0, HOME_BOOK_CACHE_NAME, 60 * 60 * 24, 1),
+
+        LATEST_NEWS_CACHE(0, LATEST_NEWS_CACHE_NAME, 60 * 10, 1),
+
+        BOOK_VISIT_RANK_CACHE(2, BOOK_VISIT_RANK_CACHE_NAME, 60 * 60 * 6, 1),
+
+        BOOK_NEWEST_RANK_CACHE(0, BOOK_NEWEST_RANK_CACHE_NAME, 60 * 30, 1),
+
+        BOOK_UPDATE_RANK_CACHE(0, BOOK_UPDATE_RANK_CACHE_NAME, 60, 1),
+
+        HOME_FRIEND_LINK_CACHE(2, HOME_FRIEND_LINK_CACHE_NAME, 0, 1),
+
+        BOOK_CATEGORY_LIST_CACHE(0, BOOK_CATEGORY_LIST_CACHE_NAME, 0, 2),
+
+        BOOK_INFO_CACHE(0, BOOK_INFO_CACHE_NAME, 60 * 60 * 18, 500),
+
+        BOOK_CHAPTER_CACHE(0, BOOK_CHAPTER_CACHE_NAME, 60 * 60 * 6, 5000),
+
+        BOOK_CONTENT_CACHE(2, BOOK_CONTENT_CACHE_NAME, 60 * 60 * 12, 3000),
+
+        LAST_UPDATE_BOOK_ID_LIST_CACHE(0, LAST_UPDATE_BOOK_ID_LIST_CACHE_NAME, 60 * 60, 10),
+
+        USER_INFO_CACHE(2, USER_INFO_CACHE_NAME, 60 * 60 * 24, 10000),
+
+        AUTHOR_INFO_CACHE(2, AUTHOR_INFO_CACHE_NAME, 60 * 60 * 48, 1000);
+
+        /**
+         * 缓存类型 0-本地 1-本地和远程 2-远程
+         */
+        private int type;
+        /**
+         * 缓存的名字
+         */
+        private String name;
+        /**
+         * 失效时间(秒) 0-永不失效
+         */
+        private int ttl;
+        /**
+         * 最大容量
+         */
+        private int maxSize;
+
+        CacheEnum(int type, String name, int ttl, int maxSize) {
+            this.type = type;
+            this.name = name;
+            this.ttl = ttl;
+            this.maxSize = maxSize;
+        }
+
+        public boolean isLocal() {
+            return type <= 1;
+        }
+
+        public boolean isRemote() {
+            return type >= 1;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public int getTtl() {
+            return ttl;
+        }
+
+        public int getMaxSize() {
+            return maxSize;
+        }
+
+    }
+
+}

+ 201 - 0
novel-demo/src/main/java/com/sf/constant/DatabaseConsts.java

@@ -0,0 +1,201 @@
+package com.sf.constant;
+
+import lombok.Getter;
+
+/**
+ * 数据库 常量
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/12
+ */
+public class DatabaseConsts {
+
+    /**
+     * 用户信息表
+     */
+    public static class UserInfoTable {
+
+        private UserInfoTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USERNAME = "username";
+
+    }
+
+    /**
+     * 用户反馈表
+     */
+    public static class UserFeedBackTable {
+
+        private UserFeedBackTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+    }
+
+    /**
+     * 用户书架表
+     */
+    public static class UserBookshelfTable {
+
+        private UserBookshelfTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+        public static final String COLUMN_BOOK_ID = "book_id";
+
+    }
+
+    /**
+     * 作家信息表
+     */
+    public static class AuthorInfoTable {
+
+        private AuthorInfoTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+    }
+
+    /**
+     * 小说类别表
+     */
+    public static class BookCategoryTable {
+
+        private BookCategoryTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_WORK_DIRECTION = "work_direction";
+
+    }
+
+    /**
+     * 小说表
+     */
+    public static class BookTable {
+
+        private BookTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_CATEGORY_ID = "category_id";
+
+        public static final String COLUMN_BOOK_NAME = "book_name";
+
+        public static final String AUTHOR_ID = "author_id";
+
+        public static final String COLUMN_VISIT_COUNT = "visit_count";
+
+        public static final String COLUMN_WORD_COUNT = "word_count";
+
+        public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
+
+    }
+
+    /**
+     * 小说章节表
+     */
+    public static class BookChapterTable {
+
+        private BookChapterTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_BOOK_ID = "book_id";
+
+        public static final String COLUMN_CHAPTER_NUM = "chapter_num";
+
+        public static final String COLUMN_LAST_CHAPTER_UPDATE_TIME = "last_chapter_update_time";
+
+    }
+
+    /**
+     * 小说内容表
+     */
+    public static class BookContentTable {
+
+        private BookContentTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_CHAPTER_ID = "chapter_id";
+
+    }
+
+    /**
+     * 小说评论表
+     */
+    public static class BookCommentTable {
+
+        private BookCommentTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_BOOK_ID = "book_id";
+
+        public static final String COLUMN_USER_ID = "user_id";
+
+    }
+
+    /**
+     * 新闻内容表
+     */
+    public static class NewsContentTable {
+
+        private NewsContentTable() {
+            throw new IllegalStateException(SystemConfigConsts.CONST_INSTANCE_EXCEPTION_MSG);
+        }
+
+        public static final String COLUMN_NEWS_ID = "news_id";
+
+    }
+
+    /**
+     * 通用列枚举类
+     */
+    @Getter
+    public enum CommonColumnEnum {
+
+        ID("id"),
+        SORT("sort"),
+        CREATE_TIME("create_time"),
+        UPDATE_TIME("update_time");
+
+        private String name;
+
+        CommonColumnEnum(String name) {
+            this.name = name;
+        }
+
+    }
+
+
+    /**
+     * SQL语句枚举类
+     */
+    @Getter
+    public enum SqlEnum {
+
+        LIMIT_1("limit 1"),
+        LIMIT_2("limit 2"),
+        LIMIT_5("limit 5"),
+        LIMIT_30("limit 30"),
+        LIMIT_500("limit 500");
+
+        private String sql;
+
+        SqlEnum(String sql) {
+            this.sql = sql;
+        }
+
+    }
+
+}

+ 45 - 0
novel-demo/src/main/java/com/sf/constant/SystemConfigConsts.java

@@ -0,0 +1,45 @@
+package com.sf.constant;
+
+/**
+ * 系统配置相关常量
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/12
+ */
+public class SystemConfigConsts {
+
+    private SystemConfigConsts() {
+        throw new IllegalStateException(CONST_INSTANCE_EXCEPTION_MSG);
+    }
+
+    /**
+     * Http 请求认证 Header
+     */
+    public static final String HTTP_AUTH_HEADER_NAME = "Authorization";
+
+    /**
+     * 前台门户系统标识
+     */
+    public static final String NOVEL_FRONT_KEY = "front";
+
+    /**
+     * 作家管理系统标识
+     */
+    public static final String NOVEL_AUTHOR_KEY = "author";
+
+    /**
+     * 后台管理系统标识
+     */
+    public static final String NOVEL_ADMIN_KEY = "admin";
+
+    /**
+     * 图片上传目录
+     */
+    public static final String IMAGE_UPLOAD_DIRECTORY = "/image/";
+
+    /**
+     * 常量类实例化异常信息
+     */
+    public static final String CONST_INSTANCE_EXCEPTION_MSG = "Constant class";
+
+}

+ 63 - 0
novel-demo/src/main/java/com/sf/controller/BookInfoController.java

@@ -0,0 +1,63 @@
+package com.sf.controller;
+
+import com.sf.common.resp.RestResp;
+import com.sf.dto.resp.BookChapterRespDto;
+import com.sf.dto.resp.BookContentAboutRespDto;
+import com.sf.dto.resp.BookInfoRespDto;
+import com.sf.service.IBookInfoService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.stereotype.Controller;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 小说信息 前端控制器
+ * </p>
+ *
+ * @author Qing
+ * @since 2023-12-01
+ */
+//@CrossOrigin(originPatterns = "*",allowCredentials = "true")
+@RestController
+@RequestMapping("/api/front/book")
+public class BookInfoController {
+
+    @Autowired
+    private IBookInfoService bookInfoService;
+
+    /**
+     * 小说章节列表查询接口
+     */
+    @Operation(summary = "小说章节列表查询接口")
+    @GetMapping("chapter/list")
+    public RestResp<List<BookChapterRespDto>> listChapters(
+            @Parameter(description = "小说ID") Long bookId) {
+        return bookInfoService.listChapters(bookId);
+    }
+
+    /**
+     * 小说内容相关信息查询接口
+     */
+    @Operation(summary = "小说内容相关信息查询接口")
+    @GetMapping("content/{chapterId}")
+    public RestResp<BookContentAboutRespDto> getBookContentAbout(
+            @Parameter(description = "章节ID") @PathVariable("chapterId") Long chapterId) {
+        return bookInfoService.getBookContentAbout(chapterId);
+    }
+
+
+    /**
+     * 小说信息查询接口
+     */
+    @Operation(summary = "小说信息查询接口")
+    @GetMapping("{id}")
+    public RestResp<BookInfoRespDto> getBookById(
+            @Parameter(description = "小说 ID") @PathVariable("id") Long bookId) {
+        return bookInfoService.getBookById(bookId);
+    }
+
+}

+ 41 - 0
novel-demo/src/main/java/com/sf/dto/req/ChapterAddReqDto.java

@@ -0,0 +1,41 @@
+package com.sf.dto.req;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 章节发布 请求DTO
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/23
+ */
+@Data
+@Builder
+public class ChapterAddReqDto {
+
+    /**
+     * 小说ID
+     */
+    @Schema(description = "小说ID", required = true)
+    private Long bookId;
+
+    /**
+     * 章节名
+     */
+    @Schema(description = "章节名", required = true)
+    private String chapterName;
+
+    /**
+     * 章节内容
+     */
+    @Schema(description = "章节内容", required = true)
+    private String chapterContent;
+
+    /**
+     * 是否收费;1-收费 0-免费
+     */
+    @Schema(description = "是否收费;1-收费 0-免费", required = true)
+    private Integer isVip;
+
+}

+ 29 - 0
novel-demo/src/main/java/com/sf/dto/resp/BookCategoryRespDto.java

@@ -0,0 +1,29 @@
+package com.sf.dto.resp;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 小说分类 响应DTO
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/16
+ */
+@Data
+@Builder
+public class BookCategoryRespDto {
+
+    /**
+     * 类别ID
+     */
+    @Schema(description = "类别ID")
+    private Long id;
+
+    /**
+     * 类别名
+     */
+    @Schema(description = "类别名")
+    private String name;
+
+}

+ 31 - 0
novel-demo/src/main/java/com/sf/dto/resp/BookChapterAboutRespDto.java

@@ -0,0 +1,31 @@
+package com.sf.dto.resp;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 小说章节相关 响应DTO
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/15
+ */
+@Data
+@Builder
+public class BookChapterAboutRespDto {
+
+    private BookChapterRespDto chapterInfo;
+
+    /**
+     * 章节总数
+     */
+    @Schema(description = "章节总数")
+    private Long chapterTotal;
+
+    /**
+     * 内容概要(30字)
+     */
+    @Schema(description = " 内容概要(30字)")
+    private String contentSummary;
+
+}

+ 78 - 0
novel-demo/src/main/java/com/sf/dto/resp/BookChapterRespDto.java

@@ -0,0 +1,78 @@
+package com.sf.dto.resp;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 小说章节 响应DTO
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/15
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BookChapterRespDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 章节ID
+     */
+    @Schema(description = "章节ID")
+    private Long id;
+
+    /**
+     * 小说ID
+     */
+    @Schema(description = "小说ID")
+    private Long bookId;
+
+    /**
+     * 章节号
+     */
+    @Schema(description = "章节号")
+    private Integer chapterNum;
+
+    /**
+     * 章节名
+     */
+    @Schema(description = "章节名")
+    private String chapterName;
+
+    /**
+     * 章节字数
+     */
+    @Schema(description = "章节字数")
+    private Integer chapterWordCount;
+
+    /**
+     * 章节更新时间
+     */
+    @Schema(description = "章节更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @JsonSerialize(using = LocalDateTimeSerializer.class)
+    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
+    private LocalDateTime chapterUpdateTime;
+
+    /**
+     * 是否收费;1-收费 0-免费
+     */
+    @Schema(description = "是否收费;1-收费 0-免费")
+    private Integer isVip;
+
+}

+ 54 - 0
novel-demo/src/main/java/com/sf/dto/resp/BookCommentRespDto.java

@@ -0,0 +1,54 @@
+package com.sf.dto.resp;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+//import io.github.xxyopen.novel.core.json.serializer.UsernameSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 小说评论 响应DTO
+ * @author xiongxiaoyang
+ * @date 2022/5/17
+ */
+@Data
+@Builder
+public class BookCommentRespDto {
+
+    @Schema(description = "评论总数")
+    private Long commentTotal;
+
+    @Schema(description = "评论列表")
+    private List<CommentInfo> comments;
+
+    @Data
+    @Builder
+    public static class CommentInfo {
+
+        @Schema(description = "评论ID")
+        private Long id;
+
+        @Schema(description = "评论内容")
+        private String commentContent;
+
+        @Schema(description = "评论用户")
+//        @JsonSerialize(using = UsernameSerializer.class)
+        private String commentUser;
+
+        @Schema(description = "评论用户ID")
+        private Long commentUserId;
+
+        @Schema(description = "评论用户头像")
+        private String commentUserPhoto;
+
+        @Schema(description = "评论时间")
+        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+        private LocalDateTime commentTime;
+
+    }
+
+}

+ 35 - 0
novel-demo/src/main/java/com/sf/dto/resp/BookContentAboutRespDto.java

@@ -0,0 +1,35 @@
+package com.sf.dto.resp;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 小说内容相关 响应DTO
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/15
+ */
+@Data
+@Builder
+public class BookContentAboutRespDto {
+
+    /**
+     * 小说信息
+     */
+    @Schema(description = "小说信息")
+    private BookInfoRespDto bookInfo;
+
+    /**
+     * 章节信息
+     */
+    @Schema(description = "章节信息")
+    private BookChapterRespDto chapterInfo;
+
+    /**
+     * 章节内容
+     */
+    @Schema(description = "章节内容")
+    private String bookContent;
+
+}

+ 122 - 0
novel-demo/src/main/java/com/sf/dto/resp/BookInfoRespDto.java

@@ -0,0 +1,122 @@
+package com.sf.dto.resp;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 小说信息 响应DTO
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/15
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class BookInfoRespDto {
+
+    /**
+     * ID
+     */
+    @Schema(description = "小说ID")
+    private Long id;
+
+    /**
+     * 类别ID
+     */
+    @Schema(description = "类别ID")
+    private Long categoryId;
+
+    /**
+     * 类别名
+     */
+    @Schema(description = "类别名")
+    private String categoryName;
+
+    /**
+     * 小说封面地址
+     */
+    @Schema(description = "小说封面地址")
+    private String picUrl;
+
+    /**
+     * 小说名
+     */
+    @Schema(description = "小说名")
+    private String bookName;
+
+    /**
+     * 作家id
+     */
+    @Schema(description = "作家id")
+    private Long authorId;
+
+    /**
+     * 作家名
+     */
+    @Schema(description = "作家名")
+    private String authorName;
+
+    /**
+     * 书籍描述
+     */
+    @Schema(description = "书籍描述")
+    private String bookDesc;
+
+    /**
+     * 书籍状态;0-连载中 1-已完结
+     */
+    @Schema(description = "书籍状态;0-连载中 1-已完结")
+    private Integer bookStatus;
+
+    /**
+     * 点击量
+     */
+    @Schema(description = "点击量")
+    private Long visitCount;
+
+    /**
+     * 总字数
+     */
+    @Schema(description = "总字数")
+    private Integer wordCount;
+
+    /**
+     * 评论数
+     */
+    @Schema(description = "评论数")
+    private Integer commentCount;
+
+    /**
+     * 首章节ID
+     */
+    @Schema(description = "首章节ID")
+    private Long firstChapterId;
+
+    /**
+     * 最新章节ID
+     */
+    @Schema(description = "最新章节ID")
+    private Long lastChapterId;
+
+    /**
+     * 最新章节名
+     */
+    @Schema(description = "最新章节名")
+    private String lastChapterName;
+
+    /**
+     * 最新章节更新时间
+     */
+    @Schema(description = "最新章节更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm")
+    private LocalDateTime updateTime;
+
+
+}

+ 84 - 0
novel-demo/src/main/java/com/sf/dto/resp/BookRankRespDto.java

@@ -0,0 +1,84 @@
+package com.sf.dto.resp;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 小说排行榜 响应DTO
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/14
+ */
+@Data
+public class BookRankRespDto implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * ID
+     */
+    @Schema(description = "小说ID")
+    private Long id;
+
+    /**
+     * 类别ID
+     */
+    @Schema(description = "类别ID")
+    private Long categoryId;
+
+    /**
+     * 类别名
+     */
+    @Schema(description = "类别名")
+    private String categoryName;
+
+    /**
+     * 小说封面地址
+     */
+    @Schema(description = "小说封面地址")
+    private String picUrl;
+
+    /**
+     * 小说名
+     */
+    @Schema(description = "小说名")
+    private String bookName;
+
+    /**
+     * 作家名
+     */
+    @Schema(description = "作家名")
+    private String authorName;
+
+    /**
+     * 书籍描述
+     */
+    @Schema(description = "书籍描述")
+    private String bookDesc;
+
+    /**
+     * 总字数
+     */
+    @Schema(description = "总字数")
+    private Integer wordCount;
+
+    /**
+     * 最新章节名
+     */
+    @Schema(description = "最新章节名")
+    private String lastChapterName;
+
+    /**
+     * 最新章节更新时间
+     */
+    @Schema(description = "最新章节更新时间")
+    @JsonFormat(pattern = "MM/dd HH:mm")
+    private LocalDateTime lastChapterUpdateTime;
+
+}

+ 74 - 0
novel-demo/src/main/java/com/sf/manager/cache/BookCacheManager.java

@@ -0,0 +1,74 @@
+package com.sf.manager.cache;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.sf.constant.CacheConsts;
+import com.sf.constant.DatabaseConsts;
+import com.sf.dto.resp.BookChapterRespDto;
+import com.sf.mapper.BookChapterMapper;
+import com.sf.mapper.BookContentMapper;
+import com.sf.po.BookChapter;
+import com.sf.po.BookContent;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Component;
+
+/**
+ * 小说内容 缓存管理类
+ *
+ * @author xiongxiaoyang
+ * @date 2022/5/12
+ */
+@Component
+//@RequiredArgsConstructor
+public class BookCacheManager {
+
+
+
+    @Autowired
+    private BookContentMapper bookContentMapper;
+
+    @Autowired
+    private BookChapterMapper bookChapterMapper;
+
+    /**
+     * 查询小说内容,并放入缓存中
+     */
+    @Cacheable(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
+        value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
+    public String getBookContent(Long chapterId) {
+        QueryWrapper<BookContent> contentQueryWrapper = new QueryWrapper<>();
+        contentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId)
+            .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
+        BookContent bookContent = bookContentMapper.selectOne(contentQueryWrapper);
+        return bookContent.getContent();
+    }
+
+    @Cacheable(cacheManager = "redisCacheManager",value = "bookChapterCache")
+    public BookChapterRespDto getBookChapterNew(Long chapterId){
+        BookChapter bookChapter = bookChapterMapper.selectById(chapterId);
+        BookChapterRespDto bookChapterDto = BookChapterRespDto.builder()
+                .chapterWordCount(bookChapter.getWordCount())
+                .chapterUpdateTime(bookChapter.getUpdateTime()).build();
+        BeanUtils.copyProperties(bookChapter,bookChapterDto);
+//        BookChapterRespDto bookChapterDto = BookChapterRespDto.builder()
+//                .id(chapterId)
+//                .bookId(bookChapter.getBookId())
+//                .chapterNum(bookChapter.getChapterNum().intValue())
+//                .chapterName(bookChapter.getChapterName())
+//                .chapterWordCount(bookChapter.getWordCount())
+////                .chapterUpdateTime(bookChapter.getUpdateTime())
+//                .isVip(bookChapter.getIsVip().intValue())
+//                .build();
+        return bookChapterDto;
+    }
+
+    @CacheEvict(cacheManager = CacheConsts.REDIS_CACHE_MANAGER,
+        value = CacheConsts.BOOK_CONTENT_CACHE_NAME)
+    public void evictBookContentCache(Long chapterId) {
+        // 调用此方法自动清除小说内容信息的缓存
+    }
+
+
+}

+ 49 - 0
novel-demo/src/main/java/com/sf/service/IBookInfoService.java

@@ -0,0 +1,49 @@
+package com.sf.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.sf.common.resp.RestResp;
+import com.sf.dto.req.ChapterAddReqDto;
+import com.sf.dto.resp.BookChapterRespDto;
+import com.sf.dto.resp.BookContentAboutRespDto;
+import com.sf.dto.resp.BookInfoRespDto;
+import com.sf.po.BookInfo;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 小说信息 服务类
+ * </p>
+ *
+ * @author Qing
+ * @since 2023-12-01
+ */
+public interface IBookInfoService extends IService<BookInfo> {
+
+    RestResp<Void> saveBookChapter(ChapterAddReqDto dto);
+
+    /**
+     * 小说信息查询
+     *
+     * @param bookId 小说ID
+     * @return 小说信息
+     */
+    RestResp<BookInfoRespDto> getBookById(Long bookId);
+
+    /**
+     * 小说章节列表查询
+     *
+     * @param bookId 小说ID
+     * @return 小说章节列表
+     */
+    RestResp<List<BookChapterRespDto>> listChapters(Long bookId);
+
+    /**
+     * 小说内容相关信息查询
+     *
+     * @param chapterId 章节ID
+     * @return 内容相关联的信息
+     */
+    RestResp<BookContentAboutRespDto> getBookContentAbout(Long chapterId);
+
+}

+ 222 - 0
novel-demo/src/main/java/com/sf/service/impl/BookInfoServiceImpl.java

@@ -0,0 +1,222 @@
+package com.sf.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.sf.common.constant.DatabaseConsts;
+import com.sf.common.resp.RestResp;
+import com.sf.dto.req.ChapterAddReqDto;
+import com.sf.dto.resp.BookChapterRespDto;
+import com.sf.dto.resp.BookContentAboutRespDto;
+import com.sf.dto.resp.BookInfoRespDto;
+import com.sf.manager.cache.BookCacheManager;
+import com.sf.mapper.BookChapterMapper;
+import com.sf.mapper.BookContentMapper;
+import com.sf.mapper.BookInfoMapper;
+import com.sf.po.BookChapter;
+import com.sf.po.BookContent;
+import com.sf.po.BookInfo;
+import com.sf.service.IBookInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * <p>
+ * 小说信息 服务实现类
+ * </p>
+ *
+ * @author Qing
+ * @since 2023-12-01
+ */
+@Service
+public class BookInfoServiceImpl extends ServiceImpl<BookInfoMapper, BookInfo> implements IBookInfoService {
+
+    @Autowired
+    private BookInfoMapper bookInfoMapper;
+
+    @Autowired
+    private BookChapterMapper bookChapterMapper;
+
+    @Autowired
+    private BookContentMapper bookContentMapper;
+
+    @Autowired
+    private BookCacheManager bookCacheManager;
+
+    @Override
+    public RestResp<Void> saveBookChapter(ChapterAddReqDto dto) {
+        // 校验该作品是否属于当前作家
+        BookInfo bookInfo = bookInfoMapper.selectById(dto.getBookId());
+//        if (!Objects.equals(bookInfo.getAuthorId(), UserHolder.getAuthorId())) {
+//            return RestResp.fail(ErrorCodeEnum.USER_UN_AUTH);
+//        }
+        // 1) 保存章节相关信息到小说章节表
+        //  a) 查询最新章节号
+        int chapterNum = 0;
+        QueryWrapper<BookChapter> chapterQueryWrapper = new QueryWrapper<>();
+        chapterQueryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, dto.getBookId())
+                .orderByDesc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
+                .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
+        BookChapter bookChapter = bookChapterMapper.selectOne(chapterQueryWrapper);
+        if (Objects.nonNull(bookChapter)) {
+            chapterNum = bookChapter.getChapterNum() + 1;
+        }
+        //  b) 设置章节相关信息并保存
+        BookChapter newBookChapter = BookChapter.builder().build();
+        newBookChapter.setBookId(dto.getBookId());
+        newBookChapter.setChapterName(dto.getChapterName());
+        newBookChapter.setChapterNum(chapterNum);
+        newBookChapter.setWordCount(dto.getChapterContent().length());
+        newBookChapter.setIsVip(dto.getIsVip().byteValue());
+        newBookChapter.setCreateTime(LocalDateTime.now());
+        newBookChapter.setUpdateTime(LocalDateTime.now());
+        bookChapterMapper.insert(newBookChapter);
+
+        // 2) 保存章节内容到小说内容表
+        BookContent bookContent = BookContent.builder().build();
+        bookContent.setContent(dto.getChapterContent());
+        bookContent.setChapterId(newBookChapter.getId());
+        bookContent.setCreateTime(LocalDateTime.now());
+        bookContent.setUpdateTime(LocalDateTime.now());
+        bookContentMapper.insert(bookContent);
+
+        // 3) 更新小说表最新章节信息和小说总字数信息
+        //  a) 更新小说表关于最新章节的信息
+        BookInfo newBookInfo = new BookInfo();
+        newBookInfo.setId(dto.getBookId());
+        newBookInfo.setLastChapterId(newBookChapter.getId());
+        newBookInfo.setLastChapterName(newBookChapter.getChapterName());
+        newBookInfo.setLastChapterUpdateTime(LocalDateTime.now());
+        newBookInfo.setWordCount(bookInfo.getWordCount() + newBookChapter.getWordCount());
+        newBookChapter.setUpdateTime(LocalDateTime.now());
+        bookInfoMapper.updateById(newBookInfo);
+        //  b) 清除小说信息缓存
+//        bookInfoCacheManager.evictBookInfoCache(dto.getBookId());
+        return RestResp.ok();
+    }
+
+    @Override
+    public RestResp<BookInfoRespDto> getBookById(Long bookId) {
+        // 查询基础信息
+        BookInfo bookInfo = bookInfoMapper.selectById(bookId);
+        // 查询首章ID
+        QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
+        queryWrapper
+                .eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId)
+                .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
+                .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
+        BookChapter firstBookChapter = bookChapterMapper.selectOne(queryWrapper);
+        // 组装响应对象
+        BookInfoRespDto dto = BookInfoRespDto.builder()
+                .id(bookInfo.getId())
+                .bookName(bookInfo.getBookName())
+                .bookDesc(bookInfo.getBookDesc())
+                .bookStatus(bookInfo.getBookStatus().intValue())
+                .authorId(bookInfo.getAuthorId())
+                .authorName(bookInfo.getAuthorName())
+                .categoryId(bookInfo.getCategoryId())
+                .categoryName(bookInfo.getCategoryName())
+                .commentCount(bookInfo.getCommentCount())
+                .firstChapterId(firstBookChapter.getId())
+                .lastChapterId(bookInfo.getLastChapterId())
+                .picUrl(bookInfo.getPicUrl())
+                .visitCount(bookInfo.getVisitCount())
+                .wordCount(bookInfo.getWordCount())
+                .build();
+        return RestResp.ok(dto);
+    }
+
+
+    @Override
+    public RestResp<BookContentAboutRespDto> getBookContentAbout(Long chapterId) {
+//        log.debug("userId:{}", UserHolder.getUserId());
+        // 查询章节信息
+//        BookChapter bookChapter = bookChapterMapper.selectById(chapterId);
+//        BookChapterRespDto bookChapterDto = BookChapterRespDto.builder()
+//                .id(chapterId)
+//                .bookId(bookChapter.getBookId())
+//                .chapterNum(bookChapter.getChapterNum().intValue())
+//                .chapterName(bookChapter.getChapterName())
+//                .chapterWordCount(bookChapter.getWordCount())
+//                .chapterUpdateTime(bookChapter.getUpdateTime())
+//                .isVip(bookChapter.getIsVip().intValue())
+//                .build();
+
+//        BookChapterRespDto bookChapterDto = bookCacheManager.getBookChapterNew(chapterId);
+//        BookChapterRespDto bookChapterDto = new ObjectMapper().convertValue(object,BookChapterRespDto.class);
+//        BookChapterRespDto bookChapterDto = JSONObject.toJavaObject(object,BookChapterRespDto.class);
+
+//        String objectJson = bookCacheManager.getBookChapter(chapterId);
+//        BookChapterRespDto bookChapterDto = new Gson().fromJson(objectJson,BookChapterRespDto.class);
+//
+        BookChapterRespDto bookChapterDto = bookCacheManager.getBookChapterNew(chapterId);
+
+        // 查询章节内容
+        String content = bookCacheManager.getBookContent(chapterId);
+//        QueryWrapper<BookContent> contentQueryWrapper = new QueryWrapper<>();
+//        contentQueryWrapper.eq(DatabaseConsts.BookContentTable.COLUMN_CHAPTER_ID, chapterId)
+//                .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
+//        BookContent bookContent = bookContentMapper.selectOne(contentQueryWrapper);
+//        String content = bookContent.getContent();
+
+        // 查询小说信息
+//        BookInfoRespDto bookInfo = bookInfoCacheManager.getBookInfo(bookChapter.getBookId());
+        // 查询基础信息
+        Long id = bookChapterDto.getBookId();
+        BookInfo bookInfo = bookInfoMapper.selectById(id);
+        // 查询首章ID
+        QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
+        queryWrapper
+                .eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, id)
+                .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM)
+                .last(DatabaseConsts.SqlEnum.LIMIT_1.getSql());
+        BookChapter firstBookChapter = bookChapterMapper.selectOne(queryWrapper);
+        // 组装响应对象
+        BookInfoRespDto bookInfoDto = BookInfoRespDto.builder()
+                .id(bookInfo.getId())
+                .bookName(bookInfo.getBookName())
+                .bookDesc(bookInfo.getBookDesc())
+                .bookStatus(bookInfo.getBookStatus().intValue())
+                .authorId(bookInfo.getAuthorId())
+                .authorName(bookInfo.getAuthorName())
+                .categoryId(bookInfo.getCategoryId())
+                .categoryName(bookInfo.getCategoryName())
+                .commentCount(bookInfo.getCommentCount())
+                .firstChapterId(firstBookChapter.getId())
+                .lastChapterId(bookInfo.getLastChapterId())
+                .picUrl(bookInfo.getPicUrl())
+                .visitCount(bookInfo.getVisitCount())
+                .wordCount(bookInfo.getWordCount())
+                .build();
+
+        // 组装数据并返回
+        return RestResp.ok(BookContentAboutRespDto.builder()
+                .bookInfo(bookInfoDto)
+                .chapterInfo(bookChapterDto)
+                .bookContent(content)
+                .build());
+    }
+
+
+    @Override
+    public RestResp<List<BookChapterRespDto>> listChapters(Long bookId) {
+        QueryWrapper<BookChapter> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq(DatabaseConsts.BookChapterTable.COLUMN_BOOK_ID, bookId)
+                .orderByAsc(DatabaseConsts.BookChapterTable.COLUMN_CHAPTER_NUM);
+        List<BookChapter> bookChapters = bookChapterMapper.selectList(queryWrapper);
+//        return RestResp.ok(null);
+        return RestResp.ok(bookChapters.stream().map(v -> BookChapterRespDto.builder()
+                .id(v.getId())
+                .bookId(bookId)
+                .chapterName(v.getChapterName())
+                .chapterNum(v.getChapterNum().intValue())
+                .chapterWordCount(v.getWordCount())
+                .isVip(v.getIsVip().intValue())
+                .build()).toList());
+    }
+
+
+}

+ 1 - 1
novel-demo/src/main/resources/application.properties

@@ -1 +1 @@
-server.port=18080
+server.port=8888

+ 22 - 0
novel-demo/src/main/resources/application.yml

@@ -6,6 +6,28 @@ spring:
     url: jdbc:mysql://localhost:3306/novel-cloud?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
     username: root
     password: root123456
+  data:
+    redis:
+      host: 127.0.0.1
+      port: 6379
+      password:
+
+novel:
+  # 跨域配置
+  cors:
+    # 允许跨域的域名
+    allow-origins:
+      - http://localhost:1024
+      - http://localhost:8080
+
+---
+spring:
+  jackson:
+    generator:
+      # JSON 序列化时,将所有 Number 类型的属性都转为 String 类型返回,避免前端数据精度丢失的问题。
+      # 由于 Javascript 标准规定所有数字处理都应使用 64 位 IEEE 754 浮点值完成,
+      # 结果是某些 64 位整数值无法准确表示(尾数只有 51 位宽)
+      write-numbers-as-strings: true
 
 #mybatis-plus:
 #  mapper-locations: classpath*:/mapper1/**/*.xml