yinshaojie hace 10 meses
padre
commit
dbd2463dd7
Se han modificado 32 ficheros con 684 adiciones y 33 borrados
  1. 7
    1
      RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml
  2. 8
    0
      RuoYi-Vue/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java
  3. 6
    0
      RuoYi-Vue/ruoyi-novel/pom.xml
  4. 16
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/config/MybatisPlusConfig.java
  5. 21
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/config/TableSupport.java
  6. 6
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/NovelAdController.java
  7. 37
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/NovelController.java
  8. 5
    2
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/NovelVoteController.java
  9. 8
    4
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/ReadingHistoryController.java
  10. 19
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/Novel.java
  11. 5
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/NovelChapter.java
  12. 4
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/NovelContent.java
  13. 6
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/ReadingRecord.java
  14. 2
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/StoryOption.java
  15. 1
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/VoteRecord.java
  16. 45
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/exception/GlobalExceptionHandler.java
  17. 18
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/NovelMapper.java
  18. 34
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/ReadingHistoryMapper.java
  19. 40
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/StoryOptionMapper.java
  20. 25
    4
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/VoteRecordMapper.java
  21. 1
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/AdService.java
  22. 9
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/NovelSearchService.java
  23. 3
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/NovelService.java
  24. 9
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/ReadingHistoryService.java
  25. 29
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/VoteService.java
  26. 67
    6
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/FinanceServiceImpl.java
  27. 81
    1
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/NovelSearchServiceImpl.java
  28. 20
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/NovelServiceImpl.java
  29. 60
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/ReadingHistoryServiceImpl.java
  30. 78
    8
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/VoteServiceImpl.java
  31. 8
    0
      RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/utils/Constants.java
  32. 6
    0
      RuoYi-Vue/ruoyi-novel/src/main/resources/mapper/novel/VoteRecordMapper.xml

+ 7
- 1
RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml Ver fichero

@@ -89,6 +89,10 @@ spring:
89 89
         max-active: 8
90 90
         # #连接池最大阻塞等待时间(使用负值表示没有限制)
91 91
         max-wait: -1ms
92
+  elasticsearch:
93
+    uris: http://localhost:9200 # ES地址
94
+    connection-timeout: 3000
95
+    socket-timeout: 5000
92 96
 # PHP系统配置
93 97
 php:
94 98
   data:
@@ -147,9 +151,11 @@ mybatis-plus:
147 151
   global-config:
148 152
     db-config:
149 153
       table-prefix: novel_  # 自动为所有实体类添加表前缀:cite[5]:cite[9]
150
-      id-type: assign_id  # 雪花算法(避免每个实体类单独配@TableId):cite[5]:cite[9]
154
+      id-type: auto  # 雪花算法(避免每个实体类单独配@TableId):cite[5]:cite[9]
151 155
   configuration:
156
+    default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler
152 157
     map-underscore-to-camel-case: true
158
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
153 159
 # PageHelper分页插件
154 160
 pagehelper:
155 161
   helperDialect: mysql

+ 8
- 0
RuoYi-Vue/ruoyi-common/src/main/java/com/ruoyi/common/exception/ServiceException.java Ver fichero

@@ -37,7 +37,15 @@ public final class ServiceException extends RuntimeException
37 37
     {
38 38
         this.message = message;
39 39
     }
40
+    // 添加支持异常链的构造函数
41
+    public ServiceException(String message, Throwable cause) {
42
+        super(message, cause);
43
+    }
40 44
 
45
+//    public ServiceException(String message, Integer code) {
46
+//        super(message);
47
+//        this.code = code;
48
+//    }
41 49
     public ServiceException(String message, Integer code)
42 50
     {
43 51
         this.message = message;

+ 6
- 0
RuoYi-Vue/ruoyi-novel/pom.xml Ver fichero

@@ -39,12 +39,18 @@
39 39
             <groupId>com.ruoyi</groupId>
40 40
             <artifactId>ruoyi-common</artifactId>
41 41
         </dependency>
42
+        <!-- Elasticsearch -->
43
+        <dependency>
44
+            <groupId>org.springframework.boot</groupId>
45
+            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
46
+        </dependency>
42 47
         <!-- MyBatis-Plus 核心依赖 -->
43 48
         <dependency>
44 49
             <groupId>com.baomidou</groupId>
45 50
             <artifactId>mybatis-plus-boot-starter</artifactId>
46 51
             <version>3.4.1</version> <!-- 2023年稳定版,兼容Spring Boot 2.7.x -->
47 52
         </dependency>
53
+        <!-- 长文本处理 -->
48 54
 
49 55
         <!-- 注解处理器(避免IDE报错) -->
50 56
         <dependency>

+ 16
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/config/MybatisPlusConfig.java Ver fichero

@@ -0,0 +1,16 @@
1
+package com.ruoyi.novel.config;
2
+
3
+import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
4
+import org.mybatis.spring.annotation.MapperScan;
5
+import org.springframework.context.annotation.Bean;
6
+import org.springframework.context.annotation.Configuration;
7
+
8
+@Configuration
9
+@MapperScan("com.ruoyi.novel.mapper")
10
+public class MybatisPlusConfig {
11
+
12
+    @Bean
13
+    public PaginationInterceptor paginationInterceptor() {
14
+        return new PaginationInterceptor();
15
+    }
16
+}

+ 21
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/config/TableSupport.java Ver fichero

@@ -0,0 +1,21 @@
1
+package com.ruoyi.novel.config;
2
+
3
+import com.ruoyi.common.core.page.PageDomain;
4
+import com.ruoyi.common.utils.ServletUtils;
5
+import com.ruoyi.novel.utils.Constants;
6
+
7
+public class TableSupport {
8
+
9
+    public static PageDomain buildPageRequest() {
10
+        return getPageDomain();
11
+    }
12
+
13
+    public static PageDomain getPageDomain() {
14
+        PageDomain pageDomain = new PageDomain();
15
+        pageDomain.setPageNum(ServletUtils.getParameterToInt(Constants.PAGE_NUM));
16
+        pageDomain.setPageSize(ServletUtils.getParameterToInt(Constants.PAGE_SIZE));
17
+        pageDomain.setOrderByColumn(ServletUtils.getParameter(Constants.ORDER_BY_COLUMN));
18
+        pageDomain.setIsAsc(ServletUtils.getParameter(Constants.IS_ASC));
19
+        return pageDomain;
20
+    }
21
+}

+ 6
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/NovelAdController.java Ver fichero

@@ -1,6 +1,7 @@
1 1
 package com.ruoyi.novel.controller;
2 2
 //# 广告计数接口
3 3
 
4
+import com.ruoyi.common.core.domain.AjaxResult;
4 5
 import com.ruoyi.novel.domain.AdCountRequest;
5 6
 import com.ruoyi.novel.service.AdService;
6 7
 import org.springframework.beans.factory.annotation.Autowired;
@@ -60,7 +61,11 @@ public class NovelAdController {
60 61
 
61 62
         return ResponseEntity.ok("Ad counted");
62 63
     }
63
-
64
+    @PostMapping("/count")
65
+    public AjaxResult logAdView(@RequestBody AdCountRequest request) {
66
+        adService.logAdView(request);
67
+        return AjaxResult.success("广告计数成功");
68
+    }
64 69
     private String resolveUserId(AdCountRequest request, String phpSessionId) {
65 70
         // 优先使用PHP会话
66 71
         if (phpSessionId != null) {

+ 37
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/NovelController.java Ver fichero

@@ -8,6 +8,8 @@ import com.ruoyi.common.core.page.PageDomain;
8 8
 import com.ruoyi.common.core.page.TableDataInfo;
9 9
 import com.ruoyi.common.core.page.TableSupport;
10 10
 import com.ruoyi.novel.domain.Novel;
11
+import com.ruoyi.novel.mapper.NovelMapper;
12
+import com.ruoyi.novel.service.NovelSearchService;
11 13
 import com.ruoyi.novel.service.NovelService;
12 14
 import org.springframework.beans.factory.annotation.Autowired;
13 15
 import org.springframework.web.bind.annotation.*;
@@ -21,7 +23,10 @@ public class NovelController {
21 23
 
22 24
     @Autowired
23 25
     private NovelService novelService;
24
-
26
+    @Autowired
27
+    private NovelSearchService searchService;
28
+    @Autowired
29
+    private NovelMapper novelMapper;
25 30
     @GetMapping("/list")
26 31
     public TableDataInfo list(Novel novel) {
27 32
         startPage();
@@ -33,6 +38,11 @@ public class NovelController {
33 38
     public AjaxResult add(@RequestBody Novel novel) {
34 39
         return toAjax(novelService.insertNovel(novel));
35 40
     }
41
+    @DeleteMapping("/{ids}")
42
+    public AjaxResult remove(@PathVariable Long[] ids) {
43
+        return toAjax(novelService.deleteNovelByIds(ids));
44
+    }
45
+
36 46
     private AjaxResult toAjax(int rows) {
37 47
         return rows > 0 ? AjaxResult.success() : AjaxResult.error();
38 48
     }
@@ -53,4 +63,30 @@ public class NovelController {
53 63
         return rspData;
54 64
     }
55 65
     // 其他接口...
66
+    @GetMapping("/search")
67
+    public AjaxResult searchNovels(@RequestParam String keyword) {
68
+        List<Novel> novels = searchService.searchNovels(keyword);
69
+        return AjaxResult.success(novels);
70
+    }
71
+
72
+    @PostMapping
73
+    public AjaxResult addNovel(@RequestBody Novel novel) {
74
+        novelMapper.insert(novel);
75
+        searchService.indexNovel(novel); // 创建索引
76
+        return AjaxResult.success();
77
+    }
78
+
79
+    @PutMapping
80
+    public AjaxResult updateNovel(@RequestBody Novel novel) {
81
+        novelMapper.updateById(novel);
82
+        searchService.updateNovelIndex(novel); // 更新索引
83
+        return AjaxResult.success();
84
+    }
85
+
86
+    @DeleteMapping("/{id}")
87
+    public AjaxResult deleteNovel(@PathVariable Long id) {
88
+        novelMapper.deleteById(id);
89
+        searchService.deleteNovelIndex(id); // 删除索引
90
+        return AjaxResult.success();
91
+    }
56 92
 }

+ 5
- 2
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/NovelVoteController.java Ver fichero

@@ -32,9 +32,12 @@ public class NovelVoteController {
32 32
                 request.getUserId(),
33 33
                 k -> RateLimiter.create(MAX_VOTES_PER_DAY)
34 34
         );
35
-
35
+        // 1. 检查用户投票频率
36
+//        if (!voteService.canUserVote(request.getUserId())) {
37
+//            return AjaxResult.error("投票过于频繁,请稍后再试");
38
+//        }
36 39
         if (!rateLimiter.tryAcquire()) {
37
-            return AjaxResult.error("Exceeded daily vote limit");
40
+            return AjaxResult.error("投票过于频繁,请稍后再试");
38 41
         }
39 42
 
40 43
         // 2. 调用DeepSeek API生成剧情

+ 8
- 4
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/controller/ReadingHistoryController.java Ver fichero

@@ -2,23 +2,27 @@ package com.ruoyi.novel.controller;
2 2
 
3 3
 import com.ruoyi.common.core.domain.AjaxResult;
4 4
 import com.ruoyi.novel.domain.ReadingRecord;
5
+import com.ruoyi.novel.service.ReadingHistoryService;
6
+import org.springframework.beans.factory.annotation.Autowired;
5 7
 import org.springframework.web.bind.annotation.*;
6 8
 
7 9
 @RestController
8 10
 @RequestMapping("/history")
9 11
 public class ReadingHistoryController {
10
-
12
+    @Autowired
13
+    private ReadingHistoryService historyService;
11 14
     @PostMapping("/record")
12 15
     public AjaxResult recordReading(@RequestBody ReadingRecord record) {
13 16
         // 记录用户阅读位置
14
-        return null;
17
+        historyService.recordReading(record);
18
+        return AjaxResult.success("阅读记录已保存");
15 19
     }
16 20
 
17 21
     @GetMapping("/last/{userId}/{novelId}")
18 22
     public AjaxResult getLastPosition(
19 23
             @PathVariable Long userId,
20 24
             @PathVariable Long novelId) {
21
-        // 获取最后阅读位置
22
-        return null;
25
+        ReadingRecord record = historyService.getLastPosition(userId, novelId);
26
+        return AjaxResult.success(record);
23 27
     }
24 28
 }

+ 19
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/Novel.java Ver fichero

@@ -1,21 +1,39 @@
1 1
 package com.ruoyi.novel.domain;
2 2
 
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableName;
3 6
 import lombok.Data;
7
+import org.springframework.data.annotation.Id;
8
+import org.springframework.data.elasticsearch.annotations.Document;
9
+import org.springframework.data.elasticsearch.annotations.Field;
10
+import org.springframework.data.elasticsearch.annotations.FieldType;
4 11
 
5 12
 import java.util.Date;
6 13
 
7 14
 // Novel.java
8 15
 @Data
16
+@TableName("novel")
17
+@Document(indexName = "novels")
9 18
 public class Novel {
19
+    @Id
20
+    @TableId(type = IdType.AUTO)
10 21
     private Long id;
22
+    @Field(type = FieldType.Text, analyzer = "ik_max_word")
11 23
     private String title;
24
+    @Field(type = FieldType.Keyword)
12 25
     private String author;
13 26
     private String coverImg;
27
+    @Field(type = FieldType.Long)
14 28
     private Long categoryId;
15
-    private String status;
29
+    @Field(type = FieldType.Keyword)
30
+    private String status;// 连载/完本
31
+    @Field(type = FieldType.Text, analyzer = "ik_smart")
16 32
     private String description;
17 33
     private Long wordCount;
34
+    @Field(type = FieldType.Long)
18 35
     private Long readCount;
36
+    @Field(type = FieldType.Date)
19 37
     private Date createTime;
20 38
     private Date updateTime;
21 39
 }

+ 5
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/NovelChapter.java Ver fichero

@@ -1,12 +1,17 @@
1 1
 package com.ruoyi.novel.domain;
2 2
 
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableName;
3 6
 import lombok.Data;
4 7
 
5 8
 import java.util.Date;
6 9
 
7 10
 // NovelChapter.java
8 11
 @Data
12
+@TableName("novel_chapter")
9 13
 public class NovelChapter {
14
+    @TableId(type = IdType.AUTO)
10 15
     private Long id;
11 16
     private Long novelId;
12 17
     private String chapterTitle;

+ 4
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/NovelContent.java Ver fichero

@@ -1,10 +1,14 @@
1 1
 package com.ruoyi.novel.domain;
2 2
 
3
+import com.baomidou.mybatisplus.annotation.TableId;
4
+import com.baomidou.mybatisplus.annotation.TableName;
3 5
 import lombok.Data;
4 6
 
5 7
 // NovelContent.java
6 8
 @Data
9
+@TableName("novel_content")
7 10
 public class NovelContent {
11
+    @TableId
8 12
     private Long chapterId;
9 13
     private String content;
10 14
 }

+ 6
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/ReadingRecord.java Ver fichero

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
5 5
 import com.baomidou.mybatisplus.annotation.TableName;
6 6
 import lombok.Data;
7 7
 
8
+import javax.validation.constraints.NotNull;
8 9
 import java.util.Date;
9 10
 
10 11
 @Data
@@ -12,8 +13,13 @@ import java.util.Date;
12 13
 public class ReadingRecord {
13 14
     @TableId(type = IdType.AUTO)
14 15
     private Long id;
16
+    @NotNull(message = "用户ID不能为空")
15 17
     private Long userId;
18
+
19
+    @NotNull(message = "章节ID不能为空")
16 20
     private Long chapterId;
21
+
22
+    @NotNull(message = "小说ID不能为空")
17 23
     private Long novelId;
18 24
     private Integer progress;
19 25
     private Long position;

+ 2
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/StoryOption.java Ver fichero

@@ -10,7 +10,8 @@ import javax.validation.constraints.NotNull;
10 10
 
11 11
 // StoryOption.java
12 12
 @Data
13
-@TableName("novel_story_option")
13
+//@TableName("novel_story_option")
14
+@TableName("story_option")
14 15
 public class StoryOption {
15 16
     @TableId(type = IdType.AUTO)
16 17
     private Long id;

+ 1
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/domain/VoteRecord.java Ver fichero

@@ -1,5 +1,6 @@
1 1
 package com.ruoyi.novel.domain;
2 2
 
3
+import com.baomidou.mybatisplus.annotation.*;
3 4
 import lombok.Data;
4 5
 
5 6
 import java.util.Date;
@@ -19,7 +20,6 @@ public class VoteRecord {
19 20
     @TableField(fill = FieldFill.INSERT)
20 21
     private Date voteTime;
21 22
 
22
-    @Column(typeHandler = LongTextTypeHandler.class)
23 23
     private String generatedContent;
24 24
 
25 25
     private Integer notifyStatus; // 0-未通知 1-已通知

+ 45
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/exception/GlobalExceptionHandler.java Ver fichero

@@ -0,0 +1,45 @@
1
+package com.ruoyi.novel.exception;
2
+
3
+import com.ruoyi.common.core.domain.AjaxResult;
4
+import com.ruoyi.common.exception.ServiceException;
5
+import com.ruoyi.novel.service.impl.ReadingHistoryServiceImpl;
6
+import org.slf4j.Logger;
7
+import org.slf4j.LoggerFactory;
8
+import org.springframework.web.bind.MethodArgumentNotValidException;
9
+import org.springframework.web.bind.annotation.ExceptionHandler;
10
+import org.springframework.web.bind.annotation.RestControllerAdvice;
11
+
12
+
13
+// 新增全局异常处理器
14
+@RestControllerAdvice
15
+public class GlobalExceptionHandler {
16
+
17
+    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
18
+
19
+    /**
20
+     * 业务异常处理
21
+     */
22
+    @ExceptionHandler(ServiceException.class)
23
+    public AjaxResult handleServiceException(ServiceException e) {
24
+        logger.error(e.getMessage(), e);
25
+        return AjaxResult.error(e.getMessage());
26
+    }
27
+
28
+    /**
29
+     * 参数校验异常
30
+     */
31
+    @ExceptionHandler(MethodArgumentNotValidException.class)
32
+    public AjaxResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
33
+        String message = e.getBindingResult().getFieldError().getDefaultMessage();
34
+        return AjaxResult.error(message);
35
+    }
36
+
37
+    /**
38
+     * 系统异常
39
+     */
40
+    @ExceptionHandler(Exception.class)
41
+    public AjaxResult handleException(Exception e) {
42
+        logger.error(e.getMessage(), e);
43
+        return AjaxResult.error("系统繁忙,请稍后再试");
44
+    }
45
+}

+ 18
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/NovelMapper.java Ver fichero

@@ -1,15 +1,32 @@
1 1
 package com.ruoyi.novel.mapper;
2 2
 
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
3 4
 import com.ruoyi.novel.domain.Novel;
5
+import org.apache.ibatis.annotations.Param;
6
+import org.apache.ibatis.annotations.Select;
7
+import org.apache.ibatis.annotations.Update;
4 8
 
5 9
 import java.util.List;
6 10
 
7 11
 // NovelMapper.java
8
-public interface NovelMapper {
12
+public interface NovelMapper extends BaseMapper<Novel> {
9 13
     List<Novel> selectNovelList(Novel novel);
10 14
     Novel selectNovelById(Long id);
11 15
     int insertNovel(Novel novel);
12 16
     int updateNovel(Novel novel);
13 17
     int deleteNovelByIds(Long[] ids);
18
+
19
+    // 以下方法已由MyBatis-Plus实现,无需额外代码
20
+    // int updateById(Novel novel);
21
+    // int deleteBatchIds(@Param("ids") List<Long> ids);
22
+    // Novel selectById(Long id);
23
+
24
+    // 自定义方法:根据分类ID查询小说
25
+    @Select("SELECT * FROM novel WHERE category_id = #{categoryId}")
26
+    List<Novel> selectByCategoryId(Long categoryId);
27
+
28
+    // 自定义方法:更新阅读量
29
+    @Update("UPDATE novel SET read_count = read_count + #{increment} WHERE id = #{id}")
30
+    int incrementReadCount(@Param("id") Long id, @Param("increment") Long increment);
14 31
 }
15 32
 

+ 34
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/ReadingHistoryMapper.java Ver fichero

@@ -0,0 +1,34 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.ruoyi.novel.domain.ReadingRecord;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+import org.apache.ibatis.annotations.Update;
9
+
10
+// ReadingHistoryMapper.java
11
+@Mapper
12
+public interface ReadingHistoryMapper extends BaseMapper<ReadingRecord> {
13
+
14
+    // 根据用户和章节查询记录
15
+    @Select("SELECT * FROM reading_history " +
16
+            "WHERE user_id = #{userId} AND chapter_id = #{chapterId}")
17
+    ReadingRecord selectByUserAndChapter(@Param("userId") Long userId,
18
+                                         @Param("chapterId") Long chapterId);
19
+
20
+    // 获取用户最后阅读位置
21
+    @Select("SELECT * FROM reading_history " +
22
+            "WHERE user_id = #{userId} AND novel_id = #{novelId} " +
23
+            "ORDER BY last_read_time DESC LIMIT 1")
24
+    ReadingRecord selectLastPosition(@Param("userId") Long userId,
25
+                                     @Param("novelId") Long novelId);
26
+
27
+    // 更新阅读进度
28
+    @Update("UPDATE reading_history SET " +
29
+            "progress = #{progress}, " +
30
+            "position = #{position}, " +
31
+            "last_read_time = NOW() " +
32
+            "WHERE id = #{id}")
33
+    int updateReadingProgress(ReadingRecord record);
34
+}

+ 40
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/StoryOptionMapper.java Ver fichero

@@ -0,0 +1,40 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.ruoyi.novel.domain.StoryOption;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+import org.apache.ibatis.annotations.Update;
9
+
10
+import java.util.List;
11
+
12
+// StoryOptionMapper.java
13
+@Mapper
14
+public interface StoryOptionMapper extends BaseMapper<StoryOption> {
15
+
16
+    // 增加选项投票数
17
+    @Update("UPDATE story_option SET vote_count = vote_count + 1 WHERE id = #{optionId}")
18
+    int incrementVoteCount(@Param("optionId") Long optionId);
19
+
20
+    // 根据章节ID获取选项
21
+    @Select("SELECT * FROM story_option WHERE chapter_id = #{chapterId} ORDER BY sort_order")
22
+    List<StoryOption> selectByChapterId(Long chapterId);
23
+
24
+    // 批量更新选项投票数
25
+    @Update("<script>" +
26
+            "UPDATE story_option SET vote_count = CASE id " +
27
+            "<foreach collection='options' item='opt'>" +
28
+            "WHEN #{opt.id} THEN #{opt.voteCount} " +
29
+            "</foreach>" +
30
+            "END WHERE id IN " +
31
+            "<foreach collection='options' item='opt' open='(' separator=',' close=')'>" +
32
+            "#{opt.id}" +
33
+            "</foreach>" +
34
+            "</script>")
35
+    int batchUpdateVoteCount(@Param("options") List<StoryOption> options);
36
+
37
+    @Update("UPDATE story_option SET vote_count = vote_count + 1 WHERE id = #{optionId}")
38
+    int incrementOptionVote(@Param("optionId") Long optionId);
39
+
40
+}

+ 25
- 4
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/mapper/VoteRecordMapper.java Ver fichero

@@ -2,10 +2,8 @@ package com.ruoyi.novel.mapper;
2 2
 
3 3
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4 4
 import com.ruoyi.novel.domain.VoteRecord;
5
-import org.apache.ibatis.annotations.Mapper;
6
-import org.apache.ibatis.annotations.Param;
7
-import org.apache.ibatis.annotations.Select;
8
-import org.apache.ibatis.annotations.Update;
5
+import lombok.Data;
6
+import org.apache.ibatis.annotations.*;
9 7
 
10 8
 import java.util.List;
11 9
 
@@ -22,4 +20,27 @@ public interface VoteRecordMapper extends BaseMapper<VoteRecord> {
22 20
 
23 21
     @Select("SELECT * FROM novel_vote_log WHERE notify_status = 0 ORDER BY vote_time ASC LIMIT 50")
24 22
     List<VoteRecord> selectUnnotifiedRecords();
23
+
24
+
25
+    // 插入投票记录(带生成内容)
26
+    @Insert("INSERT INTO vote_record (user_id, story_id, option_id, vote_time, generated_content) " +
27
+            "VALUES (#{userId}, #{storyId}, #{optionId}, NOW(), #{generatedContent})")
28
+    @Options(useGeneratedKeys = true, keyProperty = "id")
29
+    int insertVoteRecord(VoteRecord record);
30
+
31
+    // 获取用户投票记录
32
+    @Select("SELECT * FROM vote_record WHERE user_id = #{userId} ORDER BY vote_time DESC")
33
+    List<VoteRecord> selectByUserId(Long userId);
34
+
35
+    // 获取故事投票统计
36
+    @Select("SELECT option_id, COUNT(*) AS vote_count FROM vote_record " +
37
+            "WHERE story_id = #{storyId} GROUP BY option_id")
38
+    List<VoteStats> getVoteStatsByStory(Long storyId);
39
+
40
+    // 投票统计DTO
41
+    @Data
42
+    class VoteStats {
43
+        private Long optionId;
44
+        private Integer voteCount;
45
+    }
25 46
 }

+ 1
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/AdService.java Ver fichero

@@ -8,7 +8,7 @@ import java.util.List;
8 8
 @Service
9 9
 // AdService.java
10 10
 public interface AdService {
11
-    //void logAdView(AdCountRequest request);
11
+    void logAdView(AdCountRequest request);
12 12
     List<AdLog> getAdLogsByUser(Long userId);
13 13
 
14 14
     void logAdView(String userId, Long chapterId, String adType, String source);

+ 9
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/NovelSearchService.java Ver fichero

@@ -1,4 +1,13 @@
1 1
 package com.ruoyi.novel.service;
2 2
 
3
+import com.ruoyi.novel.domain.Novel;
4
+
5
+import java.util.List;
6
+
7
+// NovelSearchService.java
3 8
 public interface NovelSearchService {
9
+    List<Novel> searchNovels(String keyword);
10
+    void indexNovel(Novel novel);
11
+    void updateNovelIndex(Novel novel);
12
+    void deleteNovelIndex(Long id);
4 13
 }

+ 3
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/NovelService.java Ver fichero

@@ -10,4 +10,7 @@ public interface NovelService {
10 10
 
11 11
     @Transactional
12 12
     int insertNovel(Novel novel);
13
+    int updateNovel(Novel novel);
14
+    int deleteNovelByIds(Long[] ids);
15
+    Novel selectNovelById(Long id);
13 16
 }

+ 9
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/ReadingHistoryService.java Ver fichero

@@ -0,0 +1,9 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.ReadingRecord;
4
+
5
+// ReadingHistoryService.java
6
+public interface ReadingHistoryService {
7
+    void recordReading(ReadingRecord record);
8
+    ReadingRecord getLastPosition(Long userId, Long novelId);
9
+}

+ 29
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/VoteService.java Ver fichero

@@ -0,0 +1,29 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.VoteRecord;
4
+import com.ruoyi.novel.domain.VoteRequest;
5
+import org.springframework.scheduling.annotation.Async;
6
+import org.springframework.scheduling.annotation.Scheduled;
7
+
8
+// VoteService.java
9
+public interface VoteService {
10
+    String processVote(VoteRequest request);
11
+
12
+    String generateStoryContent(Long storyId, Long optionId);
13
+
14
+    @Async("voteTaskExecutor")
15
+    void generateStoryContent(VoteRecord record);
16
+
17
+    void notifyPhpSystem(VoteRequest request, String content);
18
+
19
+    // 添加方法实现
20
+    void incrementOptionVote(Long optionId);
21
+
22
+    boolean canUserVote(Long userId);
23
+
24
+    @Async("voteTaskExecutor")
25
+    void notifyPhpSystem(VoteRecord record);
26
+
27
+    @Scheduled(cron = "0 */10 * * * ?") // 每10分钟执行一次
28
+    void retryFailedNotifications();
29
+}

+ 67
- 6
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/FinanceServiceImpl.java Ver fichero

@@ -1,20 +1,27 @@
1 1
 package com.ruoyi.novel.service.impl;
2 2
 
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
3 4
 import com.ruoyi.common.exception.ServiceException;
5
+import com.ruoyi.novel.domain.AdLog;
4 6
 import com.ruoyi.novel.domain.RoyaltyReport;
5 7
 import com.ruoyi.novel.domain.RoyaltySummary;
8
+import com.ruoyi.novel.mapper.AdLogMapper;
6 9
 import com.ruoyi.novel.mapper.RoyaltySummaryMapper;
10
+import com.ruoyi.novel.service.IAdService;
7 11
 import com.ruoyi.novel.service.IFinanceService;
8 12
 
9 13
 import org.slf4j.Logger;
10 14
 import org.slf4j.LoggerFactory;
11 15
 import org.springframework.beans.factory.annotation.Autowired;
12 16
 import org.springframework.beans.factory.annotation.Value;
17
+import org.springframework.http.*;
13 18
 import org.springframework.scheduling.annotation.Scheduled;
14 19
 import org.springframework.stereotype.Service;
20
+import org.springframework.web.client.RestTemplate;
15 21
 
16 22
 import java.math.BigDecimal;
17 23
 import java.util.ArrayList;
24
+import java.util.Arrays;
18 25
 import java.util.List;
19 26
 
20 27
 // FinanceServiceImpl.java
@@ -30,6 +37,15 @@ public class FinanceServiceImpl implements IFinanceService {
30 37
     private BigDecimal handlingFeeRate;
31 38
     @Value("${royalty.rate}")
32 39
     private BigDecimal royaltyRate;
40
+    // 添加配置项注入
41
+    @Value("${php.data.url}")
42
+    private String phpDataUrl;
43
+
44
+    @Autowired
45
+    private AdLogMapper adLogMapper; // 确保已注入
46
+
47
+    @Autowired
48
+    private IAdService adService; // 确保已注入
33 49
 
34 50
     //@Value("${handling.fee.rate}")
35 51
     //private BigDecimal handlingFeeRate;
@@ -101,15 +117,60 @@ public class FinanceServiceImpl implements IFinanceService {
101 117
         }
102 118
     }
103 119
 
120
+//    private List<RoyaltySummary> fetchFromPhpSystem() {
121
+//        // 实现从PHP系统获取数据的逻辑
122
+//        // 返回模拟数据
123
+//        List<RoyaltySummary> list = new ArrayList<>();
124
+//        list.add(new RoyaltySummary(1001L, "2023-10", new BigDecimal("15000.00")));
125
+//        list.add(new RoyaltySummary(1002L, "2023-10", new BigDecimal("8500.00")));
126
+//        return list;
127
+//    }
128
+
129
+    // 在FinanceServiceImpl中补充完整数据同步
104 130
     private List<RoyaltySummary> fetchFromPhpSystem() {
105
-        // 实现从PHP系统获取数据的逻辑
106
-        // 返回模拟数据
107
-        List<RoyaltySummary> list = new ArrayList<>();
108
-        list.add(new RoyaltySummary(1001L, "2023-10", new BigDecimal("15000.00")));
109
-        list.add(new RoyaltySummary(1002L, "2023-10", new BigDecimal("8500.00")));
110
-        return list;
131
+        try {
132
+            // 使用RestTemplate从PHP系统获取数据
133
+            RestTemplate restTemplate = new RestTemplate();
134
+
135
+            // 设置认证头
136
+            HttpHeaders headers = new HttpHeaders();
137
+            headers.setBasicAuth("api_user", "api_password");
138
+
139
+            // 构建请求
140
+            HttpEntity<String> entity = new HttpEntity<>(headers);
141
+            ResponseEntity<RoyaltySummary[]> response = restTemplate.exchange(
142
+                    phpDataUrl + "/royalty/summary",
143
+                    HttpMethod.GET,
144
+                    entity,
145
+                    RoyaltySummary[].class
146
+            );
147
+
148
+            if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
149
+                return Arrays.asList(response.getBody());
150
+            }
151
+            throw new ServiceException("PHP系统返回错误状态码: " + response.getStatusCode());
152
+        } catch (Exception e) {
153
+            logger.error("从PHP系统获取分账数据失败: {}", e.getMessage());
154
+            throw new ServiceException("数据同步服务暂时不可用", e);
155
+        }
111 156
     }
112 157
 
158
+    // 定时任务重试机制
159
+    @Scheduled(fixedDelayString = "${scheduled.ad-retry-interval}")
160
+    public void retryFailedAdSync() {
161
+        try {
162
+            List<AdLog> failedLogs = adLogMapper.selectList(
163
+                    new QueryWrapper<AdLog>().eq("sync_status", 2) // 2=失败
164
+            );
165
+
166
+            for (AdLog log : failedLogs) {
167
+                logger.info("重试广告同步: ID={}, 平台={}", log.getId(), log.getAdPlatform());
168
+                adService.syncWithAdPlatform(log);
169
+            }
170
+        } catch (Exception e) {
171
+            logger.error("广告同步重试任务失败: {}", e.getMessage());
172
+        }
173
+    }
113 174
     @Override
114 175
     public BigDecimal getRoyaltyRate(Long authorId) {
115 176
         // 实际应根据作者等级计算

+ 81
- 1
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/NovelSearchServiceImpl.java Ver fichero

@@ -1,23 +1,103 @@
1 1
 package com.ruoyi.novel.service.impl;
2 2
 
3 3
 import com.ruoyi.novel.domain.Novel;
4
+import com.ruoyi.novel.mapper.NovelMapper;
4 5
 import com.ruoyi.novel.service.NovelSearchService;
6
+import org.elasticsearch.index.query.QueryBuilders;
7
+import org.slf4j.Logger;
8
+import org.slf4j.LoggerFactory;
5 9
 import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
11
+import org.springframework.data.elasticsearch.core.IndexOperations;
12
+import org.springframework.data.elasticsearch.core.SearchHit;
13
+import org.springframework.data.elasticsearch.core.SearchHits;
14
+import org.springframework.data.elasticsearch.core.document.Document;
15
+import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
16
+import org.springframework.data.elasticsearch.core.query.*;
17
+import org.springframework.scheduling.annotation.Scheduled;
6 18
 import org.springframework.stereotype.Service;
7 19
 
20
+import javax.annotation.PostConstruct;
21
+import java.util.HashMap;
8 22
 import java.util.List;
23
+import java.util.Map;
24
+import java.util.stream.Collectors;
9 25
 
10 26
 // NovelSearchService.java
11 27
 @Service
12 28
 public class NovelSearchServiceImpl implements NovelSearchService {
29
+    private static final Logger logger = LoggerFactory.getLogger(NovelSearchServiceImpl.class);
13 30
 
14 31
     @Autowired
15 32
     private ElasticsearchRestTemplate elasticTemplate;
33
+    @Autowired
34
+    private NovelMapper novelMapper;
35
+
36
+    private static final String INDEX_NAME = "novels";
16 37
 
38
+    @PostConstruct
39
+    public void createIndexIfNotExists() {
40
+        IndexOperations indexOps = elasticTemplate.indexOps(IndexCoordinates.of(INDEX_NAME));
41
+        if (!indexOps.exists()) {
42
+            // 创建索引
43
+            indexOps.create();
44
+
45
+            // 创建映射
46
+            Document mapping = indexOps.createMapping(Novel.class);
47
+            indexOps.putMapping(mapping);
48
+        }
49
+//        if (!elasticTemplate.indexExists(INDEX_NAME)) {
50
+//            elasticTemplate.createIndex(INDEX_NAME);
51
+//            elasticTemplate.putMapping(Novel.class);
52
+//        }
53
+    }
17 54
     public List<Novel> searchNovels(String keyword) {
55
+        // 构建查询条件
18 56
         NativeSearchQuery query = new NativeSearchQueryBuilder()
19 57
                 .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "author", "description"))
20 58
                 .build();
21
-        return elasticTemplate.search(query, Novel.class).getContent();
59
+        // 执行搜索
60
+        SearchHits<Novel> hits = elasticTemplate.search(query, Novel.class);
61
+        return hits.stream()
62
+                .map(SearchHit::getContent)
63
+                .collect(Collectors.toList());
64
+
65
+        //return elasticTemplate.search(query, Novel.class).getContent();
66
+    }
67
+
68
+    @Override
69
+    public void indexNovel(Novel novel) {
70
+        IndexQuery indexQuery = new IndexQueryBuilder()
71
+                .withId(novel.getId().toString())
72
+                .withObject(novel)
73
+                .build();
74
+        elasticTemplate.index(indexQuery, IndexCoordinates.of(INDEX_NAME));
75
+    }
76
+    @Override
77
+    public void updateNovelIndex(Novel novel) {
78
+        UpdateQuery updateQuery = UpdateQuery.builder(novel.getId().toString())
79
+                .withDocument(Document.from(convertNovelToMap(novel)))
80
+                .build();
81
+        elasticTemplate.update(updateQuery, IndexCoordinates.of(INDEX_NAME));
82
+    }
83
+    private Map<String, Object> convertNovelToMap(Novel novel) {
84
+        Map<String, Object> map = new HashMap<>();
85
+        map.put("title", novel.getTitle());
86
+        map.put("author", novel.getAuthor());
87
+        map.put("description", novel.getDescription());
88
+        map.put("categoryId", novel.getCategoryId());
89
+        map.put("status", novel.getStatus());
90
+        return map;
91
+    }
92
+    @Override
93
+    public void deleteNovelIndex(Long id) {
94
+        elasticTemplate.delete(id.toString(), IndexCoordinates.of(INDEX_NAME));
95
+    }
96
+    // 定时同步数据库数据到ES
97
+    @Scheduled(fixedRate = 3600000) // 每小时同步一次
98
+    public void syncDataToEs() {
99
+        List<Novel> novels = novelMapper.selectList(null);
100
+        novels.forEach(this::indexNovel);
101
+        logger.info("同步 {} 条小说数据到ES", novels.size());
22 102
     }
23 103
 }

+ 20
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/NovelServiceImpl.java Ver fichero

@@ -3,16 +3,20 @@ package com.ruoyi.novel.service.impl;
3 3
 import com.ruoyi.novel.domain.Novel;
4 4
 import com.ruoyi.novel.mapper.NovelMapper;
5 5
 import com.ruoyi.novel.service.NovelService;
6
+import org.slf4j.Logger;
7
+import org.slf4j.LoggerFactory;
6 8
 import org.springframework.beans.factory.annotation.Autowired;
7 9
 import org.springframework.stereotype.Service;
8 10
 import org.springframework.transaction.annotation.Transactional;
9 11
 
12
+import java.util.Arrays;
10 13
 import java.util.Date;
11 14
 import java.util.List;
12 15
 
13 16
 // NovelServiceImpl.java
14 17
 @Service
15 18
 public class NovelServiceImpl implements NovelService {
19
+    private static final Logger logger = LoggerFactory.getLogger(NovelServiceImpl.class);
16 20
 
17 21
     @Autowired
18 22
     private NovelMapper novelMapper;
@@ -29,5 +33,21 @@ public class NovelServiceImpl implements NovelService {
29 33
         return novelMapper.insertNovel(novel);
30 34
     }
31 35
 
36
+    @Override
37
+    public int updateNovel(Novel novel) {
38
+        novel.setUpdateTime(new Date());
39
+        return novelMapper.updateById(novel);
40
+    }
41
+
42
+    @Override
43
+    public int deleteNovelByIds(Long[] ids) {
44
+        return novelMapper.deleteBatchIds(Arrays.asList(ids));
45
+    }
46
+
47
+    @Override
48
+    public Novel selectNovelById(Long id) {
49
+        return novelMapper.selectById(id);
50
+    }
51
+
32 52
     // 其他CRUD方法...
33 53
 }

+ 60
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/ReadingHistoryServiceImpl.java Ver fichero

@@ -0,0 +1,60 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.ruoyi.novel.domain.ReadingRecord;
5
+import com.ruoyi.novel.mapper.ReadingHistoryMapper;
6
+import com.ruoyi.novel.service.ReadingHistoryService;
7
+import org.slf4j.Logger;
8
+import org.slf4j.LoggerFactory;
9
+import org.springframework.beans.factory.annotation.Autowired;
10
+import org.springframework.stereotype.Service;
11
+import org.springframework.transaction.annotation.Transactional;
12
+
13
+import java.util.Date;
14
+
15
+// ReadingHistoryServiceImpl.java
16
+@Service
17
+public class ReadingHistoryServiceImpl implements ReadingHistoryService {
18
+    private static final Logger logger = LoggerFactory.getLogger(ReadingHistoryServiceImpl.class);
19
+
20
+    @Autowired
21
+    private ReadingHistoryMapper historyMapper;
22
+
23
+    @Override
24
+    @Transactional
25
+    public void recordReading(ReadingRecord record) {
26
+        record.setLastReadTime(new Date());
27
+
28
+        // 检查是否已存在记录
29
+        ReadingRecord existing = historyMapper.selectOne(
30
+                new QueryWrapper<ReadingRecord>()
31
+                        .eq("user_id", record.getUserId())
32
+                        .eq("chapter_id", record.getChapterId())
33
+        );
34
+
35
+        if (existing != null) {
36
+            // 更新现有记录
37
+            existing.setProgress(record.getProgress());
38
+            existing.setPosition(record.getPosition());
39
+            existing.setLastReadTime(record.getLastReadTime());
40
+            historyMapper.updateById(existing);
41
+        } else {
42
+            // 插入新记录
43
+            historyMapper.insert(record);
44
+        }
45
+    }
46
+
47
+
48
+
49
+
50
+    @Override
51
+    public ReadingRecord getLastPosition(Long userId, Long novelId) {
52
+        return historyMapper.selectOne(
53
+                new QueryWrapper<ReadingRecord>()
54
+                        .eq("user_id", userId)
55
+                        .eq("novel_id", novelId)
56
+                        .orderByDesc("last_read_time")
57
+                        .last("LIMIT 1")
58
+        );
59
+    }
60
+}

+ 78
- 8
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/service/impl/VoteServiceImpl.java Ver fichero

@@ -1,18 +1,34 @@
1 1
 package com.ruoyi.novel.service.impl;
2 2
 
3
+import com.google.common.util.concurrent.RateLimiter;
4
+import com.ruoyi.common.exception.ServiceException;
3 5
 import com.ruoyi.novel.domain.VoteRecord;
4 6
 import com.ruoyi.novel.domain.VoteRequest;
7
+import com.ruoyi.novel.mapper.StoryOptionMapper;
8
+import com.ruoyi.novel.mapper.VoteRecordMapper;
9
+import com.ruoyi.novel.service.VoteService;
10
+import org.slf4j.Logger;
11
+import org.slf4j.LoggerFactory;
12
+import org.springframework.beans.factory.annotation.Autowired;
13
+import org.springframework.beans.factory.annotation.Value;
14
+import org.springframework.http.*;
5 15
 import org.springframework.scheduling.annotation.Async;
6 16
 import org.springframework.scheduling.annotation.Scheduled;
17
+import org.springframework.stereotype.Service;
18
+import org.springframework.web.client.RestClientException;
7 19
 import org.springframework.web.client.RestTemplate;
8 20
 
21
+import java.util.Date;
9 22
 import java.util.HashMap;
10 23
 import java.util.List;
11 24
 import java.util.Map;
25
+import java.util.concurrent.ConcurrentHashMap;
26
+import java.util.concurrent.atomic.AtomicInteger;
12 27
 
13 28
 // VoteServiceImpl.java
14 29
 @Service
15
-public class VoteServiceImpl implements IVoteService {
30
+public class VoteServiceImpl implements VoteService {
31
+    private static final Logger logger = LoggerFactory.getLogger(VoteServiceImpl.class);
16 32
 
17 33
     @Autowired
18 34
     private VoteRecordMapper voteRecordMapper;
@@ -20,13 +36,38 @@ public class VoteServiceImpl implements IVoteService {
20 36
     private StoryOptionMapper storyOptionMapper;
21 37
     @Value("${deepseek.api.key}")
22 38
     private String deepSeekApiKey;
23
-
39
+    @Value("${deepseek.api.url}")
40
+    private String deepSeekApiUrl; // 添加配置项注入
24 41
     @Value("${php.notify.url}")
25 42
     private String phpNotifyUrl;
43
+    @Autowired
44
+    private RestTemplate restTemplate; // 确保已注入
45
+
46
+    // 使用Guava RateLimiter实现限流
47
+    private static final ConcurrentHashMap<Long, RateLimiter> userRateLimiters = new ConcurrentHashMap<>();
48
+
26 49
     // 用户投票限流器 <userId, 计数器>
27 50
     private final Map<Long, AtomicInteger> userVoteCounters = new ConcurrentHashMap<>();
28 51
     private static final int DAILY_VOTE_LIMIT = 3;
29 52
 
53
+    // 添加方法实现
54
+    @Override
55
+    public void incrementOptionVote(Long optionId) {
56
+        storyOptionMapper.incrementVoteCount(optionId);
57
+    }
58
+    @Override
59
+    public boolean canUserVote(Long userId) {
60
+        RateLimiter rateLimiter = userRateLimiters.computeIfAbsent(
61
+                userId, k -> RateLimiter.create(3.0) // 每秒3次
62
+        );
63
+        return rateLimiter.tryAcquire();
64
+    }
65
+
66
+    @Override
67
+    public String generateStoryContent(Long storyId, Long optionId) {
68
+        // 实际应调用AI服务
69
+        return "生成的故事内容...";
70
+    }
30 71
     @Override
31 72
     public String processVote(VoteRequest request) {
32 73
         // 1. 检查当日投票次数
@@ -67,13 +108,40 @@ public class VoteServiceImpl implements IVoteService {
67 108
             // 通知PHP系统
68 109
             this.notifyPhpSystem(record);
69 110
         } catch (Exception e) {
70
-            log.error("AI生成失败: {}", e.getMessage());
111
+            logger.error("AI生成失败: {}", e.getMessage());
71 112
         }
72 113
     }
114
+    // 在VoteServiceImpl中补充完整API调用
73 115
     private String callDeepSeekApi(VoteRecord record) {
74
-        // 实现DeepSeek API调用
75
-        // 返回生成的剧情内容
76
-        return "生成的故事内容...";
116
+        try {
117
+            // 构建请求体
118
+            Map<String, Object> requestBody = new HashMap<>();
119
+            requestBody.put("story_id", record.getStoryId());
120
+            requestBody.put("option_id", record.getOptionId());
121
+            requestBody.put("user_id", record.getUserId());
122
+
123
+            // 设置请求头
124
+            HttpHeaders headers = new HttpHeaders();
125
+            headers.setContentType(MediaType.APPLICATION_JSON);
126
+            headers.setBearerAuth(deepSeekApiKey);
127
+
128
+            // 发送请求
129
+            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
130
+            ResponseEntity<Map> response = restTemplate.exchange(
131
+                    deepSeekApiUrl, HttpMethod.POST, entity, Map.class);
132
+
133
+            // 解析响应
134
+            if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
135
+                Map<String, Object> body = response.getBody();
136
+                if (body.containsKey("data") && ((Map)body.get("data")).containsKey("content")) {
137
+                    return (String) ((Map)body.get("data")).get("content");
138
+                }
139
+            }
140
+            throw new ServiceException("DeepSeek API响应格式错误");
141
+        } catch (RestClientException e) {
142
+            logger.error("DeepSeek API调用失败: {}", e.getMessage());
143
+            throw new ServiceException("AI服务暂时不可用", e);
144
+        }
77 145
     }
78 146
 
79 147
 //    @Async
@@ -97,6 +165,7 @@ public class VoteServiceImpl implements IVoteService {
97 165
 //        throw new ServiceException("Failed to generate story content");
98 166
 //    }
99 167
 
168
+
100 169
     @Async
101 170
     @Override
102 171
     public void notifyPhpSystem(VoteRequest request, String content) {
@@ -121,6 +190,7 @@ public class VoteServiceImpl implements IVoteService {
121 190
         voteRecordMapper.insertVoteRecord(record);
122 191
     }
123 192
 
193
+
124 194
     @Override
125 195
     @Async("voteTaskExecutor")
126 196
     public void notifyPhpSystem(VoteRecord record) {
@@ -143,7 +213,7 @@ public class VoteServiceImpl implements IVoteService {
143 213
                 record.setNotifyStatus(2); // 通知失败
144 214
             }
145 215
         } catch (Exception e) {
146
-            log.error("通知PHP系统失败: {}", e.getMessage());
216
+            logger.error("通知PHP系统失败: {}", e.getMessage());
147 217
             record.setNotifyStatus(2);
148 218
         }
149 219
         voteRecordMapper.updateById(record);
@@ -154,7 +224,7 @@ public class VoteServiceImpl implements IVoteService {
154 224
     public void retryFailedNotifications() {
155 225
         List<VoteRecord> failedRecords = voteRecordMapper.selectUnnotifiedRecords();
156 226
         failedRecords.forEach(record -> {
157
-            log.info("重试通知PHP系统: voteId={}", record.getId());
227
+            logger.info("重试通知PHP系统: voteId={}", record.getId());
158 228
             this.notifyPhpSystem(record);
159 229
         });
160 230
     }

+ 8
- 0
RuoYi-Vue/ruoyi-novel/src/main/java/com/ruoyi/novel/utils/Constants.java Ver fichero

@@ -0,0 +1,8 @@
1
+package com.ruoyi.novel.utils;
2
+
3
+public class Constants {
4
+    public static final String PAGE_NUM = "pageNum";
5
+    public static final String PAGE_SIZE = "pageSize";
6
+    public static final String ORDER_BY_COLUMN = "orderByColumn";
7
+    public static final String IS_ASC = "isAsc";
8
+}

+ 6
- 0
RuoYi-Vue/ruoyi-novel/src/main/resources/mapper/novel/VoteRecordMapper.xml Ver fichero

@@ -17,4 +17,10 @@
17 17
         SET vote_count = vote_count + 1
18 18
         WHERE id = #{optionId}
19 19
     </update>
20
+
21
+    <update id="updateNotifyStatus">
22
+        UPDATE novel_vote_log
23
+        SET notify_status = #{status}
24
+        WHERE id = #{id}
25
+    </update>
20 26
 </mapper>

Loading…
Cancelar
Guardar