fzzj 9 ay önce
ebeveyn
işleme
4e9691ebf6
69 değiştirilmiş dosya ile 2835 ekleme ve 3 silme
  1. 1
    1
      RuoYi-Vue/pom.xml
  2. 4
    0
      RuoYi-Vue/ruoyi-common/src/main/java/com/ruoyi/common/annotation/VipAccess.java
  3. 9
    1
      RuoYi-Vue/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
  4. 60
    1
      RuoYi-Vue/ruoyi-system/pom.xml
  5. 16
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/MybatisPlusConfig.java
  6. 36
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/PhpDataSyncTask.java
  7. 14
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/RestTemplateConfig.java
  8. 61
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/ScheduledTasks.java
  9. 21
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/TableSupport.java
  10. 4
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/ThreadPoolConfig.java
  11. 37
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/ChapterController.java
  12. 90
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelAdController.java
  13. 92
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelController.java
  14. 30
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelFinanceController.java
  15. 54
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelVoteController.java
  16. 28
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/ReadingHistoryController.java
  17. 27
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AdCountRequest.java
  18. 40
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AdLog.java
  19. 26
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AdPlatformConfig.java
  20. 15
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/ChapterDTO.java
  21. 39
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/Novel.java
  22. 25
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/NovelChapter.java
  23. 14
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/NovelContent.java
  24. 27
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/ReadingRecord.java
  25. 30
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/RoyaltyReport.java
  26. 26
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/RoyaltySummary.java
  27. 26
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/StoryOption.java
  28. 26
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/VoteRecord.java
  29. 18
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/VoteRequest.java
  30. 44
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/exception/GlobalExceptionHandler.java
  31. 20
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/AdLogMapper.java
  32. 8
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/AdPlatformConfigMapper.java
  33. 26
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelChapterMapper.java
  34. 14
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelContentMapper.java
  35. 32
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelMapper.java
  36. 34
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/ReadingHistoryMapper.java
  37. 33
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/RoyaltySummaryMapper.java
  38. 40
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/StoryOptionMapper.java
  39. 46
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/VoteRecordMapper.java
  40. 10
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdConfigService.java
  41. 9
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdLogService.java
  42. 15
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdService.java
  43. 10
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdSyncService.java
  44. 13
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/ChapterService.java
  45. 22
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/IAdService.java
  46. 13
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/IFinanceService.java
  47. 16
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/IVoteService.java
  48. 13
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/NovelSearchService.java
  49. 16
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/NovelService.java
  50. 9
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/ReadingHistoryService.java
  51. 29
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/VoteService.java
  52. 69
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdConfigServiceImpl.java
  53. 88
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdLogServiceImpl.java
  54. 130
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdServiceImpl.java
  55. 82
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdSyncServiceImpl.java
  56. 73
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/ChapterServiceImpl.java
  57. 210
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/FinanceServiceImpl.java
  58. 103
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/NovelSearchServiceImpl.java
  59. 53
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/NovelServiceImpl.java
  60. 60
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/ReadingHistoryServiceImpl.java
  61. 231
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/VoteServiceImpl.java
  62. 96
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/AdPlatformClient.java
  63. 8
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/Constants.java
  64. 71
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/JwtUtil.java
  65. 80
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/NovelUtils.java
  66. 43
    0
      RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/vip/VipAccessAspect.java
  67. 27
    0
      RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/AdLogMapper.xml
  68. 17
    0
      RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/RoyaltySummaryMapper.xml
  69. 26
    0
      RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/VoteRecordMapper.xml

+ 1
- 1
RuoYi-Vue/pom.xml Dosyayı Görüntüle

@@ -240,7 +240,7 @@
240 240
         <module>ruoyi-quartz</module>
241 241
         <module>ruoyi-generator</module>
242 242
         <module>ruoyi-common</module>
243
-        <module>ruoyi-novel</module>
243
+<!--        <module>ruoyi-novel</module>-->
244 244
     </modules>
245 245
     <packaging>pom</packaging>
246 246
 

+ 4
- 0
RuoYi-Vue/ruoyi-common/src/main/java/com/ruoyi/common/annotation/VipAccess.java Dosyayı Görüntüle

@@ -0,0 +1,4 @@
1
+package com.ruoyi.common.annotation;
2
+
3
+public class VipAccess {
4
+}

+ 9
- 1
RuoYi-Vue/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java Dosyayı Görüntüle

@@ -91,7 +91,8 @@ public class SysUser extends BaseEntity
91 91
 
92 92
     /** 角色ID */
93 93
     private Long roleId;
94
-
94
+    // 添加VIP状态字段
95
+    private String vipStatus;
95 96
     public SysUser()
96 97
     {
97 98
 
@@ -309,7 +310,14 @@ public class SysUser extends BaseEntity
309 310
     {
310 311
         this.roleId = roleId;
311 312
     }
313
+    // 添加getter/setter
314
+    public String getVipStatus() {
315
+        return vipStatus;
316
+    }
312 317
 
318
+    public void setVipStatus(String vipStatus) {
319
+        this.vipStatus = vipStatus;
320
+    }
313 321
     @Override
314 322
     public String toString() {
315 323
         return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

+ 60
- 1
RuoYi-Vue/ruoyi-system/pom.xml Dosyayı Görüntüle

@@ -7,6 +7,18 @@
7 7
         <groupId>com.ruoyi</groupId>
8 8
         <version>3.9.0</version>
9 9
     </parent>
10
+    <build>
11
+        <plugins>
12
+            <plugin>
13
+                <groupId>org.apache.maven.plugins</groupId>
14
+                <artifactId>maven-compiler-plugin</artifactId>
15
+                <configuration>
16
+                    <source>17</source>
17
+                    <target>17</target>
18
+                </configuration>
19
+            </plugin>
20
+        </plugins>
21
+    </build>
10 22
     <modelVersion>4.0.0</modelVersion>
11 23
 
12 24
     <artifactId>ruoyi-system</artifactId>
@@ -16,13 +28,60 @@
16 28
     </description>
17 29
 
18 30
     <dependencies>
19
-
31
+        <!-- AOP 必须 -->
32
+        <dependency>
33
+            <groupId>org.springframework.boot</groupId>
34
+            <artifactId>spring-boot-starter-aop</artifactId>
35
+        </dependency>
20 36
         <!-- 通用工具-->
21 37
         <dependency>
22 38
             <groupId>com.ruoyi</groupId>
23 39
             <artifactId>ruoyi-common</artifactId>
24 40
         </dependency>
41
+        <dependency>
42
+            <groupId>org.springframework</groupId>
43
+            <artifactId>spring-context</artifactId>
44
+        </dependency>
45
+        <dependency>
46
+            <groupId>org.springframework</groupId>
47
+            <artifactId>spring-web</artifactId>
48
+        </dependency>
49
+        <!-- Elasticsearch -->
50
+        <dependency>
51
+            <groupId>org.springframework.boot</groupId>
52
+            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
53
+        </dependency>
54
+        <!-- MyBatis-Plus 核心依赖 -->
55
+        <dependency>
56
+            <groupId>com.baomidou</groupId>
57
+            <artifactId>mybatis-plus-boot-starter</artifactId>
58
+            <version>3.4.1</version> <!-- 2023年稳定版,兼容Spring Boot 2.7.x -->
59
+        </dependency>
60
+        <!-- 长文本处理 -->
61
+        <dependency>
62
+            <groupId>org.projectlombok</groupId>
63
+            <artifactId>lombok</artifactId>
64
+        </dependency>
65
+        <!-- 注解处理器(避免IDE报错) -->
66
+        <dependency>
67
+            <groupId>com.baomidou</groupId>
68
+            <artifactId>mybatis-plus-annotation</artifactId>
69
+            <version>3.4.1</version>
70
+        </dependency>
25 71
 
72
+        <dependency>
73
+            <groupId>com.google.guava</groupId>
74
+            <artifactId>guava</artifactId>
75
+            <version>33.4.0-jre</version> <!-- 使用较新版本 -->
76
+        </dependency>
77
+        <dependency>
78
+            <groupId>org.projectlombok</groupId>
79
+            <artifactId>lombok</artifactId>
80
+        </dependency>
81
+        <dependency>
82
+            <groupId>org.aspectj</groupId>
83
+            <artifactId>aspectjweaver</artifactId>
84
+        </dependency>
26 85
     </dependencies>
27 86
 
28 87
 </project>

+ 16
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/MybatisPlusConfig.java Dosyayı Görüntüle

@@ -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
+}

+ 36
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/PhpDataSyncTask.java Dosyayı Görüntüle

@@ -0,0 +1,36 @@
1
+package com.ruoyi.novel.config;
2
+
3
+import com.ruoyi.novel.domain.RoyaltySummary;
4
+import com.ruoyi.novel.mapper.RoyaltySummaryMapper;
5
+import org.springframework.beans.factory.annotation.Autowired;
6
+import org.springframework.beans.factory.annotation.Value;
7
+import org.springframework.scheduling.annotation.Scheduled;
8
+import org.springframework.stereotype.Component;
9
+import org.springframework.web.client.RestTemplate;
10
+
11
+import java.util.Arrays;
12
+
13
+// 定时同步PHP数据任务
14
+@Component
15
+public class PhpDataSyncTask {
16
+
17
+    @Autowired
18
+    private RoyaltySummaryMapper royaltySummaryMapper;
19
+
20
+    @Value("${php.data.url}")
21
+    private String phpDataUrl;
22
+
23
+    // 每天凌晨2点执行
24
+    @Scheduled(cron = "0 0 2 * * ?")
25
+    public void syncRoyaltyData() {
26
+        RestTemplate restTemplate = new RestTemplate();
27
+        RoyaltySummary[] summaries = restTemplate.getForObject(
28
+                phpDataUrl + "/royalty/summary",
29
+                RoyaltySummary[].class
30
+        );
31
+
32
+        if (summaries != null) {
33
+            Arrays.stream(summaries).forEach(royaltySummaryMapper::insertOrUpdate);
34
+        }
35
+    }
36
+}

+ 14
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/RestTemplateConfig.java Dosyayı Görüntüle

@@ -0,0 +1,14 @@
1
+package com.ruoyi.novel.config;
2
+
3
+import org.springframework.context.annotation.Bean;
4
+import org.springframework.context.annotation.Configuration;
5
+import org.springframework.web.client.RestTemplate;
6
+
7
+@Configuration
8
+public class RestTemplateConfig {
9
+
10
+    @Bean
11
+    public RestTemplate restTemplate() {
12
+        return new RestTemplate();
13
+    }
14
+}

+ 61
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/ScheduledTasks.java Dosyayı Görüntüle

@@ -0,0 +1,61 @@
1
+package com.ruoyi.novel.config;
2
+
3
+import com.ruoyi.novel.service.IAdService;
4
+import com.ruoyi.novel.service.IFinanceService;
5
+import com.ruoyi.novel.service.IVoteService;
6
+import org.slf4j.Logger;
7
+import org.slf4j.LoggerFactory;
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.context.annotation.Configuration;
10
+import org.springframework.scheduling.annotation.EnableScheduling;
11
+import org.springframework.scheduling.annotation.Scheduled;
12
+
13
+
14
+// ScheduledTasks.java
15
+@Configuration
16
+@EnableScheduling
17
+public class ScheduledTasks {
18
+    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
19
+    @Autowired
20
+    private IAdService adService;
21
+
22
+    @Autowired
23
+    private IVoteService voteService;
24
+
25
+    @Autowired
26
+    private IFinanceService financeService;
27
+
28
+    /**
29
+     * 每5分钟重试失败的广告同步
30
+     */
31
+    @Scheduled(fixedRate = 300000)
32
+    public void retryAdSync() {
33
+        adService.retryFailedSyncs();
34
+    }
35
+
36
+    /**
37
+     * 每10分钟重试失败的PHP通知
38
+     */
39
+    @Scheduled(cron = "0 */10 * * * ?")
40
+    public void retryVoteNotify() {
41
+        voteService.retryFailedNotifications();
42
+    }
43
+
44
+    /**
45
+     * 每天凌晨2点同步分账数据
46
+     */
47
+    @Scheduled(cron = "0 0 2 * * ?")
48
+    public void syncRoyaltyData() {
49
+        financeService.syncRoyaltyData();
50
+    }
51
+
52
+    /**
53
+     * 每小时刷新投票计数器
54
+     */
55
+    @Scheduled(cron = "0 0 * * * ?")
56
+    public void resetVoteCounters() {
57
+        // 实际应基于每日重置
58
+        log.info("Resetting vote counters...");
59
+        // 计数器重置逻辑
60
+    }
61
+}

+ 21
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/TableSupport.java Dosyayı Görüntüle

@@ -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
+}

+ 4
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/config/ThreadPoolConfig.java Dosyayı Görüntüle

@@ -0,0 +1,4 @@
1
+package com.ruoyi.novel.config;
2
+
3
+public class ThreadPoolConfig {
4
+}

+ 37
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/ChapterController.java Dosyayı Görüntüle

@@ -0,0 +1,37 @@
1
+package com.ruoyi.novel.controller;
2
+
3
+import com.ruoyi.common.core.domain.AjaxResult;
4
+import com.ruoyi.novel.domain.ChapterDTO;
5
+import com.ruoyi.novel.domain.NovelChapter;
6
+import com.ruoyi.novel.service.ChapterService;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.*;
9
+
10
+import java.util.List;
11
+
12
+// ChapterController.java
13
+@RestController
14
+@RequestMapping("/chapter")
15
+public class ChapterController {
16
+
17
+    @Autowired
18
+    private ChapterService chapterService;
19
+    @GetMapping("/list/{novelId}")
20
+    public AjaxResult listChapters(@PathVariable Long novelId) {
21
+        List<NovelChapter> chapters = chapterService.selectChapterListByNovelId(novelId);
22
+        return AjaxResult.success(chapters);
23
+    }
24
+    @GetMapping("/{novelId}")
25
+    public AjaxResult getChapters(@PathVariable Long novelId) {
26
+        List<NovelChapter> chapters = chapterService.selectChapterListByNovelId(novelId);
27
+        return AjaxResult.success(chapters);
28
+    }
29
+
30
+    @PostMapping("/save")
31
+    public AjaxResult saveChapter(@RequestBody ChapterDTO dto) {
32
+        return toAjax(chapterService.saveChapter(dto.getChapter(), dto.getContent()));
33
+    }
34
+    private AjaxResult toAjax(int result) {
35
+        return result > 0 ? AjaxResult.success() : AjaxResult.error();
36
+    }
37
+}

+ 90
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelAdController.java Dosyayı Görüntüle

@@ -0,0 +1,90 @@
1
+package com.ruoyi.novel.controller;
2
+//# 广告计数接口
3
+
4
+import com.ruoyi.common.core.domain.AjaxResult;
5
+import com.ruoyi.novel.domain.AdCountRequest;
6
+import com.ruoyi.novel.service.AdService;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.data.redis.core.RedisTemplate;
9
+import org.springframework.http.HttpStatus;
10
+import org.springframework.http.ResponseEntity;
11
+import org.springframework.web.bind.annotation.*;
12
+
13
+import java.time.Duration;
14
+import java.util.Arrays;
15
+import java.util.List;
16
+
17
+@RestController
18
+@RequestMapping("/ad")
19
+public class NovelAdController {
20
+
21
+    private final AdService adService;
22
+    private final RedisTemplate<String, String> redisTemplate;
23
+
24
+    @Autowired
25
+    public NovelAdController(AdService adService, RedisTemplate<String, String> redisTemplate) {
26
+        this.adService = adService;
27
+        this.redisTemplate = redisTemplate;
28
+    }
29
+
30
+    @PostMapping("/count")
31
+    public ResponseEntity<?> countAdView(
32
+            @RequestBody AdCountRequest request,
33
+            @RequestHeader(value = "X-PHP-Session", required = false) String phpSessionId
34
+    ) {
35
+        // 验证请求来源
36
+        if (!isValidSource(request.getSource())) {
37
+            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid source");
38
+        }
39
+
40
+        // 会话同步处理
41
+        String userId = resolveUserId(request, phpSessionId);
42
+
43
+        // 防重处理 (5分钟内同一章节只计数一次)
44
+        String redisKey = String.format("ad:%s:%s:%s",
45
+                request.getSource(), userId, request.getChapterId());
46
+
47
+        if (redisTemplate.hasKey(redisKey)) {
48
+            return ResponseEntity.ok("Already counted");
49
+        }
50
+
51
+        // 记录广告
52
+        adService.logAdView(
53
+                userId,
54
+                request.getChapterId(),
55
+                request.getAdType(),
56
+                request.getSource()
57
+        );
58
+
59
+        // 设置Redis锁
60
+        redisTemplate.opsForValue().set(redisKey, "1", Duration.ofMinutes(5));
61
+
62
+        return ResponseEntity.ok("Ad counted");
63
+    }
64
+    @PostMapping("/count")
65
+    public AjaxResult logAdView(@RequestBody AdCountRequest request) {
66
+        adService.logAdView(request);
67
+        return AjaxResult.success("广告计数成功");
68
+    }
69
+    private String resolveUserId(AdCountRequest request, String phpSessionId) {
70
+        // 优先使用PHP会话
71
+        if (phpSessionId != null) {
72
+            String sessionKey = "session:" + phpSessionId;
73
+            String userId = redisTemplate.opsForValue().get(sessionKey + ":user_id");
74
+            if (userId != null) return userId;
75
+        }
76
+
77
+        // 其次使用请求中的用户ID
78
+        if (request.getUserId() != null) {
79
+            return request.getUserId().toString();
80
+        }
81
+
82
+        // 最后使用设备ID
83
+        return request.getDeviceId();
84
+    }
85
+
86
+    private boolean isValidSource(String source) {
87
+        List<String> validSources = Arrays.asList("h5", "wechat", "douyin", "app");
88
+        return validSources.contains(source);
89
+    }
90
+}

+ 92
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelController.java Dosyayı Görüntüle

@@ -0,0 +1,92 @@
1
+package com.ruoyi.novel.controller;
2
+
3
+import com.github.pagehelper.PageHelper;
4
+import com.github.pagehelper.PageInfo;
5
+import com.ruoyi.common.constant.HttpStatus;
6
+import com.ruoyi.common.core.domain.AjaxResult;
7
+import com.ruoyi.common.core.page.PageDomain;
8
+import com.ruoyi.common.core.page.TableDataInfo;
9
+import com.ruoyi.common.core.page.TableSupport;
10
+import com.ruoyi.novel.domain.Novel;
11
+import com.ruoyi.novel.mapper.NovelMapper;
12
+import com.ruoyi.novel.service.NovelSearchService;
13
+import com.ruoyi.novel.service.NovelService;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.web.bind.annotation.*;
16
+
17
+import java.util.List;
18
+
19
+// NovelController.java
20
+@RestController
21
+@RequestMapping("/novel")
22
+public class NovelController {
23
+
24
+    @Autowired
25
+    private NovelService novelService;
26
+    @Autowired
27
+    private NovelSearchService searchService;
28
+    @Autowired
29
+    private NovelMapper novelMapper;
30
+    @GetMapping("/list")
31
+    public TableDataInfo list(Novel novel) {
32
+        startPage();
33
+        List<Novel> list = novelService.selectNovelList(novel);
34
+        return getDataTable(list);
35
+    }
36
+
37
+    @PostMapping
38
+    public AjaxResult add(@RequestBody Novel novel) {
39
+        return toAjax(novelService.insertNovel(novel));
40
+    }
41
+    @DeleteMapping("/{ids}")
42
+    public AjaxResult remove(@PathVariable Long[] ids) {
43
+        return toAjax(novelService.deleteNovelByIds(ids));
44
+    }
45
+
46
+    private AjaxResult toAjax(int rows) {
47
+        return rows > 0 ? AjaxResult.success() : AjaxResult.error();
48
+    }
49
+    // 若依框架的分页方法
50
+    protected void startPage() {
51
+        PageDomain pageDomain = TableSupport.buildPageRequest();
52
+        Integer pageNum = pageDomain.getPageNum();
53
+        Integer pageSize = pageDomain.getPageSize();
54
+        PageHelper.startPage(pageNum, pageSize);
55
+    }
56
+
57
+    // 若依框架的表格数据封装
58
+    protected TableDataInfo getDataTable(List<?> list) {
59
+        TableDataInfo rspData = new TableDataInfo();
60
+        rspData.setCode(HttpStatus.SUCCESS);
61
+        rspData.setRows(list);
62
+        rspData.setTotal(new PageInfo(list).getTotal());
63
+        return rspData;
64
+    }
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
+    }
92
+}

+ 30
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelFinanceController.java Dosyayı Görüntüle

@@ -0,0 +1,30 @@
1
+package com.ruoyi.novel.controller;
2
+
3
+import com.ruoyi.common.core.domain.AjaxResult;
4
+import com.ruoyi.novel.domain.RoyaltyReport;
5
+import com.ruoyi.novel.service.IFinanceService;
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.format.annotation.DateTimeFormat;
8
+import org.springframework.web.bind.annotation.GetMapping;
9
+import org.springframework.web.bind.annotation.RequestMapping;
10
+import org.springframework.web.bind.annotation.RequestParam;
11
+import org.springframework.web.bind.annotation.RestController;
12
+
13
+//# 分账结算接口
14
+// NovelFinanceController.java
15
+@RestController
16
+@RequestMapping("/finance")
17
+public class NovelFinanceController {
18
+
19
+    @Autowired
20
+    private IFinanceService financeService;
21
+
22
+    @GetMapping("/royalty")
23
+    public AjaxResult getRoyalty(
24
+            @RequestParam Long authorId,
25
+            @RequestParam @DateTimeFormat(pattern="yyyy-MM") String month) {
26
+
27
+        RoyaltyReport report = financeService.calculateRoyalty(authorId, month);
28
+        return AjaxResult.success(report);
29
+    }
30
+}

+ 54
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/NovelVoteController.java Dosyayı Görüntüle

@@ -0,0 +1,54 @@
1
+package com.ruoyi.novel.controller;
2
+
3
+import com.google.common.util.concurrent.RateLimiter;
4
+import com.ruoyi.common.core.domain.AjaxResult;
5
+import com.ruoyi.novel.domain.VoteRequest;
6
+import com.ruoyi.novel.service.IVoteService;
7
+import org.springframework.beans.factory.annotation.Autowired;
8
+import org.springframework.web.bind.annotation.PostMapping;
9
+import org.springframework.web.bind.annotation.RequestBody;
10
+import org.springframework.web.bind.annotation.RequestMapping;
11
+import org.springframework.web.bind.annotation.RestController;
12
+
13
+import java.util.concurrent.ConcurrentHashMap;
14
+
15
+//# 剧情投票接口
16
+// NovelVoteController.java
17
+@RestController
18
+@RequestMapping("/story/vote")
19
+public class NovelVoteController {
20
+
21
+    @Autowired
22
+    private IVoteService voteService;
23
+
24
+    // 使用Guava RateLimiter限制投票频率
25
+    private static final ConcurrentHashMap<Long, RateLimiter> userRateLimiters = new ConcurrentHashMap<>();
26
+    private static final int MAX_VOTES_PER_DAY = 3;
27
+
28
+    @PostMapping("/next")
29
+    public AjaxResult voteForStoryBranch(@RequestBody VoteRequest request) {
30
+        // 1. 频率限制检查
31
+        RateLimiter rateLimiter = userRateLimiters.computeIfAbsent(
32
+                request.getUserId(),
33
+                k -> RateLimiter.create(MAX_VOTES_PER_DAY)
34
+        );
35
+        // 1. 检查用户投票频率
36
+//        if (!voteService.canUserVote(request.getUserId())) {
37
+//            return AjaxResult.error("投票过于频繁,请稍后再试");
38
+//        }
39
+        if (!rateLimiter.tryAcquire()) {
40
+            return AjaxResult.error("投票过于频繁,请稍后再试");
41
+        }
42
+
43
+        // 2. 调用DeepSeek API生成剧情
44
+        String generatedContent = voteService.generateStoryContent(
45
+                request.getStoryId(),
46
+                request.getOptionId()
47
+        );
48
+
49
+        // 3. 异步通知PHP系统
50
+        voteService.notifyPhpSystem(request, generatedContent);
51
+
52
+        return AjaxResult.success("Vote processed", generatedContent);
53
+    }
54
+}

+ 28
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/controller/ReadingHistoryController.java Dosyayı Görüntüle

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

+ 27
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AdCountRequest.java Dosyayı Görüntüle

@@ -0,0 +1,27 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import lombok.Data;
4
+
5
+import javax.validation.constraints.NotBlank;
6
+import javax.validation.constraints.NotNull;
7
+
8
+// AdCountRequest.java
9
+@Data
10
+public class AdCountRequest {
11
+    @NotNull(message = "用户ID不能为空")
12
+    private Long userId;
13
+
14
+    @NotNull(message = "章节ID不能为空")
15
+    private Long chapterId;
16
+
17
+    @NotBlank(message = "广告平台不能为空")
18
+    private String adPlatform;
19
+
20
+    @NotBlank(message = "设备ID不能为空")
21
+    private String deviceId;
22
+
23
+    public String source;
24
+
25
+    public String adType;
26
+
27
+}

+ 40
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AdLog.java Dosyayı Görüntüle

@@ -0,0 +1,40 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+
6
+import javax.validation.constraints.NotBlank;
7
+import javax.validation.constraints.NotNull;
8
+import javax.validation.constraints.Size;
9
+import java.util.Date;
10
+
11
+// AdLog.java
12
+@Data
13
+@TableName("novel_ad_log")
14
+public class AdLog {
15
+    @TableId(type = IdType.AUTO)
16
+    private Long id;
17
+
18
+    @NotNull(message = "用户ID不能为空")
19
+    private Long userId;
20
+
21
+    @NotNull(message = "章节ID不能为空")
22
+    private Long chapterId;
23
+
24
+    @NotBlank(message = "广告平台不能为空")
25
+    @Size(max = 50, message = "广告平台标识过长")
26
+    private String adPlatform;
27
+
28
+    @TableField(fill = FieldFill.INSERT)
29
+    private Date viewTime;
30
+
31
+    private String deviceInfo;
32
+
33
+    // 状态字段:0-未同步 1-已同步
34
+    private Integer syncStatus;
35
+
36
+    // 确保有默认构造方法
37
+    public AdLog() {}
38
+
39
+    public String deviceId;
40
+}

+ 26
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/AdPlatformConfig.java Dosyayı Görüntüle

@@ -0,0 +1,26 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import com.baomidou.mybatisplus.annotation.TableId;
4
+import com.baomidou.mybatisplus.annotation.TableName;
5
+import lombok.Data;
6
+
7
+@Data
8
+@TableName("novel_ad_config")
9
+public class AdPlatformConfig {
10
+    @TableId
11
+    private String platformCode;
12
+
13
+    private String platformName;
14
+    private String apiUrl;
15
+    private String authToken;
16
+    private String paramsTemplate;
17
+
18
+    // 添加带参数的构造方法
19
+    public AdPlatformConfig(String code, String name, String url, String token, String params) {
20
+        this.platformCode = code;
21
+        this.platformName = name;
22
+        this.apiUrl = url;
23
+        this.authToken = token;
24
+        this.paramsTemplate = params;
25
+    }
26
+}

+ 15
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/ChapterDTO.java Dosyayı Görüntüle

@@ -0,0 +1,15 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import lombok.Data;
4
+
5
+import javax.validation.constraints.NotBlank;
6
+import javax.validation.constraints.NotNull;
7
+
8
+@Data
9
+public class ChapterDTO {
10
+    @NotNull(message = "章节不能为空")
11
+    private NovelChapter chapter;
12
+
13
+    @NotBlank(message = "内容不能为空")
14
+    private String content;
15
+}

+ 39
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/Novel.java Dosyayı Görüntüle

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

+ 25
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/NovelChapter.java Dosyayı Görüntüle

@@ -0,0 +1,25 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableName;
6
+import lombok.Data;
7
+
8
+import java.util.Date;
9
+
10
+// NovelChapter.java
11
+@Data
12
+@TableName("novel_chapter")
13
+public class NovelChapter {
14
+    @TableId(type = IdType.AUTO)
15
+    private Long id;
16
+    private Long novelId;
17
+    private String chapterTitle;
18
+    private Integer chapterOrder;
19
+    private Date publishTime;
20
+    private String isVip;
21
+
22
+    public Date createTime;
23
+
24
+    public Date updateTime;
25
+}

+ 14
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/NovelContent.java Dosyayı Görüntüle

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

+ 27
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/ReadingRecord.java Dosyayı Görüntüle

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

+ 30
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/RoyaltyReport.java Dosyayı Görüntüle

@@ -0,0 +1,30 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import lombok.Data;
4
+
5
+import java.math.BigDecimal;
6
+
7
+// RoyaltyReport.java
8
+@Data
9
+public class RoyaltyReport {
10
+    private Long authorId;
11
+    private String authorName;
12
+    private String month;
13
+    private BigDecimal totalReads;
14
+    private BigDecimal royaltyRate;
15
+    private BigDecimal handlingFeeRate;
16
+    private BigDecimal handlingFee;
17
+    private BigDecimal netAmount;
18
+    private String currency = "CNY";
19
+
20
+    // 计算逻辑
21
+    public void calculate(BigDecimal reads, BigDecimal rate, BigDecimal feeRate) {
22
+        this.totalReads = reads;
23
+        this.royaltyRate = rate;
24
+        this.handlingFeeRate = feeRate;
25
+
26
+        BigDecimal rawAmount = reads.multiply(rate);
27
+        this.handlingFee = rawAmount.multiply(feeRate);
28
+        this.netAmount = rawAmount.subtract(handlingFee);
29
+    }
30
+}

+ 26
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/RoyaltySummary.java Dosyayı Görüntüle

@@ -0,0 +1,26 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import com.baomidou.mybatisplus.annotation.TableId;
4
+import com.baomidou.mybatisplus.annotation.TableName;
5
+import lombok.Data;
6
+
7
+import java.math.BigDecimal;
8
+import java.util.Date;
9
+
10
+// RoyaltySummary.java
11
+// RoyaltySummary.java
12
+@Data
13
+@TableName("novel_royalty_summary")
14
+public class RoyaltySummary {
15
+    @TableId
16
+    private Long authorId;
17
+
18
+    @TableId
19
+    private String month; // yyyy-MM格式
20
+
21
+    private BigDecimal totalReads;
22
+    private Date lastSyncTime;
23
+
24
+    public RoyaltySummary(long l, String s, BigDecimal bigDecimal) {
25
+    }
26
+}

+ 26
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/StoryOption.java Dosyayı Görüntüle

@@ -0,0 +1,26 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableName;
6
+import lombok.Data;
7
+
8
+import javax.validation.constraints.NotBlank;
9
+import javax.validation.constraints.NotNull;
10
+
11
+// StoryOption.java
12
+@Data
13
+//@TableName("novel_story_option")
14
+@TableName("story_option")
15
+public class StoryOption {
16
+    @TableId(type = IdType.AUTO)
17
+    private Long id;
18
+
19
+    @NotNull(message = "故事ID不能为空")
20
+    private Long storyId;
21
+
22
+    @NotBlank(message = "选项内容不能为空")
23
+    private String optionText;
24
+
25
+    private Integer voteCount;
26
+}

+ 26
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/VoteRecord.java Dosyayı Görüntüle

@@ -0,0 +1,26 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import com.baomidou.mybatisplus.annotation.*;
4
+import lombok.Data;
5
+
6
+import java.util.Date;
7
+
8
+// VoteRecord.java
9
+// VoteRecord.java
10
+@Data
11
+@TableName("novel_vote_log")
12
+public class VoteRecord {
13
+    @TableId(type = IdType.AUTO)
14
+    private Long id;
15
+
16
+    private Long userId;
17
+    private Long storyId;
18
+    private Long optionId;
19
+
20
+    @TableField(fill = FieldFill.INSERT)
21
+    private Date voteTime;
22
+
23
+    private String generatedContent;
24
+
25
+    private Integer notifyStatus; // 0-未通知 1-已通知
26
+}

+ 18
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/domain/VoteRequest.java Dosyayı Görüntüle

@@ -0,0 +1,18 @@
1
+package com.ruoyi.novel.domain;
2
+
3
+import lombok.Data;
4
+
5
+import javax.validation.constraints.NotNull;
6
+
7
+// VoteRequest.java
8
+@Data
9
+public class VoteRequest {
10
+    @NotNull(message = "用户ID不能为空")
11
+    private Long userId;
12
+
13
+    @NotNull(message = "故事ID不能为空")
14
+    private Long storyId;
15
+
16
+    @NotNull(message = "选项ID不能为空")
17
+    private Long optionId;
18
+}

+ 44
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/exception/GlobalExceptionHandler.java Dosyayı Görüntüle

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

+ 20
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/AdLogMapper.java Dosyayı Görüntüle

@@ -0,0 +1,20 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.ruoyi.novel.domain.AdLog;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
8
+
9
+import java.util.List;
10
+
11
+// AdLogMapper.java
12
+@Mapper
13
+public interface AdLogMapper extends BaseMapper<AdLog> {
14
+
15
+    @Select("SELECT COUNT(*) FROM novel_ad_log WHERE user_id = #{userId} AND chapter_id = #{chapterId}")
16
+    int countByUserAndChapter(@Param("userId") Long userId, @Param("chapterId") Long chapterId);
17
+
18
+    @Select("SELECT * FROM novel_ad_log WHERE sync_status = 0 ORDER BY view_time ASC LIMIT 100")
19
+    List<AdLog> selectUnsyncedLogs();
20
+}

+ 8
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/AdPlatformConfigMapper.java Dosyayı Görüntüle

@@ -0,0 +1,8 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.ruoyi.novel.domain.AdPlatformConfig;
5
+
6
+public interface AdPlatformConfigMapper extends BaseMapper<AdPlatformConfig> {
7
+    // 继承BaseMapper即拥有selectList/insert等方法
8
+}

+ 26
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelChapterMapper.java Dosyayı Görüntüle

@@ -0,0 +1,26 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.ruoyi.novel.domain.NovelChapter;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+import java.util.List;
8
+
9
+// NovelChapterMapper.java
10
+@Mapper
11
+public interface NovelChapterMapper {
12
+    List<NovelChapter> selectChapterListByNovelId(Long novelId);
13
+    NovelChapter selectChapterById(Long chapterId);
14
+    int insertChapter(NovelChapter chapter);
15
+    int updateChapter(NovelChapter chapter);
16
+
17
+    List<NovelChapter> selectList(QueryWrapper<NovelChapter> orderByAsc);
18
+
19
+    void insert(NovelChapter chapter);
20
+
21
+    void updateById(NovelChapter chapter);
22
+
23
+    NovelChapter selectById(Long chapterId);
24
+
25
+    int deleteBatchIds(List<Long> asList);
26
+}

+ 14
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelContentMapper.java Dosyayı Görüntüle

@@ -0,0 +1,14 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.ruoyi.novel.domain.NovelContent;
5
+import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Update;
8
+
9
+@Mapper
10
+public interface NovelContentMapper extends BaseMapper<NovelContent> {
11
+    // 特殊方法可在此定义
12
+    @Update("UPDATE novel_content SET content = #{content} WHERE chapter_id = #{chapterId}")
13
+    int updateContent(@Param("chapterId") Long chapterId, @Param("content") String content);
14
+}

+ 32
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/NovelMapper.java Dosyayı Görüntüle

@@ -0,0 +1,32 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
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;
8
+
9
+import java.util.List;
10
+
11
+// NovelMapper.java
12
+public interface NovelMapper extends BaseMapper<Novel> {
13
+    List<Novel> selectNovelList(Novel novel);
14
+    Novel selectNovelById(Long id);
15
+    int insertNovel(Novel novel);
16
+    int updateNovel(Novel novel);
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);
31
+}
32
+

+ 34
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/ReadingHistoryMapper.java Dosyayı Görüntüle

@@ -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
+}

+ 33
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/RoyaltySummaryMapper.java Dosyayı Görüntüle

@@ -0,0 +1,33 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.ruoyi.novel.domain.RoyaltySummary;
5
+import org.apache.ibatis.annotations.*;
6
+
7
+// RoyaltySummaryMapper.java
8
+@Mapper
9
+public interface RoyaltySummaryMapper extends BaseMapper<RoyaltySummary> {
10
+
11
+    @Select("SELECT * FROM novel_royalty_summary " +
12
+            "WHERE author_id = #{authorId} AND month = #{month}")
13
+    RoyaltySummary selectByAuthorAndMonth(
14
+            @Param("authorId") Long authorId,
15
+            @Param("month") String month
16
+    );
17
+
18
+    @Update("INSERT INTO novel_royalty_summary (author_id, month, total_reads, last_sync_time) " +
19
+            "VALUES (#{authorId}, #{month}, #{totalReads}, NOW()) " +
20
+            "ON DUPLICATE KEY UPDATE total_reads = #{totalReads}, last_sync_time = NOW()")
21
+    int upsertSummary(RoyaltySummary summary);
22
+
23
+
24
+    // 添加 insertOrUpdate 方法
25
+    @Insert("<script>" +
26
+            "INSERT INTO novel_royalty_summary (author_id, month, total_reads, last_sync_time) " +
27
+            "VALUES (#{authorId}, #{month}, #{totalReads}, NOW()) " +
28
+            "ON DUPLICATE KEY UPDATE " +
29
+            "total_reads = VALUES(total_reads), " +
30
+            "last_sync_time = NOW()" +
31
+            "</script>")
32
+    int insertOrUpdate(RoyaltySummary summary);
33
+}

+ 40
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/StoryOptionMapper.java Dosyayı Görüntüle

@@ -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
+}

+ 46
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/mapper/VoteRecordMapper.java Dosyayı Görüntüle

@@ -0,0 +1,46 @@
1
+package com.ruoyi.novel.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.ruoyi.novel.domain.VoteRecord;
5
+import lombok.Data;
6
+import org.apache.ibatis.annotations.*;
7
+
8
+import java.util.List;
9
+
10
+// VoteRecordMapper.java
11
+@Mapper
12
+public interface VoteRecordMapper extends BaseMapper<VoteRecord> {
13
+
14
+    @Select("SELECT COUNT(*) FROM novel_vote_log " +
15
+            "WHERE user_id = #{userId} AND story_id = #{storyId}")
16
+    int countUserVotesForStory(@Param("userId") Long userId, @Param("storyId") Long storyId);
17
+
18
+    @Update("UPDATE novel_vote_log SET notify_status = #{status} WHERE id = #{id}")
19
+    int updateNotifyStatus(@Param("id") Long id, @Param("status") Integer status);
20
+
21
+    @Select("SELECT * FROM novel_vote_log WHERE notify_status = 0 ORDER BY vote_time ASC LIMIT 50")
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
+    }
46
+}

+ 10
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdConfigService.java Dosyayı Görüntüle

@@ -0,0 +1,10 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.AdPlatformConfig;
4
+
5
+public interface AdConfigService {
6
+
7
+    void refreshConfigCache();
8
+
9
+    AdPlatformConfig getConfig(String adPlatform);
10
+}

+ 9
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdLogService.java Dosyayı Görüntüle

@@ -0,0 +1,9 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.AdLog;
4
+import org.springframework.transaction.annotation.Transactional;
5
+
6
+public interface AdLogService {
7
+    @Transactional
8
+    void logAdView(AdLog adLog);
9
+}

+ 15
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdService.java Dosyayı Görüntüle

@@ -0,0 +1,15 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.AdCountRequest;
4
+import com.ruoyi.novel.domain.AdLog;
5
+import org.springframework.stereotype.Service;
6
+
7
+import java.util.List;
8
+@Service
9
+// AdService.java
10
+public interface AdService {
11
+    void logAdView(AdCountRequest request);
12
+    List<AdLog> getAdLogsByUser(Long userId);
13
+
14
+    void logAdView(String userId, Long chapterId, String adType, String source);
15
+}

+ 10
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/AdSyncService.java Dosyayı Görüntüle

@@ -0,0 +1,10 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.AdLog;
4
+import com.ruoyi.novel.domain.AdPlatformConfig;
5
+
6
+public interface AdSyncService {
7
+    void syncToTencent(AdLog adLog, AdPlatformConfig config);
8
+
9
+    void syncToPangle(AdLog adLog, AdPlatformConfig config);
10
+}

+ 13
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/ChapterService.java Dosyayı Görüntüle

@@ -0,0 +1,13 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.NovelChapter;
4
+
5
+import java.util.List;
6
+
7
+// ChapterService.java
8
+public interface ChapterService {
9
+    List<NovelChapter> selectChapterListByNovelId(Long novelId);
10
+    int saveChapter(NovelChapter chapter, String content);
11
+    NovelChapter getChapterById(Long chapterId);
12
+    int deleteChapters(Long[] ids);
13
+}

+ 22
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/IAdService.java Dosyayı Görüntüle

@@ -0,0 +1,22 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.AdCountRequest;
4
+import com.ruoyi.novel.domain.AdLog;
5
+import com.ruoyi.novel.domain.AdPlatformConfig;
6
+import org.springframework.stereotype.Service;
7
+
8
+import java.util.List;
9
+import java.util.Map;
10
+
11
+@Service
12
+public interface IAdService {
13
+    void logAdView(AdLog adLog);
14
+
15
+    void logAdView(AdCountRequest request);
16
+
17
+    List<AdLog> getAdLogsByUser(Long userId);
18
+
19
+    void syncWithAdPlatform(AdLog adLog);
20
+    void retryFailedSyncs();
21
+    Map<String, AdPlatformConfig> loadAdConfigs();
22
+}

+ 13
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/IFinanceService.java Dosyayı Görüntüle

@@ -0,0 +1,13 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.RoyaltyReport;
4
+
5
+import java.math.BigDecimal;
6
+
7
+// IFinanceService.java
8
+public interface IFinanceService {
9
+    RoyaltyReport calculateRoyalty(Long authorId, String month);
10
+    void syncRoyaltyData();
11
+    BigDecimal getRoyaltyRate(Long authorId);
12
+    BigDecimal getHandlingFeeRate();
13
+}

+ 16
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/IVoteService.java Dosyayı Görüntüle

@@ -0,0 +1,16 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.VoteRecord;
4
+import com.ruoyi.novel.domain.VoteRequest;
5
+
6
+// IVoteService.java
7
+public interface IVoteService {
8
+    String processVote(VoteRequest request);
9
+    void generateStoryContent(VoteRecord record);
10
+    void notifyPhpSystem(VoteRecord record);
11
+    void retryFailedNotifications();
12
+
13
+    void notifyPhpSystem(VoteRequest request, String generatedContent);
14
+
15
+    String generateStoryContent(Long storyId, Long optionId);
16
+}

+ 13
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/NovelSearchService.java Dosyayı Görüntüle

@@ -0,0 +1,13 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.Novel;
4
+
5
+import java.util.List;
6
+
7
+// NovelSearchService.java
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);
13
+}

+ 16
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/NovelService.java Dosyayı Görüntüle

@@ -0,0 +1,16 @@
1
+package com.ruoyi.novel.service;
2
+
3
+import com.ruoyi.novel.domain.Novel;
4
+import org.springframework.transaction.annotation.Transactional;
5
+
6
+import java.util.List;
7
+
8
+public interface NovelService {
9
+    List<Novel> selectNovelList(Novel novel);
10
+
11
+    @Transactional
12
+    int insertNovel(Novel novel);
13
+    int updateNovel(Novel novel);
14
+    int deleteNovelByIds(Long[] ids);
15
+    Novel selectNovelById(Long id);
16
+}

+ 9
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/ReadingHistoryService.java Dosyayı Görüntüle

@@ -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-system/src/main/java/com/ruoyi/novel/service/VoteService.java Dosyayı Görüntüle

@@ -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
+}

+ 69
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdConfigServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,69 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.ruoyi.novel.domain.AdPlatformConfig;
4
+import com.ruoyi.novel.mapper.AdPlatformConfigMapper;
5
+import com.ruoyi.novel.service.AdConfigService;
6
+import org.springframework.beans.factory.annotation.Autowired;
7
+import org.springframework.stereotype.Service;
8
+
9
+import javax.annotation.PostConstruct;
10
+import java.util.HashMap;
11
+import java.util.Map;
12
+import java.util.function.Function;
13
+import java.util.stream.Collectors;
14
+
15
+// AdConfigServiceImpl.java
16
+@Service
17
+public class AdConfigServiceImpl implements AdConfigService {
18
+
19
+    @Autowired
20
+    private AdPlatformConfigMapper configMapper;
21
+
22
+    private Map<String, AdPlatformConfig> adConfigCache;
23
+    private final Object lock = new Object();
24
+
25
+    @PostConstruct
26
+    public void init() {
27
+        refreshConfigCache();
28
+    }
29
+
30
+    @Override
31
+    public void refreshConfigCache() {
32
+        synchronized (lock) {
33
+            adConfigCache = configMapper.selectList(null).stream()
34
+                    .collect(Collectors.toMap(AdPlatformConfig::getPlatformCode, Function.identity()));
35
+
36
+            // 如果数据库为空,初始化默认配置
37
+            if (adConfigCache.isEmpty()) {
38
+                initDefaultConfigs();
39
+            }
40
+        }
41
+    }
42
+
43
+    private void initDefaultConfigs() {
44
+        adConfigCache = new HashMap<>();
45
+        adConfigCache.put("tencent", new AdPlatformConfig(
46
+                "tencent", "腾讯广告",
47
+                "https://ad.tencent.com/track",
48
+                "default_tencent_token",
49
+                "{\"user_id\":\"${userId}\",\"content_id\":\"${chapterId}\"}"
50
+        ));
51
+        adConfigCache.put("pangle", new AdPlatformConfig(
52
+                "pangle", "穿山甲",
53
+                "https://ad.pangle.com/api/v1/event",
54
+                "default_pangle_token",
55
+                "{\"user\":\"${userId}\",\"content_id\":\"${chapterId}\"}"
56
+        ));
57
+
58
+        // 保存默认配置到数据库
59
+        adConfigCache.values().forEach(configMapper::insert);
60
+    }
61
+
62
+    @Override
63
+    public AdPlatformConfig getConfig(String platformCode) {
64
+        if (adConfigCache == null) {
65
+            refreshConfigCache();
66
+        }
67
+        return adConfigCache.get(platformCode);
68
+    }
69
+}

+ 88
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdLogServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,88 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.ruoyi.common.exception.ServiceException;
5
+import com.ruoyi.novel.domain.AdLog;
6
+import com.ruoyi.novel.domain.AdPlatformConfig;
7
+import com.ruoyi.novel.mapper.AdLogMapper;
8
+import com.ruoyi.novel.service.AdConfigService;
9
+import com.ruoyi.novel.service.AdLogService;
10
+import com.ruoyi.novel.service.AdSyncService;
11
+import lombok.extern.slf4j.Slf4j;
12
+import org.slf4j.Logger;
13
+import org.slf4j.LoggerFactory;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.scheduling.annotation.Async;
16
+import org.springframework.scheduling.annotation.Scheduled;
17
+import org.springframework.stereotype.Service;
18
+import org.springframework.transaction.annotation.Transactional;
19
+
20
+import java.util.List;
21
+
22
+// AdLogServiceImpl.java
23
+@Service
24
+@Slf4j
25
+public class AdLogServiceImpl implements AdLogService {
26
+    private static final Logger logger = LoggerFactory.getLogger(AdLogServiceImpl.class);
27
+
28
+    @Autowired
29
+    private AdLogMapper adLogMapper;
30
+
31
+    @Autowired
32
+    private AdConfigService adConfigService;
33
+
34
+    @Autowired
35
+    private AdSyncService adSyncService;
36
+
37
+    @Override
38
+    @Transactional
39
+    public void logAdView(AdLog adLog) {
40
+        // 1. 保存到数据库
41
+        adLog.setSyncStatus(0); // 0=未同步
42
+        adLogMapper.insert(adLog);
43
+
44
+        // 2. 异步同步到广告平台
45
+        syncAdLogAsync(adLog);
46
+    }
47
+
48
+    @Async
49
+    public void syncAdLogAsync(AdLog adLog) {
50
+        try {
51
+            AdPlatformConfig config = adConfigService.getConfig(adLog.getAdPlatform());
52
+            if (config == null) {
53
+                throw new ServiceException("广告平台配置不存在: " + adLog.getAdPlatform());
54
+            }
55
+
56
+            // 根据平台选择同步方式
57
+            switch (adLog.getAdPlatform()) {
58
+                case "tencent":
59
+                    adSyncService.syncToTencent(adLog, config);
60
+                    break;
61
+                case "pangle":
62
+                    adSyncService.syncToPangle(adLog, config);
63
+                    break;
64
+                default:
65
+                    throw new ServiceException("不支持的广告平台: " + adLog.getAdPlatform());
66
+            }
67
+
68
+            // 更新同步状态
69
+            adLog.setSyncStatus(1); // 1=同步成功
70
+        } catch (Exception e) {
71
+            log.error("广告同步失败: {}", e.getMessage());
72
+            adLog.setSyncStatus(2); // 2=同步失败
73
+        }
74
+        adLogMapper.updateById(adLog);
75
+    }
76
+
77
+    @Scheduled(fixedRate = 300000) // 每5分钟执行一次
78
+    public void retryFailedSyncs() {
79
+        List<AdLog> failedLogs = adLogMapper.selectList(
80
+                new QueryWrapper<AdLog>().eq("sync_status", 2)
81
+        );
82
+
83
+        failedLogs.forEach(log -> {
84
+            logger.info("重试广告同步: ID={}, 平台={}", log.getId(), log.getAdPlatform());
85
+            syncAdLogAsync(log);
86
+        });
87
+    }
88
+}

+ 130
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,130 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.ruoyi.novel.domain.AdCountRequest;
5
+import com.ruoyi.novel.domain.AdLog;
6
+import com.ruoyi.novel.domain.AdPlatformConfig;
7
+import com.ruoyi.novel.mapper.AdLogMapper;
8
+import com.ruoyi.novel.service.IAdService;
9
+import com.ruoyi.novel.utils.AdPlatformClient;
10
+import org.slf4j.Logger;
11
+import org.slf4j.LoggerFactory;
12
+import org.springframework.beans.factory.annotation.Autowired;
13
+import org.springframework.scheduling.annotation.Scheduled;
14
+import org.springframework.stereotype.Service;
15
+import org.springframework.transaction.annotation.Transactional;
16
+
17
+import java.util.Date;
18
+import java.util.HashMap;
19
+import java.util.List;
20
+import java.util.Map;
21
+
22
+@Service
23
+public class AdServiceImpl implements IAdService {
24
+    private static final Logger logger = LoggerFactory.getLogger(AdServiceImpl.class);
25
+    @Autowired
26
+    private AdLogMapper adLogMapper;
27
+
28
+    @Autowired
29
+    private AdPlatformClient adPlatformClient;
30
+
31
+    private Map<String, AdPlatformConfig> adConfigCache;
32
+    private final Object lock = new Object();
33
+    @Override
34
+    @Transactional
35
+    public void logAdView(AdLog adLog) {
36
+        // 防止重复记录
37
+        int count = adLogMapper.countByUserAndChapter(adLog.getUserId(), adLog.getChapterId());
38
+        if (count > 0) {
39
+            logger.warn("Duplicate ad log: user={}, chapter={}", adLog.getUserId(), adLog.getChapterId());
40
+            return;
41
+        }
42
+
43
+        adLog.setSyncStatus(0); // 未同步状态
44
+        adLogMapper.insert(adLog);
45
+    }
46
+
47
+//    @Override
48
+//    @Async("adTaskExecutor")
49
+//    public void syncWithAdPlatform(AdLog adLog) {
50
+//        try {
51
+//            adPlatformClient.syncAdPlatform(adLog);
52
+//            adLog.setSyncStatus(1); // 同步成功
53
+//        } catch (Exception e) {
54
+//            logger.error("Ad sync failed: {}", e.getMessage());
55
+//            adLog.setSyncStatus(2); // 同步失败
56
+//        }
57
+//        adLogMapper.updateById(adLog);
58
+//    }
59
+
60
+    @Override
61
+    @Scheduled(fixedRateString = "${scheduled.ad-retry-interval:300000}") // 默认5分钟重试
62
+    public void retryFailedSyncs() {
63
+        List<AdLog> failedLogs = adLogMapper.selectList(
64
+                new QueryWrapper<AdLog>().eq("sync_status", 2) // 2=同步失败状态
65
+        );
66
+        failedLogs.forEach(adLog -> {
67
+            try {
68
+                syncWithAdPlatform(adLog);  // 调用同步方法
69
+                adLog.setSyncStatus(1);      // 更新为成功状态
70
+                adLogMapper.updateById(adLog);
71
+            } catch (Exception e) {
72
+                logger.error("广告同步重试失败: ID={}", adLog.getId(), e);
73
+            }
74
+        });
75
+    }
76
+
77
+    @Override
78
+    public Map<String, AdPlatformConfig> loadAdConfigs() {
79
+        if (adConfigCache == null) {
80
+            synchronized (lock) {
81
+                if (adConfigCache == null) {
82
+                    // 实际应从数据库加载
83
+                    adConfigCache = new HashMap<>();
84
+                    adConfigCache.put("tencent", new AdPlatformConfig(
85
+                            "tencent", "腾讯广告",
86
+                            "https://ad.tencent.com/track",
87
+                            "token_123",
88
+                            "{\"user_id\":\"${userId}\",\"chapter_id\":\"${chapterId}\"}"
89
+                    ));
90
+                    // 其他平台配置...
91
+                }
92
+            }
93
+        }
94
+        return adConfigCache;
95
+    }
96
+    @Override
97
+    public void logAdView(AdCountRequest request) {
98
+        AdLog log = new AdLog();
99
+        log.setUserId(request.getUserId());
100
+        log.setChapterId(request.getChapterId());
101
+        log.setAdPlatform(request.getAdPlatform());
102
+        log.setDeviceId(request.getDeviceId());
103
+        log.setViewTime(new Date());
104
+        log.setSyncStatus(0); // 未同步
105
+
106
+        adLogMapper.insert(log);
107
+    }
108
+    @Override
109
+    public List<AdLog> getAdLogsByUser(Long userId) {
110
+        return adLogMapper.selectList(
111
+                new QueryWrapper<AdLog>().eq("user_id", userId)
112
+        );
113
+    }
114
+    @Override
115
+    public void syncWithAdPlatform(AdLog adLog) {
116
+        // 根据平台选择不同实现
117
+        switch (adLog.getAdPlatform()) {
118
+            case "tencent":
119
+                adPlatformClient.syncTencent(adLog);
120
+                adLog.setSyncStatus(1); // 同步成功
121
+                break;
122
+            case "pangle":
123
+                adPlatformClient.syncPangle(adLog);
124
+                adLog.setSyncStatus(1); // 同步成功
125
+                break;
126
+            default:
127
+                logger.warn("Unknown ad platform: {}", adLog.getAdPlatform());
128
+        }
129
+    }
130
+}

+ 82
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/AdSyncServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,82 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.ruoyi.common.exception.ServiceException;
4
+import com.ruoyi.novel.domain.AdLog;
5
+import com.ruoyi.novel.domain.AdPlatformConfig;
6
+import com.ruoyi.novel.service.AdSyncService;
7
+import lombok.extern.slf4j.Slf4j;
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.http.*;
10
+import org.springframework.stereotype.Service;
11
+import org.springframework.web.client.RestTemplate;
12
+import org.springframework.web.util.UriComponentsBuilder;
13
+
14
+// AdSyncServiceImpl.java
15
+@Service
16
+@Slf4j
17
+public class AdSyncServiceImpl implements AdSyncService {
18
+
19
+    @Autowired
20
+    private RestTemplate restTemplate;
21
+
22
+    @Override
23
+    public void syncToTencent(AdLog adLog, AdPlatformConfig config) {
24
+        // 1. 构建请求参数
25
+        String payload = buildTencentPayload(adLog, config);
26
+
27
+        // 2. 创建请求头
28
+        HttpHeaders headers = new HttpHeaders();
29
+        headers.setContentType(MediaType.APPLICATION_JSON);
30
+        headers.setBearerAuth(config.getAuthToken());
31
+
32
+        // 3. 发送请求
33
+        HttpEntity<String> entity = new HttpEntity<>(payload, headers);
34
+        ResponseEntity<String> response = restTemplate.postForEntity(
35
+                config.getApiUrl(), entity, String.class);
36
+
37
+        // 4. 处理响应
38
+        if (response.getStatusCode() != HttpStatus.OK) {
39
+            throw new ServiceException("腾讯广告同步失败: " + response.getBody());
40
+        }
41
+        log.info("腾讯广告同步成功: chapter={}, user={}", adLog.getChapterId(), adLog.getUserId());
42
+    }
43
+
44
+    private String buildTencentPayload(AdLog adLog, AdPlatformConfig config) {
45
+        // 使用模板生成请求体
46
+        return config.getParamsTemplate()
47
+                .replace("${userId}", adLog.getUserId().toString())
48
+                .replace("${chapterId}", adLog.getChapterId().toString())
49
+                .replace("${timestamp}", String.valueOf(System.currentTimeMillis()));
50
+    }
51
+
52
+    @Override
53
+    public void syncToPangle(AdLog adLog, AdPlatformConfig config) {
54
+        // 1. 构建请求URL
55
+        String url = buildPangleUrl(adLog, config);
56
+
57
+        // 2. 创建请求头
58
+        HttpHeaders headers = new HttpHeaders();
59
+        headers.setBearerAuth(config.getAuthToken());
60
+
61
+        // 3. 发送请求
62
+        HttpEntity<?> entity = new HttpEntity<>(headers);
63
+        ResponseEntity<String> response = restTemplate.exchange(
64
+                url, HttpMethod.GET, entity, String.class);
65
+
66
+        // 4. 处理响应
67
+        if (response.getStatusCode() != HttpStatus.OK) {
68
+            throw new ServiceException("穿山甲同步失败: " + response.getBody());
69
+        }
70
+        log.info("穿山甲同步成功: chapter={}, user={}", adLog.getChapterId(), adLog.getUserId());
71
+    }
72
+
73
+    private String buildPangleUrl(AdLog adLog, AdPlatformConfig config) {
74
+        // 构建带参数的URL
75
+        return UriComponentsBuilder.fromHttpUrl(config.getApiUrl())
76
+                .queryParam("user", adLog.getUserId())
77
+                .queryParam("content_id", adLog.getChapterId())
78
+                .queryParam("event", "ad_view")
79
+                .queryParam("ts", System.currentTimeMillis())
80
+                .toUriString();
81
+    }
82
+}

+ 73
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/ChapterServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,73 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.ruoyi.novel.domain.NovelChapter;
5
+import com.ruoyi.novel.domain.NovelContent;
6
+import com.ruoyi.novel.mapper.NovelChapterMapper;
7
+import com.ruoyi.novel.mapper.NovelContentMapper;
8
+import com.ruoyi.novel.service.ChapterService;
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.Arrays;
14
+import java.util.Date;
15
+import java.util.List;
16
+
17
+// ChapterServiceImpl.java
18
+@Service
19
+public class ChapterServiceImpl implements ChapterService {
20
+
21
+    @Autowired
22
+    private NovelChapterMapper chapterMapper;
23
+
24
+    @Autowired
25
+    private NovelContentMapper contentMapper;
26
+
27
+    @Override
28
+    public List<NovelChapter> selectChapterListByNovelId(Long novelId) {
29
+        return chapterMapper.selectList(
30
+                new QueryWrapper<NovelChapter>()
31
+                        .eq("novel_id", novelId)
32
+                        .orderByAsc("chapter_order")
33
+        );
34
+    }
35
+
36
+    @Override
37
+    @Transactional
38
+    public int saveChapter(NovelChapter chapter, String content) {
39
+        if (chapter.getId() == null) {
40
+            // 新增章节
41
+            chapter.setCreateTime(new Date());
42
+            chapterMapper.insert(chapter);
43
+
44
+            NovelContent novelContent = new NovelContent();
45
+            novelContent.setChapterId(chapter.getId());
46
+            novelContent.setContent(content);
47
+            return contentMapper.insert(novelContent);
48
+        } else {
49
+            // 更新章节
50
+            chapter.setUpdateTime(new Date());
51
+            chapterMapper.updateById(chapter);
52
+
53
+            NovelContent novelContent = new NovelContent();
54
+            novelContent.setChapterId(chapter.getId());
55
+            novelContent.setContent(content);
56
+            return contentMapper.updateById(novelContent);
57
+        }
58
+    }
59
+
60
+    @Override
61
+    public NovelChapter getChapterById(Long chapterId) {
62
+        return chapterMapper.selectById(chapterId);
63
+    }
64
+
65
+    @Override
66
+    @Transactional
67
+    public int deleteChapters(Long[] ids) {
68
+        // 删除章节内容
69
+        contentMapper.deleteBatchIds(Arrays.asList(ids));
70
+        // 删除章节
71
+        return chapterMapper.deleteBatchIds(Arrays.asList(ids));
72
+    }
73
+}

+ 210
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/FinanceServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,210 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
4
+import com.ruoyi.common.exception.ServiceException;
5
+import com.ruoyi.novel.domain.AdLog;
6
+import com.ruoyi.novel.domain.RoyaltyReport;
7
+import com.ruoyi.novel.domain.RoyaltySummary;
8
+import com.ruoyi.novel.mapper.AdLogMapper;
9
+import com.ruoyi.novel.mapper.RoyaltySummaryMapper;
10
+import com.ruoyi.novel.service.IAdService;
11
+import com.ruoyi.novel.service.IFinanceService;
12
+import org.slf4j.Logger;
13
+import org.slf4j.LoggerFactory;
14
+import org.springframework.beans.factory.annotation.Autowired;
15
+import org.springframework.beans.factory.annotation.Value;
16
+import org.springframework.http.*;
17
+import org.springframework.scheduling.annotation.Scheduled;
18
+import org.springframework.stereotype.Service;
19
+import org.springframework.web.client.RestTemplate;
20
+
21
+import java.math.BigDecimal;
22
+import java.util.Arrays;
23
+import java.util.List;
24
+
25
+// FinanceServiceImpl.java
26
+@Service
27
+public class FinanceServiceImpl implements IFinanceService {
28
+    private static final Logger logger = LoggerFactory.getLogger(FinanceServiceImpl.class);
29
+    @Autowired
30
+    private RoyaltySummaryMapper royaltySummaryMapper;
31
+    @Value("${royalty.base.rate:0.15}")
32
+    private BigDecimal baseRoyaltyRate;
33
+
34
+    @Value("${handling.fee.rate:0.05}")
35
+    private BigDecimal handlingFeeRate;
36
+    @Value("${royalty.rate}")
37
+    private BigDecimal royaltyRate;
38
+    // 添加配置项注入
39
+    @Value("${php.data.url}")
40
+    private String phpDataUrl;
41
+
42
+    @Autowired
43
+    private AdLogMapper adLogMapper; // 确保已注入
44
+
45
+    @Autowired
46
+    private IAdService adService; // 确保已注入
47
+
48
+    //@Value("${handling.fee.rate}")
49
+    //private BigDecimal handlingFeeRate;
50
+
51
+    @Override
52
+    public RoyaltyReport calculateRoyalty(Long authorId, String month) {
53
+        // 1. 从PHP系统拉取数据(补充缺失的调用)
54
+//        RoyaltySummary summary = phpApiClient.getAuthorRoyaltySummary(authorId, month);
55
+//        // 2. 计算分成金额(补充业务逻辑)
56
+//        BigDecimal royaltyRate = getRoyaltyRate(authorId);
57
+//        BigDecimal grossAmount = summary.getTotalReads().multiply(royaltyRate);
58
+//        BigDecimal handlingFee = grossAmount.multiply(handlingFeeRate);
59
+//        BigDecimal netAmount = grossAmount.subtract(handlingFee);
60
+//        // 3. 构建报表对象
61
+//        return new RoyaltyReport(authorId, month, summary.getTotalReads(),
62
+//                royaltyRate, handlingFeeRate, handlingFee, netAmount);
63
+        // 1. 从PHP主库获取基础数据(假设已通过定时任务同步)
64
+        RoyaltySummary summary = royaltySummaryMapper.selectByAuthorAndMonth(authorId, month);
65
+
66
+//        if (summary == null) {
67
+//            throw new ServiceException("No data found for author: " + authorId + " in " + month);
68
+//        }
69
+        if (summary == null) {
70
+            throw new ServiceException("未找到分账数据: author=" + authorId + ", month=" + month);
71
+        }
72
+        // 2. 计算实际分成比例(可根据作者等级调整)
73
+        BigDecimal actualRate = getRoyaltyRate(authorId);
74
+        // 3. 构建报表
75
+        RoyaltyReport report = new RoyaltyReport();
76
+        report.setAuthorId(authorId);
77
+        report.setMonth(month);
78
+        report.calculate(summary.getTotalReads(), actualRate, handlingFeeRate);
79
+
80
+        return report;
81
+
82
+        // 2. 计算分账金额
83
+//        BigDecimal rawAmount = summary.getTotalReads().multiply(royaltyRate);
84
+//        BigDecimal handlingFee = rawAmount.multiply(handlingFeeRate);
85
+//        BigDecimal netAmount = rawAmount.subtract(handlingFee);
86
+//
87
+//        // 3. 构建报表
88
+//        return new RoyaltyReport(
89
+//                authorId,
90
+//                month,
91
+//                summary.getTotalReads(),
92
+//                royaltyRate,
93
+//                handlingFeeRate,
94
+//                handlingFee,
95
+//                netAmount
96
+//        );
97
+    }
98
+
99
+    @Override
100
+    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
101
+    public void syncRoyaltyData() {
102
+        logger.info("开始同步分账数据...");
103
+        try {
104
+            // 1. 从PHP主库获取数据
105
+            List<RoyaltySummary> summaries = fetchFromPhpSystem();
106
+
107
+            // 2. 更新到本地数据库
108
+            summaries.forEach(summary -> {
109
+                royaltySummaryMapper.upsertSummary(summary);
110
+            });
111
+
112
+            logger.info("同步完成: {}条记录", summaries.size());
113
+        } catch (Exception e) {
114
+            logger.error("分账数据同步失败: {}", e.getMessage());
115
+        }
116
+    }
117
+
118
+//    private List<RoyaltySummary> fetchFromPhpSystem() {
119
+//        // 实现从PHP系统获取数据的逻辑
120
+//        // 返回模拟数据
121
+//        List<RoyaltySummary> list = new ArrayList<>();
122
+//        list.add(new RoyaltySummary(1001L, "2023-10", new BigDecimal("15000.00")));
123
+//        list.add(new RoyaltySummary(1002L, "2023-10", new BigDecimal("8500.00")));
124
+//        return list;
125
+//    }
126
+
127
+    // 在FinanceServiceImpl中补充完整数据同步
128
+    private List<RoyaltySummary> fetchFromPhpSystem() {
129
+        try {
130
+            // 使用RestTemplate从PHP系统获取数据
131
+            RestTemplate restTemplate = new RestTemplate();
132
+
133
+            // 设置认证头
134
+            HttpHeaders headers = new HttpHeaders();
135
+            headers.setBasicAuth("api_user", "api_password");
136
+
137
+            // 构建请求
138
+            HttpEntity<String> entity = new HttpEntity<>(headers);
139
+            ResponseEntity<RoyaltySummary[]> response = restTemplate.exchange(
140
+                    phpDataUrl + "/royalty/summary",
141
+                    HttpMethod.GET,
142
+                    entity,
143
+                    RoyaltySummary[].class
144
+            );
145
+
146
+            if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
147
+                return Arrays.asList(response.getBody());
148
+            }
149
+            throw new ServiceException("PHP系统返回错误状态码: " + response.getStatusCode());
150
+        } catch (Exception e) {
151
+            logger.error("从PHP系统获取分账数据失败: {}", e.getMessage());
152
+            throw new ServiceException("数据同步服务暂时不可用", e);
153
+        }
154
+    }
155
+
156
+    // 定时任务重试机制
157
+    @Scheduled(fixedDelayString = "${scheduled.ad-retry-interval}")
158
+    public void retryFailedAdSync() {
159
+        try {
160
+            List<AdLog> failedLogs = adLogMapper.selectList(
161
+                    new QueryWrapper<AdLog>().eq("sync_status", 2) // 2=失败
162
+            );
163
+
164
+            for (AdLog log : failedLogs) {
165
+                logger.info("重试广告同步: ID={}, 平台={}", log.getId(), log.getAdPlatform());
166
+                adService.syncWithAdPlatform(log);
167
+            }
168
+        } catch (Exception e) {
169
+            logger.error("广告同步重试任务失败: {}", e.getMessage());
170
+        }
171
+    }
172
+    @Override
173
+    public BigDecimal getRoyaltyRate(Long authorId) {
174
+        // 实际应根据作者等级计算
175
+        // 这里返回基础比例
176
+        return baseRoyaltyRate;
177
+    }
178
+
179
+    @Override
180
+    public BigDecimal getHandlingFeeRate() {
181
+        return handlingFeeRate;
182
+    }
183
+
184
+
185
+//    @Override
186
+//    public RoyaltyReport calculateRoyalty(Long authorId, String month) {
187
+//        // 1. 从PHP系统拉取数据(补充缺失的调用)
188
+//        RoyaltySummary summary = phpApiClient.getAuthorRoyaltySummary(authorId, month);
189
+//
190
+//        // 2. 计算分成金额(补充业务逻辑)
191
+//        BigDecimal royaltyRate = getRoyaltyRate(authorId);
192
+//        BigDecimal grossAmount = summary.getTotalReads().multiply(royaltyRate);
193
+//        BigDecimal handlingFee = grossAmount.multiply(handlingFeeRate);
194
+//        BigDecimal netAmount = grossAmount.subtract(handlingFee);
195
+//
196
+//        // 3. 构建报表对象
197
+//        return new RoyaltyReport(authorId, month, summary.getTotalReads(),
198
+//                royaltyRate, handlingFeeRate, handlingFee, netAmount);
199
+//    }
200
+//
201
+//    // 补充缺失的辅助方法
202
+//    private BigDecimal getRoyaltyRate(Long authorId) {
203
+//        Author author = authorService.getById(authorId);
204
+//        BigDecimal rate = baseRoyaltyRate;
205
+//        if (author.isVip()) {
206
+//            rate = rate.add(vipBonusRate); // VIP作者加成
207
+//        }
208
+//        return rate.min(new BigDecimal("0.35")); // 上限35%:cite[4]
209
+//    }
210
+}

+ 103
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/NovelSearchServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,103 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.ruoyi.novel.domain.Novel;
4
+import com.ruoyi.novel.mapper.NovelMapper;
5
+import com.ruoyi.novel.service.NovelSearchService;
6
+import org.elasticsearch.index.query.QueryBuilders;
7
+import org.slf4j.Logger;
8
+import org.slf4j.LoggerFactory;
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;
18
+import org.springframework.stereotype.Service;
19
+
20
+import javax.annotation.PostConstruct;
21
+import java.util.HashMap;
22
+import java.util.List;
23
+import java.util.Map;
24
+import java.util.stream.Collectors;
25
+
26
+// NovelSearchService.java
27
+@Service
28
+public class NovelSearchServiceImpl implements NovelSearchService {
29
+    private static final Logger logger = LoggerFactory.getLogger(NovelSearchServiceImpl.class);
30
+
31
+    @Autowired
32
+    private ElasticsearchRestTemplate elasticTemplate;
33
+    @Autowired
34
+    private NovelMapper novelMapper;
35
+
36
+    private static final String INDEX_NAME = "novels";
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
+    }
54
+    public List<Novel> searchNovels(String keyword) {
55
+        // 构建查询条件
56
+        NativeSearchQuery query = new NativeSearchQueryBuilder()
57
+                .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "author", "description"))
58
+                .build();
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());
102
+    }
103
+}

+ 53
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/NovelServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,53 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.ruoyi.novel.domain.Novel;
4
+import com.ruoyi.novel.mapper.NovelMapper;
5
+import com.ruoyi.novel.service.NovelService;
6
+import org.slf4j.Logger;
7
+import org.slf4j.LoggerFactory;
8
+import org.springframework.beans.factory.annotation.Autowired;
9
+import org.springframework.stereotype.Service;
10
+import org.springframework.transaction.annotation.Transactional;
11
+
12
+import java.util.Arrays;
13
+import java.util.Date;
14
+import java.util.List;
15
+
16
+// NovelServiceImpl.java
17
+@Service
18
+public class NovelServiceImpl implements NovelService {
19
+    private static final Logger logger = LoggerFactory.getLogger(NovelServiceImpl.class);
20
+
21
+    @Autowired
22
+    private NovelMapper novelMapper;
23
+
24
+    @Override
25
+    public List<Novel> selectNovelList(Novel novel) {
26
+        return novelMapper.selectNovelList(novel);
27
+    }
28
+
29
+    @Override
30
+    @Transactional
31
+    public int insertNovel(Novel novel) {
32
+        novel.setCreateTime(new Date());
33
+        return novelMapper.insertNovel(novel);
34
+    }
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
+
52
+    // 其他CRUD方法...
53
+}

+ 60
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/ReadingHistoryServiceImpl.java Dosyayı Görüntüle

@@ -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
+}

+ 231
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/service/impl/VoteServiceImpl.java Dosyayı Görüntüle

@@ -0,0 +1,231 @@
1
+package com.ruoyi.novel.service.impl;
2
+
3
+import com.google.common.util.concurrent.RateLimiter;
4
+import com.ruoyi.common.exception.ServiceException;
5
+import com.ruoyi.novel.domain.VoteRecord;
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.*;
15
+import org.springframework.scheduling.annotation.Async;
16
+import org.springframework.scheduling.annotation.Scheduled;
17
+import org.springframework.stereotype.Service;
18
+import org.springframework.web.client.RestClientException;
19
+import org.springframework.web.client.RestTemplate;
20
+
21
+import java.util.Date;
22
+import java.util.HashMap;
23
+import java.util.List;
24
+import java.util.Map;
25
+import java.util.concurrent.ConcurrentHashMap;
26
+import java.util.concurrent.atomic.AtomicInteger;
27
+
28
+// VoteServiceImpl.java
29
+@Service
30
+public class VoteServiceImpl implements VoteService {
31
+    private static final Logger logger = LoggerFactory.getLogger(VoteServiceImpl.class);
32
+
33
+    @Autowired
34
+    private VoteRecordMapper voteRecordMapper;
35
+    @Autowired
36
+    private StoryOptionMapper storyOptionMapper;
37
+    @Value("${deepseek.api.key}")
38
+    private String deepSeekApiKey;
39
+    @Value("${deepseek.api.url}")
40
+    private String deepSeekApiUrl; // 添加配置项注入
41
+    @Value("${php.notify.url}")
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
+
49
+    // 用户投票限流器 <userId, 计数器>
50
+    private final Map<Long, AtomicInteger> userVoteCounters = new ConcurrentHashMap<>();
51
+    private static final int DAILY_VOTE_LIMIT = 3;
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
+    }
71
+    @Override
72
+    public String processVote(VoteRequest request) {
73
+        // 1. 检查当日投票次数
74
+        AtomicInteger counter = userVoteCounters.computeIfAbsent(
75
+                request.getUserId(),
76
+                k -> new AtomicInteger(0)
77
+        );
78
+
79
+        if (counter.get() >= DAILY_VOTE_LIMIT) {
80
+            throw new ServiceException("今日投票次数已达上限");
81
+        }
82
+
83
+        // 2. 记录投票
84
+        VoteRecord record = new VoteRecord();
85
+        record.setUserId(request.getUserId());
86
+        record.setStoryId(request.getStoryId());
87
+        record.setOptionId(request.getOptionId());
88
+        record.setNotifyStatus(0); // 未通知状态
89
+        voteRecordMapper.insert(record);
90
+
91
+        // 3. 更新选项计数
92
+        storyOptionMapper.incrementOptionVote(request.getOptionId());
93
+
94
+        // 4. 异步生成内容
95
+        this.generateStoryContent(record);
96
+
97
+        counter.incrementAndGet();
98
+        return "投票成功,剧情生成中...";
99
+    }
100
+    @Override
101
+    @Async("voteTaskExecutor")
102
+    public void generateStoryContent(VoteRecord record) {
103
+        try {
104
+            String content = callDeepSeekApi(record);
105
+            record.setGeneratedContent(content);
106
+            voteRecordMapper.updateById(record);
107
+
108
+            // 通知PHP系统
109
+            this.notifyPhpSystem(record);
110
+        } catch (Exception e) {
111
+            logger.error("AI生成失败: {}", e.getMessage());
112
+        }
113
+    }
114
+    // 在VoteServiceImpl中补充完整API调用
115
+    private String callDeepSeekApi(VoteRecord record) {
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
+        }
145
+    }
146
+
147
+//    @Async
148
+//    @Override
149
+//    public void generateStoryContent(Long storyId, Long optionId) {
150
+//        // 调用DeepSeek API
151
+//        DeepSeekRequest request = new DeepSeekRequest(storyId, optionId);
152
+//        String apiUrl = "https://api.deepseek.com/v1/story/generate";
153
+//
154
+//        RestTemplate restTemplate = new RestTemplate();
155
+//        HttpHeaders headers = new HttpHeaders();
156
+//        headers.setBearerAuth(deepSeekApiKey);
157
+//
158
+//        HttpEntity<DeepSeekRequest> entity = new HttpEntity<>(request, headers);
159
+//        ResponseEntity<String> response = restTemplate.exchange(
160
+//                apiUrl, HttpMethod.POST, entity, String.class);
161
+//
162
+//        if (response.getStatusCode() == HttpStatus.OK) {
163
+//            return parseGeneratedContent(response.getBody());
164
+//        }
165
+//        throw new ServiceException("Failed to generate story content");
166
+//    }
167
+
168
+
169
+    @Async
170
+    @Override
171
+    public void notifyPhpSystem(VoteRequest request, String content) {
172
+        // 组装通知数据
173
+        Map<String, Object> payload = new HashMap<>();
174
+        payload.put("userId", request.getUserId());
175
+        payload.put("storyId", request.getStoryId());
176
+        payload.put("optionId", request.getOptionId());
177
+        payload.put("content", content);
178
+
179
+        // 发送异步通知
180
+        RestTemplate restTemplate = new RestTemplate();
181
+        restTemplate.postForObject(phpNotifyUrl, payload, String.class);
182
+
183
+        // 记录投票日志
184
+        VoteRecord record = new VoteRecord();
185
+        record.setUserId(request.getUserId());
186
+        record.setStoryId(request.getStoryId());
187
+        record.setOptionId(request.getOptionId());
188
+        record.setVoteTime(new Date());
189
+        record.setGeneratedContent(content);
190
+        voteRecordMapper.insertVoteRecord(record);
191
+    }
192
+
193
+
194
+    @Override
195
+    @Async("voteTaskExecutor")
196
+    public void notifyPhpSystem(VoteRecord record) {
197
+        try {
198
+            // 构建通知数据
199
+            Map<String, Object> payload = new HashMap<>();
200
+            payload.put("userId", record.getUserId());
201
+            payload.put("storyId", record.getStoryId());
202
+            payload.put("optionId", record.getOptionId());
203
+            payload.put("content", record.getGeneratedContent());
204
+
205
+            // 发送HTTP请求
206
+            RestTemplate restTemplate = new RestTemplate();
207
+            ResponseEntity<String> response = restTemplate.postForEntity(
208
+                    phpNotifyUrl, payload, String.class);
209
+
210
+            if (response.getStatusCode() == HttpStatus.OK) {
211
+                record.setNotifyStatus(1); // 通知成功
212
+            } else {
213
+                record.setNotifyStatus(2); // 通知失败
214
+            }
215
+        } catch (Exception e) {
216
+            logger.error("通知PHP系统失败: {}", e.getMessage());
217
+            record.setNotifyStatus(2);
218
+        }
219
+        voteRecordMapper.updateById(record);
220
+    }
221
+
222
+    @Override
223
+    @Scheduled(cron = "0 */10 * * * ?") // 每10分钟执行一次
224
+    public void retryFailedNotifications() {
225
+        List<VoteRecord> failedRecords = voteRecordMapper.selectUnnotifiedRecords();
226
+        failedRecords.forEach(record -> {
227
+            logger.info("重试通知PHP系统: voteId={}", record.getId());
228
+            this.notifyPhpSystem(record);
229
+        });
230
+    }
231
+}

+ 96
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/AdPlatformClient.java Dosyayı Görüntüle

@@ -0,0 +1,96 @@
1
+package com.ruoyi.novel.utils;
2
+
3
+import com.alibaba.fastjson2.JSONObject;
4
+import com.ruoyi.common.exception.ServiceException;
5
+import com.ruoyi.novel.domain.AdLog;
6
+import com.ruoyi.novel.domain.AdPlatformConfig;
7
+import com.ruoyi.novel.service.IAdService;
8
+
9
+import com.ruoyi.novel.service.impl.NovelSearchServiceImpl;
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.Qualifier;
14
+import org.springframework.beans.factory.annotation.Value;
15
+import org.springframework.http.*;
16
+import org.springframework.stereotype.Component;
17
+import org.springframework.web.client.RestTemplate;
18
+import java.util.HashMap;
19
+import java.util.Map;
20
+
21
+// AdPlatformClient.java
22
+@Component
23
+public class AdPlatformClient {
24
+    private static final Logger logger = LoggerFactory.getLogger(AdPlatformClient.class);
25
+
26
+    @Autowired
27
+    @Qualifier("adServiceImpl")
28
+    private IAdService iAdService;
29
+
30
+    @Autowired
31
+    private RestTemplate restTemplate;
32
+    @Value("${ad.tencent.url}")
33
+    private String tencentUrl;
34
+
35
+    @Value("${ad.pangle.url}")
36
+    private String pangleUrl;
37
+    public void syncAdPlatform(AdLog adLog) {
38
+        Map<String, AdPlatformConfig> configs = iAdService.loadAdConfigs();
39
+        AdPlatformConfig config = configs.get(adLog.getAdPlatform());
40
+
41
+        if (config == null) {
42
+            throw new ServiceException("广告平台配置不存在: " + adLog.getAdPlatform());
43
+        }
44
+
45
+        // 构建请求参数
46
+        String payload = buildPayload(adLog, config.getParamsTemplate());
47
+
48
+        // 设置请求头
49
+        HttpHeaders headers = new HttpHeaders();
50
+        headers.setContentType(MediaType.APPLICATION_JSON);
51
+        headers.setBearerAuth(config.getAuthToken());
52
+
53
+        // 发送请求
54
+        HttpEntity<String> entity = new HttpEntity<>(payload, headers);
55
+        ResponseEntity<String> response = restTemplate.exchange(
56
+                config.getApiUrl(), HttpMethod.POST, entity, String.class);
57
+
58
+        // 处理响应
59
+        if (!response.getStatusCode().is2xxSuccessful()) {
60
+            throw new ServiceException("广告平台同步失败: " + response.getBody());
61
+        }
62
+
63
+        logger.info("广告同步成功: logId={}, platform={}", adLog.getId(), adLog.getAdPlatform());
64
+    }
65
+    private String buildPayload(AdLog adLog, String template) {
66
+        // 使用占位符替换实际值
67
+        return template
68
+                .replace("${userId}", adLog.getUserId().toString())
69
+                .replace("${chapterId}", adLog.getChapterId().toString())
70
+                .replace("${timestamp}", String.valueOf(System.currentTimeMillis()));
71
+    }
72
+    public void syncTencent(AdLog adLog) {
73
+        Map<String, String> params = new HashMap<>();
74
+        params.put("user_id", adLog.getUserId().toString());
75
+        params.put("chapter_id", adLog.getChapterId().toString());
76
+        params.put("event_time", String.valueOf(adLog.getViewTime().getTime()));
77
+
78
+        RestTemplate restTemplate = new RestTemplate();
79
+        restTemplate.postForObject(tencentUrl, params, String.class);
80
+    }
81
+
82
+    public void syncPangle(AdLog adLog) {
83
+        // 穿山甲平台需要不同的参数格式
84
+        JSONObject payload = new JSONObject();
85
+        payload.put("user", adLog.getUserId());
86
+        payload.put("content_id", adLog.getChapterId());
87
+        payload.put("timestamp", System.currentTimeMillis());
88
+
89
+        RestTemplate restTemplate = new RestTemplate();
90
+        HttpHeaders headers = new HttpHeaders();
91
+        headers.setContentType(MediaType.APPLICATION_JSON);
92
+
93
+        HttpEntity<String> entity = new HttpEntity<>(payload.toString(), headers);
94
+        restTemplate.postForObject(pangleUrl, entity, String.class);
95
+    }
96
+}

+ 8
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/Constants.java Dosyayı Görüntüle

@@ -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
+}

+ 71
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/JwtUtil.java Dosyayı Görüntüle

@@ -0,0 +1,71 @@
1
+package com.ruoyi.novel.utils;
2
+
3
+import io.jsonwebtoken.Claims;
4
+import io.jsonwebtoken.Jwts;
5
+import io.jsonwebtoken.SignatureAlgorithm;
6
+import org.springframework.beans.factory.annotation.Value;
7
+
8
+import java.nio.charset.StandardCharsets;
9
+import java.util.Date;
10
+
11
+// JwtUtil.java
12
+public class JwtUtil {
13
+    private static String SECRET = "your_jwt_secret_key";
14
+    @Value("${jwt.secret}")
15
+    public void setSecret(String secret) {
16
+        SECRET = secret;
17
+    }
18
+
19
+    private static final long EXPIRE = 86400; // 24小时
20
+    public static String generateToken(Long userId) {
21
+        Date now = new Date();
22
+        Date expire = new Date(now.getTime() + EXPIRE * 1000);
23
+
24
+        return Jwts.builder()
25
+                .setHeaderParam("typ", "JWT")
26
+                .setSubject(userId.toString())
27
+                .setIssuedAt(now)
28
+                .setExpiration(expire)
29
+                .signWith(SignatureAlgorithm.HS512, SECRET)
30
+                .compact();
31
+    }
32
+
33
+    public static boolean verifyToken(String token, Long userId) {
34
+        try {
35
+            Claims claims = Jwts.parser()
36
+                    .setSigningKey(SECRET)
37
+                    .parseClaimsJws(token)
38
+                    .getBody();
39
+
40
+            String subject = claims.getSubject();
41
+            return userId.toString().equals(subject);
42
+        } catch (Exception e) {
43
+            return false;
44
+        }
45
+    }
46
+
47
+    public static Long getUserIdFromToken(String token) {
48
+        Claims claims = Jwts.parser()
49
+                .setSigningKey(SECRET)
50
+                .parseClaimsJws(token)
51
+                .getBody();
52
+        return Long.parseLong(claims.getSubject());
53
+    }
54
+
55
+    // 补充Token解析方法
56
+    public static Claims parseToken(String token) {
57
+        return Jwts.parser()
58
+                .setSigningKey(SECRET.getBytes(StandardCharsets.UTF_8))
59
+                .parseClaimsJws(token)
60
+                .getBody();
61
+    }
62
+
63
+    // 补充缺失的Token生成方法
64
+//    public static String generateToken(Long userId) {
65
+//        return Jwts.builder()
66
+//                .setSubject(userId.toString())
67
+//                .setExpiration(new Date(System.currentTimeMillis() + 86400 * 1000))
68
+//                .signWith(SignatureAlgorithm.HS512, SECRET)
69
+//                .compact();
70
+//    }
71
+}

+ 80
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/utils/NovelUtils.java Dosyayı Görüntüle

@@ -0,0 +1,80 @@
1
+package com.ruoyi.novel.utils;
2
+
3
+import java.util.HashSet;
4
+import java.util.Set;
5
+
6
+// NovelUtils.java
7
+public class NovelUtils {
8
+    
9
+    // 初始化敏感词库(实际项目中应从数据库或配置文件加载)
10
+    private static final Set<String> SENSITIVE_WORDS = new HashSet<>();
11
+
12
+    static {
13
+        // 添加敏感词示例
14
+        SENSITIVE_WORDS.add("暴力");
15
+        SENSITIVE_WORDS.add("色情");
16
+        SENSITIVE_WORDS.add("赌博");
17
+        SENSITIVE_WORDS.add("毒品");
18
+        // 可以添加更多敏感词...
19
+    }
20
+    // 敏感词过滤
21
+    public static String filterSensitiveWords(String content) {
22
+        // 实现敏感词过滤逻辑
23
+        if (content == null || content.isEmpty()) {
24
+            return content;  // 返回空内容
25
+        }
26
+
27
+        // 创建过滤后的字符串
28
+        StringBuilder filteredContent = new StringBuilder();
29
+
30
+        // 遍历每个字符
31
+        for (int i = 0; i < content.length(); i++) {
32
+            // 检查是否包含敏感词
33
+            boolean isSensitive = false;
34
+
35
+            // 检查当前字符开始的每个可能子串
36
+            for (int j = i + 1; j <= content.length(); j++) {
37
+                String sub = content.substring(i, j);
38
+                if (SENSITIVE_WORDS.contains(sub)) {
39
+                    // 替换敏感词为星号
40
+                    filteredContent.append("*".repeat(sub.length()));
41
+                    i = j - 1;  // 跳过已处理的敏感词
42
+                    isSensitive = true;
43
+                    break;
44
+                }
45
+            }
46
+
47
+            // 如果不是敏感词,添加原始字符
48
+            if (!isSensitive) {
49
+                filteredContent.append(content.charAt(i));
50
+            }
51
+        }
52
+
53
+        return filteredContent.toString();  // 返回过滤后的内容
54
+    }
55
+
56
+    // 自动生成章节序号
57
+    // 自动生成章节序号 - 完善版本
58
+    public static Integer generateChapterOrder(Long novelId) {
59
+        // 1. 验证输入
60
+        if (novelId == null || novelId <= 0) {
61
+            throw new IllegalArgumentException("无效的小说ID");
62
+        }
63
+
64
+        // 2. 查询数据库获取最新章节序号
65
+        try {
66
+            Integer latestOrder = chapterService.findLatestChapterOrder(novelId);
67
+
68
+            // 3. 如果还没有章节,从1开始
69
+            if (latestOrder == null) {
70
+                return 1;
71
+            }
72
+
73
+            // 4. 返回最新序号+1
74
+            return latestOrder + 1;
75
+        } catch (Exception e) {
76
+            // 5. 处理异常情况
77
+            throw new RuntimeException("生成章节序号失败: " + e.getMessage(), e);
78
+        }
79
+    }
80
+}

+ 43
- 0
RuoYi-Vue/ruoyi-system/src/main/java/com/ruoyi/novel/vip/VipAccessAspect.java Dosyayı Görüntüle

@@ -0,0 +1,43 @@
1
+package com.ruoyi.novel.vip;
2
+
3
+
4
+import com.ruoyi.common.annotation.VipAccess;
5
+import com.ruoyi.common.exception.ServiceException;
6
+import com.ruoyi.common.utils.SecurityUtils;
7
+import com.ruoyi.system.service.ISysUserService;
8
+import org.aspectj.lang.JoinPoint;
9
+import org.aspectj.lang.annotation.Aspect;
10
+import org.aspectj.lang.annotation.Before;
11
+import org.springframework.beans.factory.annotation.Autowired;
12
+import org.springframework.stereotype.Component;
13
+
14
+@Aspect
15
+@Component
16
+public class VipAccessAspect {
17
+
18
+    // 修复1:添加@Autowired注解注入服务
19
+    @Autowired
20
+    private ISysUserService userService;
21
+
22
+    // 修复2:添加JoinPoint参数并修正注解表达式
23
+    @Before("@annotation(vipAccess)")
24
+    public void checkVipAccess(JoinPoint joinPoint, VipAccess vipAccess) {
25
+        // 修复3:完整路径引用SecurityUtils
26
+        Long userId = SecurityUtils.getUserId();
27
+
28
+        // 修复4:添加空值检查防止NPE
29
+        if (userId == null) {
30
+            throw new ServiceException("用户未登录,无法访问VIP功能");
31
+        }
32
+
33
+        // 修复5:添加服务空值检查
34
+        if (userService == null) {
35
+            throw new ServiceException("用户服务未初始化");
36
+        }
37
+
38
+        // 修复6:添加详细的错误信息
39
+        if (!userService.isVip(userId)) {
40
+            throw new ServiceException("仅VIP用户可访问此功能,当前用户ID: " + userId);
41
+        }
42
+    }
43
+}

+ 27
- 0
RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/AdLogMapper.xml Dosyayı Görüntüle

@@ -0,0 +1,27 @@
1
+<!-- AdLogMapper.xml -->
2
+<mapper namespace="com.ruoyi.novel.mapper.AdLogMapper">
3
+
4
+    <resultMap id="AdLogResult" type="AdLog">
5
+        <id property="id" column="id" />
6
+        <result property="userId" column="user_id" />
7
+        <result property="chapterId" column="chapter_id" />
8
+        <result property="adPlatform" column="ad_platform" />
9
+        <result property="viewTime" column="view_time" />
10
+        <result property="deviceInfo" column="device_info" />
11
+        <result property="syncStatus" column="sync_status" />
12
+    </resultMap>
13
+
14
+    <sql id="selectAdLogVo">
15
+        SELECT id, user_id, chapter_id, ad_platform, view_time, device_info, sync_status
16
+        FROM novel_ad_log
17
+    </sql>
18
+
19
+    <update id="updateSyncStatus">
20
+        UPDATE novel_ad_log
21
+        SET sync_status = #{status}
22
+        WHERE id IN
23
+        <foreach item="id" collection="ids" open="(" separator="," close=")">
24
+            #{id}
25
+        </foreach>
26
+    </update>
27
+</mapper>

+ 17
- 0
RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/RoyaltySummaryMapper.xml Dosyayı Görüntüle

@@ -0,0 +1,17 @@
1
+<!-- RoyaltySummaryMapper.xml -->
2
+<mapper namespace="com.ruoyi.novel.mapper.RoyaltySummaryMapper">
3
+
4
+    <resultMap id="RoyaltySummaryResult" type="RoyaltySummary">
5
+        <id property="authorId" column="author_id" />
6
+        <id property="month" column="month" />
7
+        <result property="totalReads" column="total_reads" />
8
+        <result property="lastSyncTime" column="last_sync_time" />
9
+    </resultMap>
10
+
11
+    <select id="selectMonthlySummary" resultMap="RoyaltySummaryResult">
12
+        SELECT author_id, month, SUM(total_reads) AS total_reads
13
+        FROM novel_royalty_detail
14
+        WHERE month = #{month}
15
+        GROUP BY author_id
16
+    </select>
17
+</mapper>

+ 26
- 0
RuoYi-Vue/ruoyi-system/src/main/resources/mapper/novel/VoteRecordMapper.xml Dosyayı Görüntüle

@@ -0,0 +1,26 @@
1
+<!-- VoteRecordMapper.xml -->
2
+<mapper namespace="com.ruoyi.novel.mapper.VoteRecordMapper">
3
+
4
+    <resultMap id="VoteRecordResult" type="VoteRecord">
5
+        <id property="id" column="id" />
6
+        <result property="userId" column="user_id" />
7
+        <result property="storyId" column="story_id" />
8
+        <result property="optionId" column="option_id" />
9
+        <result property="voteTime" column="vote_time" />
10
+        <result property="generatedContent" column="generated_content"
11
+                typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
12
+        <result property="notifyStatus" column="notify_status" />
13
+    </resultMap>
14
+
15
+    <update id="incrementOptionVote">
16
+        UPDATE novel_story_option
17
+        SET vote_count = vote_count + 1
18
+        WHERE id = #{optionId}
19
+    </update>
20
+
21
+    <update id="updateNotifyStatus">
22
+        UPDATE novel_vote_log
23
+        SET notify_status = #{status}
24
+        WHERE id = #{id}
25
+    </update>
26
+</mapper>

Loading…
İptal
Kaydet