Bläddra i källkod

feat: 新增小说服务模块

hidewnd 1 månad sedan
förälder
incheckning
ced8c8b33d
52 ändrade filer med 4694 tillägg och 0 borttagningar
  1. 1 0
      pom.xml
  2. 51 0
      yt-common/src/main/java/com/ytpm/app/model/YtNovel.java
  3. 29 0
      yt-common/src/main/java/com/ytpm/app/model/YtNovelCategory.java
  4. 40 0
      yt-common/src/main/java/com/ytpm/app/model/YtNovelChapter.java
  5. 39 0
      yt-common/src/main/java/com/ytpm/app/model/YtNovelReadRecord.java
  6. 41 0
      yt-common/src/main/java/com/ytpm/app/model/YtNovelShelf.java
  7. 18 0
      yt-common/src/main/java/com/ytpm/app/model/YtNovelUser.java
  8. 18 0
      yt-common/src/main/java/com/ytpm/app/param/AdRiskParam.java
  9. 122 0
      yt-gateway/src/main/resources/logback-spring.xml
  10. 126 0
      yt-novel/pom.xml
  11. 32 0
      yt-novel/yt-novel-feign/pom.xml
  12. 9 0
      yt-novel/yt-novel-feign/src/main/java/com/ytpm/novel/feign/base/BaseNovelFeign.java
  13. 15 0
      yt-novel/yt-novel-feign/src/main/java/com/ytpm/novel/feign/feign/NovelFeign.java
  14. 95 0
      yt-novel/yt-novel-service/pom.xml
  15. 30 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/NovelApplication.java
  16. 38 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/aop/DataSourceAspect.java
  17. 37 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/YtWebMvcConfigurerAdapter.java
  18. 58 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/DataSourceConfig.java
  19. 10 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/DynamicDataSource.java
  20. 20 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/DynamicDataSourceContextHolder.java
  21. 38 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/MyBatisConfig.java
  22. 54 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/interceptor/HttpInterceptor.java
  23. 29 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/redis/RedisListenerConfig.java
  24. 40 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/swagger/SwaggerConfig.java
  25. 95 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/controller/AdController.java
  26. 76 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/controller/NovelController.java
  27. 83 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/controller/VisitorController.java
  28. 85 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/AdRecordMapper.java
  29. 209 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/AppUserMapper.java
  30. 32 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/LoginRecordMapper.java
  31. 63 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/NovelMapper.java
  32. 156 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/handle/CustomerExceptionHandler.java
  33. 27 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/dto/UserStaticParam.java
  34. 30 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/dto/YtNovelDto.java
  35. 19 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/param/NovelPageParam.java
  36. 28 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/param/NovelRecordParam.java
  37. 20 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/param/NovelShelfParam.java
  38. 21 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/view/AgentNetworkAgg.java
  39. 110 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/monitor/RedisKeyExpirationListener.java
  40. 293 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/redis/RedisService.java
  41. 57 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/AdService.java
  42. 19 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/AppUserService.java
  43. 36 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/NovelService.java
  44. 320 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/AdServiceImpl.java
  45. 176 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/AppUserServiceImpl.java
  46. 146 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/NovelServiceImpl.java
  47. 228 0
      yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/VisitorLoginServiceImpl.java
  48. 64 0
      yt-novel/yt-novel-service/src/main/resources/bootstrap.yml
  49. 280 0
      yt-novel/yt-novel-service/src/main/resources/mapper/AdRecordMapper.xml
  50. 822 0
      yt-novel/yt-novel-service/src/main/resources/mapper/AppUserMapper.xml
  51. 90 0
      yt-novel/yt-novel-service/src/main/resources/mapper/LoginRecordMapper.xml
  52. 119 0
      yt-novel/yt-novel-service/src/main/resources/mapper/NovelMapper.xml

+ 1 - 0
pom.xml

@@ -19,6 +19,7 @@
         <module>yt-ios-lemon</module>
         <module>nacos-monitor</module>
         <module>yt-question</module>
+        <module>yt-novel</module>
     </modules>
 
     <properties>

+ 51 - 0
yt-common/src/main/java/com/ytpm/app/model/YtNovel.java

@@ -0,0 +1,51 @@
+package com.ytpm.app.model;
+
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author lih
+ * @date 2025-09-30 13:46
+ */
+@Data
+@ApiModel("小说信息")
+public class YtNovel {
+    @ApiModelProperty("小说ID")
+    private String novelId;
+    @ApiModelProperty("小说名称")
+    private String novelName;
+    @ApiModelProperty("小说作者")
+    private String novelAuthor;
+    @ApiModelProperty("小说简介")
+    private String novelIntroduction;
+
+    @ApiModelProperty("小说字数")
+    private Long novelWords;
+    @ApiModelProperty("阅读数")
+    private Long novelReads;
+    @ApiModelProperty("封面")
+    private String novelImg;
+    @ApiModelProperty("整篇小说地址")
+    private String novelUrl;
+    @ApiModelProperty("书籍id(第三方APIid)")
+    private String bookId;
+
+    @ApiModelProperty("小说上下架状态|0未上架|1上架")
+    private Integer status;
+    @ApiModelProperty("连载状态|0连载中|1完结")
+    private Integer state;
+    @ApiModelProperty("删除标志|0未删除|1已删除")
+    private Integer deleted;
+
+    private String createBy;
+    private Date createTime;
+    private String updateBy;
+    private Date updateTime;
+
+    @ApiModelProperty("乐观锁ID")
+    private Integer revision;
+}

+ 29 - 0
yt-common/src/main/java/com/ytpm/app/model/YtNovelCategory.java

@@ -0,0 +1,29 @@
+package com.ytpm.app.model;
+
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author lih
+ * @date 2025-09-30 11:22
+ */
+@Data
+@ApiModel("小说分类")
+public class YtNovelCategory {
+
+    @ApiModelProperty("分类ID")
+    private String categoryId;
+
+    @ApiModelProperty("分类名称")
+    private String categoryName;
+
+    @ApiModelProperty("分类介绍")
+    private String categoryDesc;
+
+    @ApiModelProperty("排序号")
+    private Integer sort;
+
+
+}

+ 40 - 0
yt-common/src/main/java/com/ytpm/app/model/YtNovelChapter.java

@@ -0,0 +1,40 @@
+package com.ytpm.app.model;
+
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author lih
+ * @date 2025-09-30 13:53
+ */
+
+@Data
+public class YtNovelChapter {
+
+    @ApiModelProperty("章节ID")
+    private String chapterId;
+    @ApiModelProperty("小说ID")
+    private String novelId;
+    @ApiModelProperty("章节数")
+    private Integer chapter;
+    @ApiModelProperty("章节名称")
+    private String chapterName;
+    @ApiModelProperty("章节内容链接")
+    private String content;
+    @ApiModelProperty("章节内容")
+    private String contentText;
+    @ApiModelProperty("删除标志|0未删除|1已删除")
+    private Integer deleted;
+
+    private String createBy;
+    private Date createTime;
+    private String updateBy;
+    private Date updateTime;
+
+    @ApiModelProperty("乐观锁ID")
+    private Integer revision;
+
+}

+ 39 - 0
yt-common/src/main/java/com/ytpm/app/model/YtNovelReadRecord.java

@@ -0,0 +1,39 @@
+package com.ytpm.app.model;
+
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author lih
+ * @date 2025-09-30 14:03
+ */
+@Data
+@ApiModel("小说用户阅读记录")
+public class YtNovelReadRecord {
+
+    @ApiModelProperty("记录ID")
+    private String recordId;
+    @ApiModelProperty("用户ID")
+    private String userId;
+    @ApiModelProperty("小说ID")
+    private String novelId;
+    @ApiModelProperty("小说章节ID")
+    private String chapterId;
+    @ApiModelProperty("小说名称")
+    private String novelName;
+    @ApiModelProperty("章节名称")
+    private String novelChapterName;
+    @ApiModelProperty("阅读时间")
+    private Date readTime;
+    @ApiModelProperty("删除标志|0未删除|1已删除")
+    private Integer deleted;
+
+    private String createBy;
+    private Date createTime;
+    private String updateBy;
+    private Date updateTime;
+}

+ 41 - 0
yt-common/src/main/java/com/ytpm/app/model/YtNovelShelf.java

@@ -0,0 +1,41 @@
+package com.ytpm.app.model;
+
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author lih
+ * @date 2025-09-30 11:24
+ */
+@Data
+@ApiModel("小说书架")
+public class YtNovelShelf {
+
+    @ApiModelProperty("书架ID")
+    private String shelfId;
+
+    @ApiModelProperty("用户ID")
+    private String userId;
+
+    @ApiModelProperty("书架名称")
+    private String shelfName;
+
+    @ApiModelProperty("小说ID集合")
+    private String novelIds;
+
+    @ApiModelProperty("删除标志|0未删除|1已删除")
+    private Integer deleted;
+
+    @ApiModelProperty("乐观锁ID")
+    private Integer revision;
+
+    private String createBy;
+    private Date createTime;
+    private String updateBy;
+    private Date updateTime;
+
+}

+ 18 - 0
yt-common/src/main/java/com/ytpm/app/model/YtNovelUser.java

@@ -0,0 +1,18 @@
+package com.ytpm.app.model;
+
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * @author lih
+ * @date 2025-09-30 09:38
+ */
+@Data
+@ApiModel("小说用户信息")
+@EqualsAndHashCode(callSuper = true)
+public class YtNovelUser extends YtUser {
+
+
+}

+ 18 - 0
yt-common/src/main/java/com/ytpm/app/param/AdRiskParam.java

@@ -0,0 +1,18 @@
+package com.ytpm.app.param;
+
+
+import com.ytpm.app.model.YtDyzAdRecord;
+import com.ytpm.app.model.YtUser;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025-09-30 11:27
+ */
+@Data
+public class AdRiskParam {
+    private YtUser ytUser;
+    private List<YtDyzAdRecord> adRecordList;
+}

+ 122 - 0
yt-gateway/src/main/resources/logback-spring.xml

@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+    <!-- 日志根目录-->
+    <springProperty scope="context" name="LOG_HOME" source="logging.path" defaultValue="/data/book/log"/>
+
+    <!-- 日志级别 -->
+    <springProperty scope="context" name="LOG_ROOT_LEVEL" source="logging.level.root" defaultValue="INFO"/>
+
+    <!--  标识这个"STDOUT" 将会添加到这个logger -->
+    <springProperty scope="context" name="STDOUT" source="log.stdout" defaultValue="STDOUT"/>
+
+    <!-- 日志文件名称-->
+    <property name="LOG_PREFIX" value="YT-book-logback" />
+
+    <!-- 日志文件编码-->
+    <property name="LOG_CHARSET" value="UTF-8" />
+
+    <!-- 日志文件路径+日期-->
+    <property name="LOG_DIR" value="${LOG_HOME}/%d{yyyyMMdd}" />
+
+    <!--对日志进行格式化-->
+    <property name="LOG_MSG" value="- | [%X{requestUUID}] | [%d{yyyyMMdd HH:mm:ss.SSS}] | [%level] | [${HOSTNAME}] | [%thread] | [%logger{36}] | --> %msg|%n "/>
+
+    <!--文件大小,默认10MB-->
+    <property name="MAX_FILE_SIZE" value="50MB" />
+
+    <!-- 配置日志的滚动时间 ,表示只保留最近 10 天的日志-->
+    <property name="MAX_HISTORY" value="180"/>
+
+    <!--输出到控制台-->
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- 输出的日志内容格式化-->
+        <layout class="ch.qos.logback.classic.PatternLayout">
+            <pattern>${LOG_MSG}</pattern>
+        </layout>
+    </appender>
+
+    <!--输出到文件-->
+    <appender name="0" class="ch.qos.logback.core.rolling.RollingFileAppender">
+    </appender>
+
+    <!-- 定义 ALL 日志的输出方式:-->
+    <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!--日志文件路径,日志文件名称-->
+        <File>${LOG_HOME}/all_${LOG_PREFIX}.log</File>
+
+        <!-- 设置滚动策略,当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
+            <!--日志文件路径,新的 ALL 日志文件名称,“ i ” 是个变量 -->
+            <FileNamePattern>${LOG_DIR}/all_${LOG_PREFIX}%i.log</FileNamePattern>
+
+            <!-- 配置日志的滚动时间 ,表示只保留最近 10 天的日志-->
+            <MaxHistory>${MAX_HISTORY}</MaxHistory>
+
+            <!--当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB-->
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+
+        </rollingPolicy>
+
+        <!-- 输出的日志内容格式化-->
+        <layout class="ch.qos.logback.classic.PatternLayout">
+            <pattern>${LOG_MSG}</pattern>
+        </layout>
+    </appender>
+
+    <!-- 定义 ERROR 日志的输出方式:-->
+    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 下面为配置只输出error级别的日志 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <OnMismatch>DENY</OnMismatch>
+            <OnMatch>ACCEPT</OnMatch>
+        </filter>
+        <!--日志文件路径,日志文件名称-->
+        <File>${LOG_HOME}/err_${LOG_PREFIX}.log</File>
+
+        <!-- 设置滚动策略,当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+
+            <!--日志文件路径,新的 ERR 日志文件名称,“ i ” 是个变量 -->
+            <FileNamePattern>${LOG_DIR}/err_${LOG_PREFIX}%i.log</FileNamePattern>
+
+            <!-- 配置日志的滚动时间 ,表示只保留最近 10 天的日志-->
+            <MaxHistory>${MAX_HISTORY}</MaxHistory>
+
+            <!--当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB-->
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+
+        <!-- 输出的日志内容格式化-->
+        <layout class="ch.qos.logback.classic.PatternLayout">
+            <Pattern>${LOG_MSG}</Pattern>
+        </layout>
+    </appender>
+
+    <!-- additivity 设为false,则logger内容不附加至root ,配置以配置包下的所有类的日志的打印,级别是 ERROR-->
+    <logger name="org.springframework"     level="ERROR" />
+    <logger name="org.apache.commons"      level="ERROR" />
+    <logger name="org.apache.zookeeper"    level="ERROR"  />
+    <logger name="com.alibaba.dubbo.monitor" level="ERROR"/>
+    <logger name="com.alibaba.dubbo.remoting" level="ERROR" />
+
+    <!-- ${LOG_ROOT_LEVEL} 日志级别 -->
+    <root level="${LOG_ROOT_LEVEL}">
+
+        <!-- 标识这个"${STDOUT}"将会添加到这个logger -->
+        <appender-ref ref="${STDOUT}"/>
+
+        <!-- FILE_ALL 日志输出添加到 logger -->
+        <appender-ref ref="FILE_ALL"/>
+
+        <!-- FILE_ERROR 日志输出添加到 logger -->
+        <appender-ref ref="FILE_ERROR"/>
+    </root>
+
+</configuration>

+ 126 - 0
yt-novel/pom.xml

@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.ytpm</groupId>
+        <artifactId>yt_platform</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>yt-novel</artifactId>
+    <name>yt-novel</name>
+    <description>子服务-小说模式</description>
+    <modules>
+        <module>yt-novel-feign</module>
+        <module>yt-novel-service</module>
+    </modules>
+    <packaging>pom</packaging>
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+            <version>1.4.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>guava</artifactId>
+                    <groupId>com.google.guava</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>jsr305</artifactId>
+                    <groupId>com.google.code.findbugs</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-log4j2</artifactId>
+        </dependency>
+
+        <!--Spring Cloud Alibaba-->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>guava</artifactId>
+                    <groupId>com.google.guava</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>
+
+        <!--database-->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>jsqlparser</artifactId>
+                    <groupId>com.github.jsqlparser</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>com.ytpm</groupId>
+            <artifactId>yt-common</artifactId>
+            <version>1.0-SNAPSHOT</version>
+            <exclusions>
+                <exclusion>
+                    <artifactId>guava</artifactId>
+                    <groupId>com.google.guava</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>jsqlparser</artifactId>
+                    <groupId>com.github.jsqlparser</groupId>
+                </exclusion>
+                <exclusion>
+                    <artifactId>jsr305</artifactId>
+                    <groupId>com.google.code.findbugs</groupId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework.cloud</groupId>
+                    <artifactId>spring-cloud-starter-oauth2</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 32 - 0
yt-novel/yt-novel-feign/pom.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.ytpm</groupId>
+        <artifactId>yt-novel</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>yt-novel-feign</artifactId>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring-boot.version}</version>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 9 - 0
yt-novel/yt-novel-feign/src/main/java/com/ytpm/novel/feign/base/BaseNovelFeign.java

@@ -0,0 +1,9 @@
+package com.ytpm.novel.feign.base;
+
+
+/**
+ * @author lih
+ * @date 2025-09-30 09:09
+ */
+public interface BaseNovelFeign {
+}

+ 15 - 0
yt-novel/yt-novel-feign/src/main/java/com/ytpm/novel/feign/feign/NovelFeign.java

@@ -0,0 +1,15 @@
+package com.ytpm.novel.feign.feign;
+
+
+import com.ytpm.novel.feign.base.BaseNovelFeign;
+import org.springframework.cloud.openfeign.FeignClient;
+
+/**
+ * @author lih
+ * @date 2025/9/30
+ */
+@FeignClient("novel-service")
+public interface NovelFeign extends BaseNovelFeign {
+
+
+}

+ 95 - 0
yt-novel/yt-novel-service/pom.xml

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.ytpm</groupId>
+        <artifactId>yt-novel</artifactId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>yt-novel-service</artifactId>
+    <packaging>jar</packaging>
+    <description>子服务-小说模式</description>
+    <name>question-service</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <hikar-icp.version>4.0.3</hikar-icp.version>
+        <zxing.version>3.5.1</zxing.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>31.0-jre</version>
+        </dependency>
+        <dependency>
+            <groupId>com.ytpm</groupId>
+            <artifactId>risk-feign</artifactId>
+            <version>1.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.zaxxer</groupId>
+            <artifactId>HikariCP</artifactId>
+            <version>${hikar-icp.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.zxing</groupId>
+            <artifactId>core</artifactId>
+            <version>${zxing.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.name}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>8</source>
+                    <target>8</target>
+                    <encoding>utf-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.5.3</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                    <mainClass>com.ytpm.question.QuestionApplication</mainClass>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 30 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/NovelApplication.java

@@ -0,0 +1,30 @@
+package com.ytpm.novel;
+
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 小说模式服务
+ * @author lih
+ * @date 2025-09-30 09:17
+ */
+@RestController
+@SpringBootApplication(exclude = {
+        DataSourceAutoConfiguration.class,
+        DataSourceTransactionManagerAutoConfiguration.class,
+        JdbcTemplateAutoConfiguration.class
+})
+@EnableDiscoveryClient
+@EnableFeignClients(basePackages = {"com.ytpm.feign"})
+public class NovelApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(NovelApplication.class, args);
+    }
+}

+ 38 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/aop/DataSourceAspect.java

@@ -0,0 +1,38 @@
+package com.ytpm.novel.aop;
+
+import com.ytpm.app.enums.DataSourceType;
+import com.ytpm.novel.config.datasource.DynamicDataSourceContextHolder;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.After;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+@Aspect
+@Component
+@Order(1)
+public class DataSourceAspect {
+
+    @Pointcut("execution(* com.ytpm.question.service.*.*(..))")
+    public void servicePointcut() {}
+
+    @Before("servicePointcut()")
+    public void beforeService(JoinPoint joinPoint) {
+        String methodName = joinPoint.getSignature().getName();
+        if (methodName.startsWith("get") ||
+            methodName.startsWith("find") ||
+            methodName.startsWith("select") ||
+            methodName.startsWith("query")) {
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
+        } else {
+            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
+        }
+    }
+
+    @After("servicePointcut()")
+    public void afterService() {
+        DynamicDataSourceContextHolder.clear();
+    }
+}

+ 37 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/YtWebMvcConfigurerAdapter.java

@@ -0,0 +1,37 @@
+package com.ytpm.novel.config;
+
+import com.ytpm.novel.config.interceptor.HttpInterceptor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+
+/**
+ * 自定义拦截器
+ */
+@Slf4j
+@Configuration
+public class YtWebMvcConfigurerAdapter implements WebMvcConfigurer {
+
+    @Resource
+    private HttpInterceptor httpInterceptor;
+
+    @Value("${spring.application.name:}")
+    private String applicationName;
+
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(httpInterceptor)
+                .addPathPatterns("/**").excludePathPatterns("/resources/**");
+    }
+
+    @PostConstruct
+    private void handlePostConstruct() {
+        log.info("current service: {}", applicationName);
+    }
+}

+ 58 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/DataSourceConfig.java

@@ -0,0 +1,58 @@
+package com.ytpm.novel.config.datasource;
+
+import com.ytpm.app.enums.DataSourceType;
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import javax.sql.DataSource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 多数据源配置
+ *
+ * @Marx
+ */
+@Configuration
+@EnableTransactionManagement
+public class DataSourceConfig {
+    @Bean(name = "masterDataSource")
+    @ConfigurationProperties(prefix = "spring.datasource.master.hikari")
+    public DataSource masterDataSource() {
+        return DataSourceBuilder.create().type(HikariDataSource.class).build();
+    }
+
+    @Bean(name = "slaveDataSource")
+    @ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
+    public DataSource slaveDataSource() {
+        return DataSourceBuilder.create().type(HikariDataSource.class).build();
+    }
+
+    @Primary
+    @Bean(name = "dynamicDataSource")
+    @DependsOn({"masterDataSource", "slaveDataSource"})
+    public DataSource dynamicDataSource() {
+        Map<Object, Object> dataSources = new HashMap<>();
+        dataSources.put(DataSourceType.MASTER, masterDataSource());
+        dataSources.put(DataSourceType.SLAVE, slaveDataSource());
+
+        DynamicDataSource ds = new DynamicDataSource();
+        ds.setDefaultTargetDataSource(masterDataSource());
+        ds.setTargetDataSources(dataSources);
+        return ds;
+    }
+
+    @Bean
+    public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
+        return new DataSourceTransactionManager(dynamicDataSource);
+    }
+}

+ 10 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/DynamicDataSource.java

@@ -0,0 +1,10 @@
+package com.ytpm.novel.config.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+public class DynamicDataSource extends AbstractRoutingDataSource {
+    @Override
+    protected Object determineCurrentLookupKey() {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+}

+ 20 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,20 @@
+package com.ytpm.novel.config.datasource;
+
+
+import com.ytpm.app.enums.DataSourceType;
+
+public class DynamicDataSourceContextHolder {
+    private static final ThreadLocal<DataSourceType> CONTEXT = new ThreadLocal<>();
+
+    public static void setDataSourceType(DataSourceType type) {
+        CONTEXT.set(type);
+    }
+
+    public static DataSourceType getDataSourceType() {
+        return CONTEXT.get() == null ? DataSourceType.MASTER : CONTEXT.get();
+    }
+
+    public static void clear() {
+        CONTEXT.remove();
+    }
+}

+ 38 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/datasource/MyBatisConfig.java

@@ -0,0 +1,38 @@
+package com.ytpm.novel.config.datasource;
+
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.SqlSessionTemplate;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+
+import javax.sql.DataSource;
+
+@Configuration
+@MapperScan(basePackages = "com.ytpm.novel.dao", sqlSessionFactoryRef = "sqlSessionFactory")
+public class MyBatisConfig {
+
+    @Bean
+    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
+        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        // 添加XML映射文件位置(如果有)
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
+                .getResources("classpath*:mapper/**/*.xml"));
+        // 添加类型别名包
+        sessionFactory.setTypeAliasesPackage("com.ytpm.app.model");
+        // 配置其他MyBatis设置
+        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
+        configuration.setMapUnderscoreToCamelCase(true);
+        sessionFactory.setConfiguration(configuration);
+        return sessionFactory.getObject();
+    }
+
+    @Bean
+    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
+        return new SqlSessionTemplate(sqlSessionFactory);
+    }
+}

+ 54 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/interceptor/HttpInterceptor.java

@@ -0,0 +1,54 @@
+package com.ytpm.novel.config.interceptor;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Date;
+
+@Slf4j
+@Component
+public class HttpInterceptor implements HandlerInterceptor {
+
+    @Value("${spring.application.name-zh:}")
+    private String applicationNameZh;
+
+    @Override
+    public boolean preHandle(HttpServletRequest request,
+                             HttpServletResponse response, Object obj) throws Exception {
+        // 获取客户端IP地址
+        String clientIp = getClientIp(request);
+        log.info("{}收到来自客户端[{}]的用户请求", applicationNameZh, clientIp);
+        return true;
+    }
+
+    private String getClientIp(HttpServletRequest request) {
+        String xfHeader = request.getHeader("X-Forwarded-For");
+        if (xfHeader == null) {
+            return request.getRemoteAddr();
+        }
+        return xfHeader.split(",")[0]; // 可能会有多个IP,这里取第一个逗号前的IP
+    }
+
+    /**
+     * 请求处理之后调用;在视图渲染之前,controller处理之后。
+     */
+    @Override
+    public void postHandle(HttpServletRequest request,
+                           HttpServletResponse response, Object obj, ModelAndView mv)
+            throws Exception {
+        response.setDateHeader("Expires", 0);
+        response.setHeader("Buffer", "True");
+        response.setHeader("Cache-Control", "no-cache");
+        response.setHeader("Expires", "0");
+        response.setHeader("ETag", String.valueOf(System.currentTimeMillis()));
+        response.setHeader("Pragma", "no-cache");
+        response.setHeader("Date", String.valueOf(new Date()));
+        response.setHeader("Last-Modified", String.valueOf(new Date()));
+
+    }
+}

+ 29 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/redis/RedisListenerConfig.java

@@ -0,0 +1,29 @@
+package com.ytpm.novel.config.redis;
+
+import com.ytpm.novel.monitor.RedisKeyExpirationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+
+/**
+ * redis 监听器配置
+ * @author marx
+ * @date 2025/7/29 16:09
+ */
+@Configuration
+public class RedisListenerConfig {
+
+    @Bean
+    RedisMessageListenerContainer listenerContainer(RedisConnectionFactory connectionFactory) {
+        RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();
+        listenerContainer.setConnectionFactory(connectionFactory);
+        return listenerContainer;
+    }
+
+    @Bean("questionRedisKeyListener")
+    KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
+        return new RedisKeyExpirationListener(listenerContainer);
+    }
+}

+ 40 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/config/swagger/SwaggerConfig.java

@@ -0,0 +1,40 @@
+package com.ytpm.novel.config.swagger;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+@Configuration
+@EnableSwagger2
+public class SwaggerConfig {
+    private static final String VERSION = "1.0.0";
+
+    @Bean
+    public Docket createApi(){
+        return new Docket(DocumentationType.SWAGGER_2)
+                .apiInfo(apiInfo())
+                .select()
+                .apis(RequestHandlerSelectors.basePackage("com.ytpm.question.controller"))
+                .paths(PathSelectors.any())
+                .build();
+    }
+
+    private ApiInfo apiInfo() {
+        return new ApiInfoBuilder()
+                .title("")
+                .contact(new Contact("易推网络","http://www.sourcetreasure.com/index.html","marxjaw6466@163.com"))
+                .description("代理商管理系统")
+                .termsOfServiceUrl("https://juejin.cn/user/4310510864972254")
+                .license("The Apache License, Version 2.0")
+                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
+                .version(VERSION)
+                .build();
+    }
+}

+ 95 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/controller/AdController.java

@@ -0,0 +1,95 @@
+package com.ytpm.novel.controller;
+
+import com.ytpm.agent.view.AgentAdGroupStaticsVO;
+import com.ytpm.agent.view.AgentTopCountView;
+import com.ytpm.app.param.DyzAdRecordParam;
+import com.ytpm.general.Result;
+import com.ytpm.middle.view.DashboardRankingListVO;
+import com.ytpm.middle.view.DashboardRevenueVO;
+import com.ytpm.middle.view.DashboardRiskVO;
+import com.ytpm.novel.service.AdService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+
+@Api(tags = "用户浏览广告记录")
+@RestController
+@RequestMapping("/ad")
+public class AdController {
+
+    @Resource
+    private AdService adService;
+
+    /**
+     * 保存广告记录
+     */
+    @ApiOperation("保存")
+    @PostMapping("/saveRecord")
+    public Result<?> saveRecord(@RequestBody DyzAdRecordParam param) {
+        return adService.saveRecord(param);
+    }
+
+    /**
+     * 根据应用ID查询广告数
+     */
+    @ApiOperation("根据应用ID查询广告数")
+    @GetMapping("/getAdCount")
+    public Map<String, BigDecimal> getAdCount(@RequestParam(name = "appIds") String appIds) {
+        return adService.getAdCount(appIds);
+    }
+
+    /**
+     * 查询应用排行榜信息
+     */
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "sortBy", value = "排行榜排序规则", required = true),
+            @ApiImplicitParam(name = "limit", value = "排行榜显示数量", required = true),
+    })
+    @ApiOperation("查询应用排行榜信息")
+    @GetMapping("/getAppRankingList")
+    public DashboardRankingListVO queryRankingList(@RequestParam(name = "sortBy") Integer sortBy,
+                                                   @RequestParam(name = "limit") Integer limit) {
+        return adService.queryRankingList(sortBy, limit);
+    }
+
+    /**
+     * 查询各渠道应用收益分时统计
+     */
+    @ApiImplicitParam(name = "apkIds", value = "渠道应用ID(,拼接)", required = true)
+    @ApiOperation("查询各渠道应用收益分时统计")
+    @GetMapping("/revenueStatics")
+    public DashboardRevenueVO revenueStatics(@RequestParam(name = "apkIds") String apkIds) {
+        return adService.revenueStatics(apkIds);
+    }
+
+    @ApiOperation("查询渠道商顶部数据")
+    @GetMapping("/getAppTopCount")
+    public AgentTopCountView getAppTopCount(@RequestParam(name = "appIds") String appIds) {
+        return adService.getAppTopCount(appIds);
+    }
+
+    @ApiOperation("查询用户风控分时数据统计")
+    @GetMapping("/userStatics")
+    public DashboardRiskVO userStatics(@RequestParam(name = "appId") String appId) {
+        return adService.userStatics(appId);
+    }
+
+    @ApiOperation("查询代理商首页统计广告数据")
+    @GetMapping("/getAgentProfit")
+    public List<AgentAdGroupStaticsVO> getAgentProfit(@RequestParam(name = "appIds") String appIds) {
+        return adService.getAgentProfit(appIds);
+    }
+}

+ 76 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/controller/NovelController.java

@@ -0,0 +1,76 @@
+package com.ytpm.novel.controller;
+
+
+import com.ytpm.app.model.YtNovel;
+import com.ytpm.app.model.YtNovelCategory;
+import com.ytpm.general.Result;
+import com.ytpm.general.ResultTable;
+import com.ytpm.novel.model.dto.YtNovelDto;
+import com.ytpm.novel.model.param.NovelPageParam;
+import com.ytpm.novel.model.param.NovelRecordParam;
+import com.ytpm.novel.model.param.NovelShelfParam;
+import com.ytpm.novel.service.NovelService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025-09-30 11:20
+ */
+@RefreshScope
+@RestController
+@Api(tags = "小说管理")
+@RequestMapping("/novel")
+public class NovelController {
+
+    @Resource
+    private NovelService novelService;
+
+    @ApiOperation("获取小说列表")
+    @PostMapping("/page")
+    public ResultTable<List<YtNovelDto>> queryNovelList(@RequestBody NovelPageParam param) {
+        return ResultTable.resultTableOk(novelService.selectNovelPage(param));
+    }
+
+    @ApiOperation("保存小说章节阅读记录")
+    @PostMapping("/record")
+    public Result<String> saveNovelRecord(@RequestBody NovelRecordParam param) {
+        return Result.resultObjOk(novelService.saveNovelRecord(param));
+    }
+
+    @ApiOperation("获取小说信息")
+    @GetMapping("/info")
+    public Result<YtNovelDto> queryNovelInfo(@RequestParam("novelId") String novelId,
+                                             @RequestParam(value = "userId", required = false) String userId) {
+        return Result.resultObjOk(novelService.selectNovelById(novelId,userId));
+    }
+
+    @ApiOperation("获取书架小说列表")
+    @GetMapping("/shelf/list")
+    public Result<List<YtNovel>> queryNovelShelfList(@RequestParam("userId") String userId) {
+        return Result.resultObjOk(novelService.selectNovelShelf(userId));
+    }
+
+    @ApiOperation("保存书架")
+    @PostMapping("/shelf/save")
+    public Result<String> saveShelf(@RequestBody NovelShelfParam param) {
+        return Result.resultObjOk(novelService.saveNovelShelf(param));
+    }
+
+    @ApiOperation(("获取小说分类列表"))
+    @GetMapping("/category/list")
+    public Result<List<YtNovelCategory>> queryCategoryList() {
+        return Result.resultObjOk(novelService.selectCategoryList());
+    }
+
+}

+ 83 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/controller/VisitorController.java

@@ -0,0 +1,83 @@
+package com.ytpm.novel.controller;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.ytpm.app.enums.LoginType;
+import com.ytpm.app.model.YtDyzUser;
+import com.ytpm.app.param.VisitorLoginParam;
+import com.ytpm.general.Result;
+import com.ytpm.handle.LoginServiceFactory;
+import com.ytpm.novel.dao.AppUserMapper;
+import com.ytpm.novel.redis.RedisService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 游客登陆
+ *
+ * @author Marx
+ * @date 2025/9/2 15:02
+ */
+@RefreshScope
+@RestController
+@Api(tags = "游客管理")
+@RequestMapping("/visitor")
+public class VisitorController {
+
+    @Resource
+    private AppUserMapper appUserMapper;
+
+    @Resource
+    private LoginServiceFactory loginServiceFactory;
+
+    @Resource
+    private RedisService redisService;
+
+    @ApiOperation("获取游客登陆幂等Token")
+    @GetMapping("/get/requestId")
+    public Result<String> getRequestId(@RequestParam("ditchId") String ditchId, @RequestParam("appType") String appType,
+                                       @RequestParam("deviceId") String deviceId){
+        String redisKey = StrUtil.format("visitor:requestId:{}:{}:{}", ditchId, appType, deviceId);
+        String requestId = "";
+        if (redisService.hasKey(redisKey)) {
+            requestId = redisService.getStr(redisKey);
+        }
+        if (StrUtil.isEmpty(requestId)) {
+            requestId = IdUtil.fastSimpleUUID();
+            redisService.setTimeOutMinutesStr(redisKey, requestId, 1);
+        }
+        return Result.resultObjOk(requestId);
+    }
+
+    @PostMapping("/login")
+    @ApiOperation("游客登录")
+    @Transactional(rollbackFor = Exception.class)
+    public Result<YtDyzUser> visitorLogin(@RequestBody VisitorLoginParam param, HttpServletRequest request) {
+        return loginServiceFactory.login(LoginType.VISITOR, param, request);
+    }
+
+    @ApiOperation("设备在指定时间内注册的渠道")
+    @GetMapping("/getDitchCount")
+    public int getDitchCount(@RequestParam("deviceId") String deviceId, @RequestParam("hours") Integer hours) {
+        return appUserMapper.countDitch(deviceId, hours);
+    }
+
+    @ApiOperation("设备在指定时间内注册的渠道")
+    @GetMapping("/getLoginDitchCount")
+    public int getLoginDitchCount(@RequestParam("deviceId") String deviceId, @RequestParam("hours") Integer hours) {
+        return appUserMapper.countLoginDitch(deviceId, hours);
+    }
+
+
+}

+ 85 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/AdRecordMapper.java

@@ -0,0 +1,85 @@
+package com.ytpm.novel.dao;
+
+import com.ytpm.agent.param.AdRecordListParam;
+import com.ytpm.app.model.YtDyzAdRecord;
+import com.ytpm.app.param.YtAppUserListParam;
+import com.ytpm.middle.view.AppRevenueHourVO;
+import com.ytpm.novel.model.view.AgentNetworkAgg;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+@Mapper
+public interface AdRecordMapper {
+    /**
+     * 新增广告记录
+     */
+    void addOne(YtDyzAdRecord adRecord);
+
+    /**
+     * 保存游客广告记录
+     */
+    void addOneVisitor(YtDyzAdRecord adRecord);
+
+    /**
+     * 查询用户的广告记录
+     */
+    List<YtDyzAdRecord> getByUserId(@Param("userId") String userId, @Param("adsourceType") Integer adsourceType);
+
+    List<YtDyzAdRecord> getByUserByParam(AdRecordListParam param);
+
+    /**
+     * 查询用户广告记录统计数
+     */
+    Integer getRecordMonthCount(@Param("userId") String userId,
+                                @Param("adSourceType") Integer adSourceType,
+                                @Param("startTime") Date startTime);
+
+    /**
+     * 查询激励视频记录
+     */
+    List<YtDyzAdRecord> getByUserIds(@Param("userIds") String userIds);
+
+    /**
+     * 查询应用分小时收益数据
+     */
+    List<AppRevenueHourVO> getHourRevenue(@Param("appId") String appId, @Param("type") int type);
+
+    /**
+     * 根据应用统计激励广告数
+     */
+    int countByAppIds(@Param("appIds") String appIds);
+
+    /**
+     * 根据时间统计收益
+     */
+    BigDecimal getRevenueByTime(YtAppUserListParam param);
+
+    /**
+     * 查询今日广告
+     */
+    List<YtDyzAdRecord> getTodayRecord(@Param("appIds") String appIds, @Param("firstDay") String firstDay, @Param("lastDay") String lastDay);
+
+    /**
+     * 查询指定广告记录
+     */
+    List<YtDyzAdRecord> selectRecordByIds(@Param("loginStatus") Integer loginStatus, @Param("ids") List<String> adRecordIds);
+
+    /**
+     * 今日分小时按广告平台聚合
+     */
+    List<AgentNetworkAgg> getTodayAggByNetworkHour(@Param("appIds") String appIds);
+
+    /**
+     * 昨日按广告平台聚合
+     */
+    List<AgentNetworkAgg> getYesterdayAggByNetwork(@Param("appIds") String appIds);
+
+    /**
+     * 本月按广告平台聚合
+     */
+    List<AgentNetworkAgg> getMonthAggByNetwork(@Param("appIds") String appIds);
+}

+ 209 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/AppUserMapper.java

@@ -0,0 +1,209 @@
+package com.ytpm.novel.dao;
+
+import com.ytpm.agent.param.AuditUserParam;
+import com.ytpm.agent.view.AgentAuditCheckVO;
+import com.ytpm.app.model.YtAppDefaultConfig;
+import com.ytpm.app.model.YtDyzPowerRecord;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.app.param.AppUserParam;
+import com.ytpm.app.param.YtAppUserListParam;
+import com.ytpm.app.view.HourCountView;
+import com.ytpm.app.view.WxDefaultConfig;
+import com.ytpm.app.view.YtAppUserListView;
+import com.ytpm.middle.view.AppRankingListVO;
+import com.ytpm.middle.view.AppUserHourVO;
+import com.ytpm.middle.view.UserRankingListVO;
+import com.ytpm.novel.model.dto.UserStaticParam;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+@Mapper
+public interface AppUserMapper {
+
+    /**
+     * openid查询用户信息
+     */
+    YtNovelUser getYtAppUser(@Param("openid") String openid, @Param("ditchId") long ditchId);
+
+    /**
+     * 主键查询
+     */
+    YtNovelUser selectPrimaryKey(@Param("userId") String userId);
+
+    /**
+     * 新增应用用户
+     */
+    void addOne(YtNovelUser user);
+
+    /**
+     * 增加体力
+     */
+    void addOnePower(@Param("userId") String userId);
+
+    /**
+     * 减少体力
+     */
+    void subOnePower(@Param("userId") String userId);
+
+    /**
+     * 体力增加记录
+     */
+    void addPowerRecord(YtDyzPowerRecord record);
+
+    /**
+     * 修改用户信息
+     */
+    void updateUser(YtNovelUser newUser);
+
+    /**
+     * 查询所有玩家
+     */
+    List<YtAppUserListView> queryAll(YtAppUserListParam param);
+
+    /**
+     * 查询所有玩家
+     * 按照注册时间范围查询
+     */
+    List<YtNovelUser> queryAllByTime(@Param("param") UserStaticParam param);
+
+    List<HourCountView> countUsersByHour(@Param("param") UserStaticParam param);
+
+    List<YtNovelUser> queryTodayBanned(@Param("param") UserStaticParam param);
+
+    /**
+     * 查询用户信息
+     */
+    List<YtNovelUser> getUserList(AppUserParam param);
+
+    /**
+     * 根据应用类型查询应用默认配置
+     */
+    WxDefaultConfig getDefaultConfig(@Param("appType") int appType);
+
+    WxDefaultConfig getDefaultConfigByAppId(@Param("appId") String appId);
+
+    /**
+     * 根据微信openid查询用户
+     */
+    List<YtNovelUser> queryByOpenid(@Param("openid") String openid);
+
+    /**
+     * 根据设备ID查询平台ID
+     */
+    String getByDeviceId(@Param("deviceId") String deviceId, @Param("openid") String openid);
+
+    /**
+     * 保存应用配置
+     */
+    void saveAppConfig(YtAppDefaultConfig defaultConfig);
+
+    /**
+     * 修改应用配置
+     */
+    void updateAppConfig(YtAppDefaultConfig defaultConfig);
+
+
+    /**
+     * 根据APP_ID获取配置
+     */
+    List<WxDefaultConfig> getConfigByIds(@Param("appIds") String appIds);
+
+    /**
+     * 根据应用统计广告数
+     */
+    int getAdCount(@Param("appIds") String appIds);
+
+    /**
+     * 根据应用统计收益
+     */
+    BigDecimal getRevenueCount(@Param("appIds") String appIds);
+
+    /**
+     * 查询应用排行榜
+     */
+    List<AppRankingListVO> getAppRanking(@Param("sortBy") int sortBy, @Param("limit") int limit);
+
+    /**
+     * 查询用户排行榜数据
+     */
+    List<UserRankingListVO> getUserRanking(@Param("sortBy") int sortBy, @Param("limit") int limit);
+
+    /**
+     * 根据应用查询用户数量
+     */
+    int countUserByAppIds(@Param("appIds") String appIds);
+
+    /**
+     * 根据应用统计预估收益
+     */
+    BigDecimal countRevenueByAppIds(@Param("appIds") String appIds);
+
+    /**
+     * 根据应用查询风控用户数
+     */
+    int countBannedByAppIds(@Param("appIds") String appIds);
+
+    /**
+     * 统计注册用户数
+     */
+    int countRegistryUser(@Param("appId") String appId, @Param("type") int type);
+
+    /**
+     * 统计登录用户数
+     */
+    int countLoginUser(@Param("appId") String appId, @Param("type") int type);
+
+    /**
+     * 分时统计注册数
+     */
+    List<AppUserHourVO> countRegistryHour(@Param("appId") String appId, @Param("type") int type);
+
+    /**
+     * 分时统计登录
+     */
+    List<AppUserHourVO> countLoginHour(@Param("appId") String appId, @Param("type") int type);
+
+    /**
+     * 根据应用ID删除默认配置
+     */
+    void delByAppId(@Param("appId") String appId);
+
+    void unlockUser(@Param("userIds") String userIds);
+
+    List<YtNovelUser> queryByUserIds(@Param("userIds") String userIds);
+
+    /**
+     * 分类型查询用户当天视频广告
+     */
+    List<AgentAuditCheckVO> queryTodayUserAd(AuditUserParam auditParam);
+
+    List<YtNovelUser> getMonthRegistryUser(@Param("appIds") String appIds, @Param("type") Integer type);
+
+    /**
+     * 查询设备指定天数内注册的渠道数
+     */
+    int countDitch(@Param("deviceId") String deviceId, @Param("hours") int hours);
+
+    /**
+     * 查询设备指定时间内登录的用户和渠道
+     */
+    int countLoginDitch(@Param("deviceId") String deviceId, @Param("hours") int hours);
+
+    /**
+     * 查询设备最新注册的用户
+     */
+    List<YtNovelUser> getLastRegistryUser(@Param("deviceId") String deviceId);
+
+    /**
+     * 根据设备ID 和 渠道ID 查询用户信息
+     */
+    YtNovelUser getByDeviceAndDitch(@Param("deviceId") String deviceId, @Param("ditchId") Long ditchId);
+
+    String getPlatformByDeviceId(@Param("deviceId") String deviceId);
+
+    void updateTotal(@Param("userId") String userId, @Param("videoCount") int videoCount, @Param("revenue") BigDecimal revenue);
+
+}

+ 32 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/LoginRecordMapper.java

@@ -0,0 +1,32 @@
+package com.ytpm.novel.dao;
+
+import com.ytpm.app.model.YtDyzLoginRecord;
+import com.ytpm.app.param.AppUserQueryParam;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface LoginRecordMapper {
+    /**
+     * 增加用户登录记录
+     */
+    void insertOne(YtDyzLoginRecord loginRecord);
+
+    /**
+     * 查询用户登录记录
+     */
+    List<YtDyzLoginRecord> getLoginRecords(@Param("userId") String userId);
+
+    /**
+     * 查询最近一次登陆记录
+     */
+    YtDyzLoginRecord getLastLoginRecord(@Param("userId") String userId, @Param("isVisitor") Integer isVisitor);
+
+    List<YtDyzLoginRecord> getLoginRecordByIds(@Param("userIds") String userIds);
+
+    List<String> queryLoginCount(AppUserQueryParam appUserQueryParam);
+
+    int getTodayLoginCount(@Param("userId") String userId);
+}

+ 63 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/dao/NovelMapper.java

@@ -0,0 +1,63 @@
+package com.ytpm.novel.dao;
+
+
+import com.ytpm.app.model.YtNovel;
+import com.ytpm.app.model.YtNovelChapter;
+import com.ytpm.app.model.YtNovelShelf;
+import com.ytpm.app.model.YtNovelReadRecord;
+import com.ytpm.novel.model.dto.YtNovelDto;
+import com.ytpm.novel.model.param.NovelPageParam;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025-09-30 15:02
+ */
+@Mapper
+public interface NovelMapper {
+
+    /**
+     * 分页查询小说信息
+     */
+    List<YtNovelDto> selectNovelList(@Param("param") NovelPageParam param);
+
+    /**
+     * 根据小说ID查询小说信息
+     */
+    YtNovelDto selectByNovelId(@Param("novelId") String novelId);
+
+    List<YtNovel> selectByNovelIds(@Param("novelIds") List<String> novelIds);
+
+    /**
+     * 根据小说ID查询小说章节集合
+     */
+    List<YtNovelChapter> selectNovelChapter(@Param("novelId")String novelId);
+
+    /**
+     * 查询用户书架
+     */
+    YtNovelShelf selectShelf(@Param("userId") String userId);
+
+    /**
+     * 查询用户最后阅读章节
+     */
+    YtNovelReadRecord selectLastRecord(@Param("userId") String userId, @Param("novelId") String novelId);
+
+
+    /**
+     * 保存阅读记录
+     */
+    void insertReadRecord(YtNovelReadRecord readRecord);
+
+    /**
+     *  保存用户书架
+     */
+    void insertNovelShelf(YtNovelShelf shelf);
+    /**
+     *  更新用户书架
+     */
+    void updateNovelShelf(YtNovelShelf shelf);
+}

+ 156 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/handle/CustomerExceptionHandler.java

@@ -0,0 +1,156 @@
+package com.ytpm.novel.handle;
+
+import cn.hutool.core.util.StrUtil;
+import com.ytpm.general.Result;
+import com.ytpm.general.StatusCode;
+import com.ytpm.handle.CommonException;
+import com.ytpm.handle.CustomerException;
+import com.ytpm.handle.ValidatedException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.util.CollectionUtils;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
+import org.springframework.web.multipart.MultipartException;
+import org.springframework.web.servlet.HandlerExceptionResolver;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Marx
+ * @date 2025/8/28 10:38
+ */
+@Slf4j(topic = "answer-exception")
+@RestControllerAdvice(annotations = RestController.class)
+public class CustomerExceptionHandler implements HandlerExceptionResolver {
+
+    @ResponseBody
+    @ExceptionHandler(Exception.class)
+    public Result<?> handleException(Exception ex) {
+        //打印异常信息
+        log.error(StrUtil.format("抛出异常信息,异常信息为:ex={}", ex.getMessage()), ex);
+        String msg = "查询超时,请稍后重试!";
+        return new Result<>(StatusCode.ERROR, msg);
+    }
+
+    /**
+     * 对象参数校验失败
+     *
+     */
+    @ResponseBody
+    @ExceptionHandler(MissingServletRequestParameterException.class)
+    public Result<?> handleMissingServletRequestParameterException(Exception ex) {
+        log.error(StrUtil.format("对象参数校验失败,异常信息为:e={}", ex.getMessage()), ex);
+        String msg = MessageFormat.format("缺少参数{0}", ((MissingServletRequestParameterException) ex).getParameterName());
+        return new Result<>(StatusCode.MISSING_PARAMETER, msg);
+    }
+
+    /**
+     * 单个参数校验失败
+     */
+    @ResponseBody
+    @ExceptionHandler(ConstraintViolationException.class)
+    public Result<?> handleConstraintViolationException(Exception ex) {
+        log.error(StrUtil.format("单个参数校验失败,异常信息为:ex={}", ex.getMessage()), ex);
+        String returnMessage = "参数校验失败";
+        Set<ConstraintViolation<?>> sets = ((ConstraintViolationException) ex).getConstraintViolations();
+        if (!CollectionUtils.isEmpty(sets)) {
+            StringBuilder sb = new StringBuilder();
+            sets.forEach(error -> sb.append(error.getMessage()).append(";"));
+            String msg = sb.toString();
+            returnMessage = StrUtil.sub(msg, 0, msg.length() - 1);
+        }
+        return new Result<>(StatusCode.PARAMETER_CHECK_ERR, returnMessage);
+    }
+
+    /**
+     * 自定义校验异常信息捕获
+     *
+     */
+    @ResponseBody
+    @ExceptionHandler(IllegalArgumentException.class)
+    public Result<?> handleSpringCheckException(Exception ex) {
+        log.error(StrUtil.format("自定义校验[IllegalArgumentException]异常信息捕获,异常信息为:ex={}", ex.getMessage()), ex);
+        return new Result<>(StatusCode.PARAMETER_CHECK_ERR, ex.getMessage());
+    }
+
+    /**
+     * POST 请求参数校验失败
+     *
+     */
+    @ResponseBody
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public Result<?> handleMethodArgumentNotValidException(Exception ex) {
+        log.error(StrUtil.format("请求参数校验失败,异常信息为:ex={}", ex.getMessage()), ex);
+        List<ObjectError> errors = ((MethodArgumentNotValidException) ex).getBindingResult().getAllErrors();
+        String msg = "绑定异常";
+        if (!CollectionUtils.isEmpty(errors)) {
+            StringBuilder sb = new StringBuilder();
+            errors.forEach(error -> sb.append(error.getDefaultMessage()).append(";"));
+            msg = StrUtil.sub(sb.toString(), 0, sb.length() - 1);
+        }
+        return new Result<>(StatusCode.PARAMETER_CHECK_ERR, msg);
+    }
+
+    @ResponseBody
+    @ExceptionHandler(ValidatedException.class)
+    public Result<?> handleValidatedException(Exception ex) {
+        log.error(" 请求参数校验失败,异常信息为:ex={}", ex.getMessage());
+        return new Result<>(StatusCode.BUSINESS_CERTIFICATE_TYPE_STATUS_ERROR, ex.getMessage());
+    }
+
+    /**
+     * 上传文件超出限制处理
+     */
+    @ResponseBody
+    @ExceptionHandler(MaxUploadSizeExceededException.class)
+    public Result<?> handlerFileUploadException(MaxUploadSizeExceededException ex) {
+        log.error(StrUtil.format("抛出异常信息,异常信息为:ex={}", ex.getMessage()), ex);
+        return new Result<>(StatusCode.ERROR, "文件超出限制(10MB)");
+    }
+
+    @Override
+    public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
+                                         Object o, Exception e) {
+        log.error(StrUtil.format("系统异常,异常信息为:e={}", e.getMessage()), e.getMessage());
+        ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
+        if (e instanceof MultipartException) {
+            mv.addObject("message", "系统异常");
+            mv.addObject("code", "801");
+            mv.setStatus(HttpStatus.OK);
+        } else {
+            mv.addObject("msg", e.getMessage());
+        }
+        return mv;
+    }
+
+    @ResponseBody
+    @ExceptionHandler(CustomerException.class)
+    public Result<?> handleCustomerException(Exception ex) {
+        log.error(StrUtil.format("自定义校验异常[CustomerException]信息捕获,异常信息为:ex={}", ex), ex);
+        return new Result<>(StatusCode.ACCESS_ERR, ex.getMessage());
+    }
+
+    @ResponseBody
+    @ExceptionHandler(CommonException.class)
+    public Result<?> handleCommonException(Exception ex) {
+        log.error(StrUtil.format("自定义校验异常[CommonException]信息捕获,异常信息为:ex={}", ex), ex);
+        return new Result<>(StatusCode.ACCESS_ERR, ex.getMessage());
+    }
+
+
+}

+ 27 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/dto/UserStaticParam.java

@@ -0,0 +1,27 @@
+package com.ytpm.novel.model.dto;
+
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025-09-30 10:01
+ */
+@Data
+public class UserStaticParam {
+    @ApiModelProperty("开始时间(可选)")
+    private Date startTime;
+
+    @ApiModelProperty("结束时间(可选)")
+    private Date endTime;
+
+    private List<String> userIdList;
+
+    private List<String> appIdList;
+
+    private List<Integer> adSourceTypes;
+}

+ 30 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/dto/YtNovelDto.java

@@ -0,0 +1,30 @@
+package com.ytpm.novel.model.dto;
+
+
+import com.ytpm.app.model.YtNovel;
+import com.ytpm.app.model.YtNovelChapter;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 小说详情信息
+ * @author lih
+ * @date 2025-09-30 14:34
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class YtNovelDto extends YtNovel implements Serializable {
+
+    @ApiModelProperty("小说章节集合")
+    private List<YtNovelChapter> chapterList;
+
+    @ApiModelProperty("是否已在书架|0否|1是")
+    private Integer ifSelfShelf;
+
+    @ApiModelProperty("最后阅读章节")
+    private YtNovelChapter lastReadChapter;
+}

+ 19 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/param/NovelPageParam.java

@@ -0,0 +1,19 @@
+package com.ytpm.novel.model.param;
+
+
+import com.ytpm.general.PageMeta;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * @author lih
+ * @date 2025-09-30 15:25
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class NovelPageParam extends PageMeta {
+
+    private String categoryId;
+
+    private String novelName;
+}

+ 28 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/param/NovelRecordParam.java

@@ -0,0 +1,28 @@
+package com.ytpm.novel.model.param;
+
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * @author lih
+ * @date 2025-09-30 14:55
+ */
+@Data
+public class NovelRecordParam {
+
+    @ApiModelProperty("用户ID")
+    private String userId;
+
+    @ApiModelProperty("小说ID")
+    private String novelId;
+
+    @ApiModelProperty("小说章节ID")
+    private String chapterId;
+
+    @ApiModelProperty("小说名称")
+    private String novelName;
+
+    @ApiModelProperty("章节名称")
+    private String chapterName;
+}

+ 20 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/param/NovelShelfParam.java

@@ -0,0 +1,20 @@
+package com.ytpm.novel.model.param;
+
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025-09-30 14:41
+ */
+@Data
+public class NovelShelfParam {
+
+    private String userId;
+
+    private String shelfName;
+
+    private List<String> novelIds;
+}

+ 21 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/model/view/AgentNetworkAgg.java

@@ -0,0 +1,21 @@
+package com.ytpm.novel.model.view;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AgentNetworkAgg {
+    private String networkId;
+    private String networkName;
+    private String time;
+    private Long cnt;
+    private BigDecimal revenue;
+    private BigDecimal ecpm;
+}
+
+

+ 110 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/monitor/RedisKeyExpirationListener.java

@@ -0,0 +1,110 @@
+package com.ytpm.novel.monitor;
+
+import cn.hutool.core.util.StrUtil;
+import com.ytpm.agent.enums.UserStatusEnum;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.feign.RiskFeign;
+import com.ytpm.novel.dao.AppUserMapper;
+import com.ytpm.risk.enums.BannedTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+import org.springframework.lang.NonNull;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Objects;
+
+/**
+ * redis 监听器
+ */
+@Component
+@RefreshScope
+@Slf4j(topic = "RedisKeyExpire")
+public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
+
+    @Resource
+    private AppUserMapper appUserMapper;
+    @Resource
+    private RiskFeign riskFeign;
+
+    @Value("${spring.application.name:}")
+    private String applicationName;
+
+    private static final String FLAG_LOCK = "lock";
+    private static final String FLAG_UNLOCK = "unlock";
+
+    /**
+     *
+     * @param listenerContainer must not be {@literal null}.
+     */
+    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
+        super(listenerContainer);
+    }
+
+    /**
+     * 监听redis过期的 key 进行处理
+     */
+    @Override
+    public void onMessage(@NonNull Message message, byte[] pattern) {
+        String key = String.valueOf(message);
+        // 全局监听
+        if (key.startsWith("lock_") || key.startsWith("unlock_")) {
+            String[] arr = key.split("_");
+            if (arr.length < 2) {
+                return;
+            }
+            handleUserLock(key, arr[1], arr[0]);
+        }
+        // 仅限自身服务的监听
+        String serviceKey = StrUtil.replace(applicationName, "-service", "");
+        if (key.startsWith(serviceKey)) {
+            String[] arr = key.split("_");
+            if (arr.length < 3) {
+                return;
+            }
+            handleUserLock(key, arr[2], arr[1]);
+        }
+    }
+
+    private void handleUserLock(String key, String userId, String flag) {
+        YtNovelUser dyzUser = appUserMapper.selectPrimaryKey(userId);
+        if (Objects.nonNull(dyzUser)) {
+            log.info("App-Service 发现预定风控用户:{}", key);
+            // 用户锁定
+            if (FLAG_LOCK.equals(flag)) {
+                lockUser(dyzUser, UserStatusEnum.RISK.getCode());
+            }
+            // 用户解锁
+            if (FLAG_UNLOCK.equals(flag)) {
+                unLockUser(dyzUser);
+            }
+        }
+    }
+
+    /**
+     * 锁定用户
+     */
+    private void lockUser(YtNovelUser dyzUser, Integer userStatus) {
+        YtNovelUser newUser = new YtNovelUser();
+        newUser.setUserId(dyzUser.getUserId());
+        newUser.setUserStatus(userStatus);
+        newUser.setRiskReason(BannedTypeEnum.CHANNEL.getDesc() + "禁止登录");
+        appUserMapper.updateUser(newUser);
+    }
+
+    /**
+     * 解锁用户
+     */
+    private void unLockUser(YtNovelUser dyzUser) {
+        YtNovelUser newUser = new YtNovelUser();
+        newUser.setUserId(dyzUser.getUserId());
+        newUser.setUserStatus(UserStatusEnum.NORMAL.getCode());
+        appUserMapper.updateUser(newUser);
+        riskFeign.addBlockingRecord(dyzUser.getUserId());
+    }
+
+}

+ 293 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/redis/RedisService.java

@@ -0,0 +1,293 @@
+package com.ytpm.novel.redis;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Calendar;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class RedisService {
+
+    @Autowired
+    StringRedisTemplate stringRedisTemplate;
+
+    @Autowired
+    RedisTemplate<Object, Object> redisTemplate;
+
+    @Autowired(required = false)
+    public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) {
+        //序列化key值,防止key值前面乱码
+        RedisSerializer<String> stringSerializer = new StringRedisSerializer();
+        redisTemplate.setKeySerializer(stringSerializer);
+        redisTemplate.setHashKeySerializer(stringSerializer);
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Resource(name = "redisTemplate")
+    ValueOperations<Object, Object> valOpsObj;
+
+    /**
+     * 根据指定key获取String
+     *
+     * @param key
+     * @return
+     */
+    public String getStr(String key) {
+        return stringRedisTemplate.opsForValue().get(key);
+    }
+
+
+    /**
+     * 查询并获取key的有效时间
+     */
+    public Long getExpire(String key) {
+        return valOpsObj.getOperations().getExpire(key, TimeUnit.MINUTES);
+    }
+
+    /**
+     * 设置Str缓存
+     *
+     * @param key
+     * @param val
+     */
+    public void setStr(String key, String val) {
+        stringRedisTemplate.opsForValue().set(key, val);
+    }
+
+    /**
+     * 设置Str缓存 timeOut单位  毫秒
+     *
+     * @param key
+     * @param val
+     */
+    public void setTimeOutStr(String key, String val, long timeOut) {
+        stringRedisTemplate.opsForValue().set(key, val, timeOut, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 判断是否存在key
+     *
+     * @param key
+     * @return
+     */
+    public Boolean hasKey(String key) {
+        return redisTemplate.hasKey(key);
+    }
+
+    /**
+     * 设置Str缓存 timeOut单位  小时
+     *
+     * @param key
+     * @param val
+     * @param timeOut
+     */
+    public void setTimeOutHoursStr(String key, String val, long timeOut) {
+        stringRedisTemplate.opsForValue().set(key, val, timeOut, TimeUnit.HOURS);
+    }
+
+    /**
+     * 设置Str缓存 timeOut单位  分钟
+     *
+     * @param key
+     * @param val
+     * @param timeOut
+     */
+    public void setTimeOutMinutesStr(String key, String val, long timeOut) {
+        stringRedisTemplate.opsForValue().set(key, val, timeOut, TimeUnit.MINUTES);
+    }
+
+    /**
+     * 删除指定key
+     *
+     * @param key
+     */
+    public void del(String key) {
+        stringRedisTemplate.delete(key);
+    }
+
+    /**
+     * 设置指定key值的超时时间
+     *
+     * @param key
+     */
+    public void expire(String key, long timeOut) {
+        TimeUnit timeUnit = TimeUnit.MILLISECONDS;
+        stringRedisTemplate.expire(key, timeOut, timeUnit);
+    }
+
+    /**
+     * 根据前缀批量删除
+     *
+     * @param keysPrefix
+     */
+    public void delKeys(String keysPrefix) {
+        Set<String> set = stringRedisTemplate.keys(keysPrefix);
+        stringRedisTemplate.delete(set);
+    }
+
+
+    /**
+     * 根据指定o获取Object
+     *
+     * @param o
+     * @return
+     */
+    public Object getObj(Object o) {
+        return valOpsObj.get(o);
+    }
+
+    /**
+     * 设置obj缓存
+     *
+     * @param o1
+     * @param o2
+     */
+    public void setObj(Object o1, Object o2) {
+        valOpsObj.set(o1, o2);
+    }
+
+    /**
+     * 设置obj缓存
+     *
+     * @param o1
+     * @param o2
+     */
+    public void setObj(Object o1, Object o2, long timeout) {
+        valOpsObj.set(o1, o2, timeout);
+    }
+
+
+    /**
+     * 删除Obj缓存
+     *
+     * @param o
+     */
+    public void delObj(Object o) {
+        redisTemplate.delete(o);
+    }
+
+
+    /**
+     * 自增1
+     *
+     * @param key
+     * @return
+     */
+    public long incr(String key) {
+        long num = stringRedisTemplate.opsForValue().increment(key, 1);
+        return num;
+    }
+
+    /**
+     * 自增1
+     *
+     * @param key
+     * @return
+     */
+    public long incrByExp(String key, long timeout) {
+        long num = stringRedisTemplate.opsForValue().increment(key, 1);
+        TimeUnit timeUnit = TimeUnit.MILLISECONDS;
+        stringRedisTemplate.expire(key, timeout, timeUnit);
+        return num;
+    }
+
+    /**
+     * 自增
+     *
+     * @param key
+     * @param delta 自增值
+     * @return
+     */
+    public long incrByExp(String key, int delta, long timeout) {
+        long num = stringRedisTemplate.opsForValue().increment(key, delta);
+        TimeUnit timeUnit = TimeUnit.MILLISECONDS;
+        stringRedisTemplate.expire(key, timeout, timeUnit);
+        return num;
+    }
+
+    /**
+     * 自增
+     *
+     * @param key
+     * @param delta 自增值
+     * @return
+     */
+    public long incr(String key, long delta) {
+        return stringRedisTemplate.opsForValue().increment(key, delta);
+    }
+
+    /**
+     * 获取应用用户ID
+     */
+    public synchronized String getAppUserId() {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyMdd");
+        LocalDate currentDate = LocalDate.now();
+        LocalDate yesterday = currentDate.minusDays(1);
+        String todayKey = currentDate.format(formatter);
+        String yesterdayKey = yesterday.format(formatter);
+        if (this.hasKey(todayKey)) {
+            this.incr(todayKey);
+            return this.getStr(todayKey);
+        }
+        this.del(yesterdayKey);
+        this.setStr(todayKey, todayKey + "00001");
+        return this.getStr(todayKey);
+    }
+
+    public Long getDitchId() {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
+        LocalDate currentDate = LocalDate.now();
+        LocalDate yesterday = currentDate.minusDays(1);
+        String todayKey = currentDate.format(formatter);
+        String yesterdayKey = yesterday.format(formatter);
+        String prefix = "Ditch_";
+        if (this.hasKey(prefix + todayKey)) {
+            this.incr(prefix + todayKey);
+            return Long.parseLong(this.getStr(prefix + todayKey));
+        }
+        this.del(yesterdayKey);
+        this.setStr(prefix + todayKey, todayKey + "01");
+        return Long.parseLong(this.getStr(prefix + todayKey));
+    }
+
+    /**
+     *
+     * 生成业务单编号
+     *
+     * @return
+     */
+    public String generateOrderNo(String key) {
+        Calendar cl = Calendar.getInstance();
+        int year = cl.get(Calendar.YEAR);
+        int month = cl.get(Calendar.MONTH) + 1;
+        int day = cl.get(Calendar.DATE);
+        String orderNoPrefix = getTimeStr(year) + getTimeStr(month) + getTimeStr(day);
+        if (this.hasKey(key)) {
+            String old = this.getStr(key);
+            String sub = old.substring(0, 8);
+            if (sub.equals(orderNoPrefix)) {
+                long incr = this.incr(key);
+                return String.valueOf(incr);
+            }
+        }
+        this.setStr(key, orderNoPrefix + "001");
+        return orderNoPrefix + "001";
+    }
+
+    private String getTimeStr(int num) {
+        if (num > 9) {
+            return String.valueOf(num);
+        }
+        return "0" + num;
+    }
+}

+ 57 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/AdService.java

@@ -0,0 +1,57 @@
+package com.ytpm.novel.service;
+
+import com.ytpm.agent.view.AgentAdGroupStaticsVO;
+import com.ytpm.agent.view.AgentTopCountView;
+import com.ytpm.app.model.YtDyzAdRecord;
+import com.ytpm.app.model.YtDyzUser;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.app.param.DyzAdRecordParam;
+import com.ytpm.general.Result;
+import com.ytpm.middle.view.DashboardRankingListVO;
+import com.ytpm.middle.view.DashboardRevenueVO;
+import com.ytpm.middle.view.DashboardRiskVO;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+public interface AdService {
+    /**
+     * 保存广告记录
+     */
+    Result<?> saveRecord(DyzAdRecordParam param);
+
+    /**
+     * 根据应用查询广告数量
+     */
+    Map<String, BigDecimal> getAdCount(String appIds);
+
+    /**
+     * 查询排行榜信息
+     */
+    DashboardRankingListVO queryRankingList(Integer sortBy, Integer limit);
+
+    /**
+     * 查询各渠道应用收益分时统计
+     */
+    DashboardRevenueVO revenueStatics(String apkIds);
+
+    /**
+     * 查询代理商顶部数据统计
+     */
+    AgentTopCountView getAppTopCount(String appIds);
+
+    /**
+     * 查询用户风控分时数据统计
+     */
+    DashboardRiskVO userStatics(String appIds);
+
+    /**
+     * 查询代理商首页广告数据统计
+     */
+    List<AgentAdGroupStaticsVO> getAgentProfit(String appIds);
+
+    String saveRecordAndChangeUser(DyzAdRecordParam param, YtNovelUser user);
+
+    List<YtDyzAdRecord> queryRecordByIds(List<String> adRecordIds);
+}

+ 19 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/AppUserService.java

@@ -0,0 +1,19 @@
+package com.ytpm.novel.service;
+
+import com.ytpm.app.model.YtDyzUser;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.app.param.WxLoginParam;
+import com.ytpm.app.view.WxLoginResult;
+import com.ytpm.app.view.WxUserInfo;
+
+public interface AppUserService {
+    /**
+     * openid查询用户信息
+     */
+    YtNovelUser selectByOpenid(String openid, Long ditchId);
+
+    /**
+     * 子事务处理用户crud
+     */
+    YtNovelUser crudForNewTrans(WxLoginParam param, WxUserInfo wxUserInfo, WxLoginResult loginResult);
+}

+ 36 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/NovelService.java

@@ -0,0 +1,36 @@
+package com.ytpm.novel.service;
+
+
+import com.github.pagehelper.PageInfo;
+import com.ytpm.app.model.YtNovel;
+import com.ytpm.app.model.YtNovelCategory;
+import com.ytpm.novel.model.dto.YtNovelDto;
+import com.ytpm.novel.model.param.NovelPageParam;
+import com.ytpm.novel.model.param.NovelRecordParam;
+import com.ytpm.novel.model.param.NovelShelfParam;
+
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025/9/30
+ */
+public interface NovelService {
+
+    /**
+     * 根据分类ID、小说名称查询小说列表
+     */
+    PageInfo<YtNovelDto> selectNovelPage(NovelPageParam param);
+
+    YtNovelDto selectNovelById(String novelId, String userId);
+
+    List<YtNovel> selectNovelShelf(String userId);
+
+    String saveNovelRecord(NovelRecordParam param);
+
+    String saveNovelShelf(NovelShelfParam param);
+
+    List<YtNovelCategory> selectCategoryList();
+
+
+}

+ 320 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/AdServiceImpl.java

@@ -0,0 +1,320 @@
+package com.ytpm.novel.service.impl;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import com.ytpm.advertise.enums.AdPlatformTypeEnum;
+import com.ytpm.advertise.enums.AdSourceTypeEnum;
+import com.ytpm.agent.enums.AdRecordEnum;
+import com.ytpm.agent.enums.UserStatusEnum;
+import com.ytpm.agent.view.AgentAdGroupStaticsVO;
+import com.ytpm.agent.view.AgentTopCountView;
+import com.ytpm.app.model.YtDyzAdRecord;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.app.param.DyzAdRecordParam;
+import com.ytpm.app.view.WxDefaultConfig;
+import com.ytpm.feign.RiskFeign;
+import com.ytpm.general.RepMessage;
+import com.ytpm.general.Result;
+import com.ytpm.general.StatusCode;
+import com.ytpm.middle.view.AppRankingListVO;
+import com.ytpm.middle.view.AppRevenueHourVO;
+import com.ytpm.middle.view.AppUserHourVO;
+import com.ytpm.middle.view.DashboardAppRevenueVO;
+import com.ytpm.middle.view.DashboardRankingListVO;
+import com.ytpm.middle.view.DashboardRevenueVO;
+import com.ytpm.middle.view.DashboardRiskVO;
+import com.ytpm.middle.view.UserRankingListVO;
+import com.ytpm.novel.dao.AdRecordMapper;
+import com.ytpm.novel.dao.AppUserMapper;
+import com.ytpm.novel.service.AdService;
+import com.ytpm.novel.model.view.AgentNetworkAgg;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RefreshScope
+public class AdServiceImpl implements AdService {
+
+    @Autowired
+    private AdRecordMapper adRecordMapper;
+    @Autowired
+    private AppUserMapper appUserMapper;
+    @Autowired
+    private RiskFeign riskFeign;
+
+    @Value(" ${risk.config.banned.tips:}")
+    private String tips;
+
+    /**
+     * 保存广告记录
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Result<?> saveRecord(DyzAdRecordParam param) {
+        YtNovelUser user = appUserMapper.selectPrimaryKey(param.getUserId());
+        log.debug(StrUtil.format("[saveRecord] userId:{} revenue:{} ", param.getUserId(), param.getRevenue()));
+        if (Objects.isNull(user)) {
+            return Result.resultOk(RepMessage.SAVE_SUCCESS);
+        }
+        if (!UserStatusEnum.NORMAL.getCode().equals(user.getUserStatus())) {
+            return new Result<>(StatusCode.ACCESS_ERR, getTipsMsg());
+        }
+        saveRecordAndChangeUser(param, user);
+        //调用风控广告校验
+        if (AdSourceTypeEnum.rewarded_video.getAdSourceType() == param.getAdSourceType()) {
+            WxDefaultConfig config = appUserMapper.getDefaultConfigByAppId(user.getAppId());
+            String format = StrUtil.emptyToDefault(config.getTaskLimitTip(), "当前设备完成任务次数过多,请{}后重试!");
+//            user.setPowerLimitTip(format);
+//            Result<?> result = riskFeign.checkAdRisk(user);
+//            if (result.getCode() != 200) {
+//                Result<Object> resultObj = new Result<>(StatusCode.ACCESS_ERR, getTipsMsg());
+//                // 766处理
+//                if ("766".contains(result.getMessage())) {
+//                    resultObj.setData(result.getMessage());
+//                } else if (result.getCode() == StatusCode.ACCESS_RISK_ERROR && StrUtil.isNotBlank(result.getMessage())) {
+//                    // 体力不再获取处理
+//                    resultObj.setMessage(result.getMessage());
+//                    resultObj.setData("767");
+//                }
+//                return resultObj;
+//            }
+        }
+        return Result.resultOk(RepMessage.SAVE_SUCCESS);
+    }
+
+    private String getTipsMsg() {
+        String[] split = tips.split(",");
+        return split[RandomUtil.randomInt(split.length)];
+    }
+
+    /**
+     * 根据应用查询广告数量
+     */
+    @Override
+    public Map<String, BigDecimal> getAdCount(String appIds) {
+        Map<String, BigDecimal> map = new HashMap<>();
+        //查询这些app的用户统计广告数量相加
+        int adCount = appUserMapper.getAdCount(appIds);
+        BigDecimal total = appUserMapper.getRevenueCount(appIds);
+        map.put("adCount", BigDecimal.valueOf(adCount));
+        map.put("expectRevenue", total);
+        return map;
+    }
+
+    /**
+     * 查询排行榜信息
+     */
+    @Override
+    public DashboardRankingListVO queryRankingList(Integer sortBy, Integer limit) {
+        //查询前五个应用名称 当日收益  用户数量
+        List<AppRankingListVO> appRankingList = appUserMapper.getAppRanking(sortBy, limit);
+        //查询前五个用户  所属应用  视频数 总收益 ecpm
+        List<UserRankingListVO> userRankingList = appUserMapper.getUserRanking(sortBy, limit);
+        return new DashboardRankingListVO(appRankingList, userRankingList);
+    }
+
+    /**
+     * 查询各渠道应用收益分时统计
+     */
+    @Override
+    public DashboardRevenueVO revenueStatics(String apkIds) {
+        DashboardRevenueVO vo = new DashboardRevenueVO();
+        List<DashboardAppRevenueVO> appRevenueList = new ArrayList<>();
+        //查询出各应用的用户及收益数据
+        List<String> appIdList = Arrays.asList(apkIds.split(","));
+        int index = appIdList.size();
+        // 由于子表数据庞大导致range查询低于const
+        CountDownLatch countDownLatch = new CountDownLatch(index);
+        DashboardAppRevenueVO appRevenueVO;
+        String appId;
+        do {
+            --index;
+            appId = appIdList.get(index);
+            //根据应用ID查询收益数据及个小时数据  1-今日
+            List<AppRevenueHourVO> todayRevenues = adRecordMapper.getHourRevenue(appId, 1);
+            Map<String, BigDecimal> todayHourMap = todayRevenues.stream().collect(Collectors.toMap(AppRevenueHourVO::getHour, AppRevenueHourVO::getRevenue));
+            appRevenueVO = new DashboardAppRevenueVO();
+            appRevenueVO.setAppId(appId);
+            //统计今日数据
+            BigDecimal todayRevenue = todayHourMap.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+            appRevenueVO.setTodayHourRevenueMap(todayHourMap);
+            appRevenueVO.setTodayRevenue(todayRevenue);
+            //统计昨日数据
+            List<AppRevenueHourVO> yesterdayRevenues = adRecordMapper.getHourRevenue(appId, 2);
+            Map<String, BigDecimal> yesterdayHourMap = yesterdayRevenues.stream().collect(Collectors.toMap(AppRevenueHourVO::getHour, AppRevenueHourVO::getRevenue));
+            appRevenueVO.setYesterdayHourRevenueMap(yesterdayHourMap);
+            BigDecimal yesterdayRevenue = yesterdayHourMap.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
+            appRevenueVO.setYesterdayRevenue(yesterdayRevenue);
+            //统计本月数据
+            List<AppRevenueHourVO> monthRevenues = adRecordMapper.getHourRevenue(appId, 3);
+            BigDecimal monthRevenue = monthRevenues.stream().map(AppRevenueHourVO::getRevenue).reduce(BigDecimal.ZERO, BigDecimal::add);
+            appRevenueVO.setMonthRevenue(monthRevenue);
+            appRevenueList.add(appRevenueVO);
+            countDownLatch.countDown();
+        } while (index > 0);
+        vo.setAppRevenueList(appRevenueList);
+        //直接统计各应用的收益
+        vo.setTodayTotalRevenue(appRevenueList.stream().map(DashboardAppRevenueVO::getTodayRevenue).reduce(BigDecimal.ZERO, BigDecimal::add));
+        vo.setYesterdayTotalRevenue(appRevenueList.stream().map(DashboardAppRevenueVO::getYesterdayRevenue).reduce(BigDecimal.ZERO, BigDecimal::add));
+        vo.setMonthTotalRevenue(appRevenueList.stream().map(DashboardAppRevenueVO::getMonthRevenue).reduce(BigDecimal.ZERO, BigDecimal::add));
+        return vo;
+    }
+
+    /**
+     * 查询代理商顶部数据
+     */
+    @Override
+    public AgentTopCountView getAppTopCount(String appIds) {
+        AgentTopCountView view = new AgentTopCountView();
+        int dyzAdCount = adRecordMapper.countByAppIds(appIds);
+        view.setAdCount(dyzAdCount);
+        int dyzUserCount = appUserMapper.countUserByAppIds(appIds);
+        view.setUserCount(dyzUserCount);
+        BigDecimal totalRevenue = new BigDecimal(0);
+        BigDecimal dyzRevenue = appUserMapper.countRevenueByAppIds(appIds);
+        if (Objects.nonNull(dyzRevenue)) {
+            totalRevenue = totalRevenue.add(dyzRevenue);
+        }
+        view.setRevenue(totalRevenue);
+        int bannedCount = appUserMapper.countBannedByAppIds(appIds);
+        view.setRiskCount(bannedCount);
+        return view;
+    }
+
+    /**
+     * 查询用户风控分时数据统计
+     */
+    @Override
+    public DashboardRiskVO userStatics(String appId) {
+        DashboardRiskVO vo = new DashboardRiskVO();
+        vo.setTodayRegistryCount(appUserMapper.countRegistryUser(appId, 1));
+        vo.setYesterdayRegistryCount(appUserMapper.countRegistryUser(appId, 2));
+        vo.setMonthRegistryCount(appUserMapper.countRegistryUser(appId, 3));
+
+        vo.setTodayLoginCount(appUserMapper.countLoginUser(appId, 1));
+        vo.setYesterdayLoginCount(appUserMapper.countLoginUser(appId, 2));
+        vo.setMonthLoginCount(appUserMapper.countLoginUser(appId, 3));
+
+        List<AppUserHourVO> registryHour = appUserMapper.countRegistryHour(appId, 1);
+        Map<String, Integer> registryMap = registryHour.stream().collect(Collectors.toMap(AppUserHourVO::getTime, AppUserHourVO::getCount));
+        vo.setTodayRegistryHourMap(registryMap);
+
+        List<AppUserHourVO> loginHour = appUserMapper.countLoginHour(appId, 1);
+        Map<String, Integer> loginMap = loginHour.stream().collect(Collectors.toMap(AppUserHourVO::getTime, AppUserHourVO::getCount));
+        vo.setTodayLoginHourMap(loginMap);
+        return vo;
+    }
+
+    /**
+     * 查询代理商首页广告数据统计
+     */
+    @Override
+    public List<AgentAdGroupStaticsVO> getAgentProfit(String appIds) {
+        // 使用数据库聚合减少内存占用
+        List<AgentNetworkAgg> todayHourAgg = adRecordMapper.getTodayAggByNetworkHour(appIds);
+        List<AgentNetworkAgg> yesterdayAgg = adRecordMapper.getYesterdayAggByNetwork(appIds);
+        List<AgentNetworkAgg> monthAgg = adRecordMapper.getMonthAggByNetwork(appIds);
+
+        Map<String, List<AgentNetworkAgg>> todayByNetwork = todayHourAgg.stream().collect(Collectors.groupingBy(AgentNetworkAgg::getNetworkId));
+        Map<String, AgentNetworkAgg> yesterdayByNetwork = yesterdayAgg.stream().collect(Collectors.toMap(AgentNetworkAgg::getNetworkId, v -> v, (a, b) -> a));
+        Map<String, AgentNetworkAgg> monthByNetwork = monthAgg.stream().collect(Collectors.toMap(AgentNetworkAgg::getNetworkId, v -> v, (a, b) -> a));
+
+        List<AgentAdGroupStaticsVO> result = new ArrayList<>();
+        for (Map.Entry<String, List<AgentNetworkAgg>> e : todayByNetwork.entrySet()) {
+            String networkId = e.getKey();
+            List<AgentNetworkAgg> hourly = e.getValue();
+            AgentAdGroupStaticsVO vo = new AgentAdGroupStaticsVO();
+            vo.setNetowrkId(networkId);
+            vo.setNetowrkName(hourly.get(0).getNetworkName());
+
+            BigDecimal todayRevenue = hourly.stream().map(AgentNetworkAgg::getRevenue).reduce(BigDecimal.ZERO, BigDecimal::add);
+            BigDecimal todayEcpm = hourly.stream().map(AgentNetworkAgg::getEcpm).reduce(BigDecimal.ZERO, BigDecimal::add)
+                    .divide(new BigDecimal(Math.max(1, hourly.size())), 2, RoundingMode.HALF_UP);
+
+            AgentNetworkAgg yAgg = yesterdayByNetwork.get(networkId);
+            AgentNetworkAgg mAgg = monthByNetwork.get(networkId);
+
+            vo.setTodayRevenue(todayRevenue);
+            vo.setYesterdayRevenue(yAgg != null ? yAgg.getRevenue() : BigDecimal.ZERO);
+            vo.setMonthRevenue(mAgg != null ? mAgg.getRevenue() : BigDecimal.ZERO);
+
+            vo.setTodayEcpm(todayEcpm);
+            vo.setYesterdayEcpm(yAgg != null ? yAgg.getEcpm() : BigDecimal.ZERO);
+            vo.setMonthEcpm(mAgg != null ? mAgg.getEcpm() : BigDecimal.ZERO);
+
+            Map<String, BigDecimal> hourRev = new HashMap<>();
+            Map<String, BigDecimal> hourEcpm = new HashMap<>();
+            for (AgentNetworkAgg a : hourly) {
+                hourRev.put(a.getTime(), a.getRevenue());
+                hourEcpm.put(a.getTime(), a.getEcpm());
+            }
+            vo.setTodayHourRevenueMap(hourRev);
+            vo.setTodayHourEcpmMap(hourEcpm);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    /**
+     * 保存记录
+     * 始终创建新的事务以保障子方法的独立事务
+     */
+    @Override
+//    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
+    public String saveRecordAndChangeUser(DyzAdRecordParam param, YtNovelUser user) {
+        //增加广告记录
+        YtDyzAdRecord adRecord = new YtDyzAdRecord();
+        if (Objects.isNull(param.getBeginTime()) || "null".equals(param.getBeginTime())) {
+            if ("null".equals(param.getBeginTime())) {
+                log.warn("param beginTime is null !");
+            }
+            param.setBeginTime(param.getFinishTime());
+        }
+        BeanUtils.copyProperties(param, adRecord);
+        adRecord.setRecordId(IdUtil.fastSimpleUUID());
+        adRecord.setNetworkName(AdPlatformTypeEnum.getDesc(Integer.parseInt(param.getNetworkFormId())));
+        if (param.getLoginStatus() != null && AdRecordEnum.LOGIN_BEFORE.getCode().equals(param.getLoginStatus())) {
+            adRecordMapper.addOneVisitor(adRecord);
+        } else {
+            adRecordMapper.addOne(adRecord);
+        }
+        boolean updateUserTotal = true;
+        // 游客登陆下 激励视频广告记录不算入用户统计
+        if (AdRecordEnum.LOGIN_BEFORE.getCode().equals(param.getLoginStatus())
+                && AdSourceTypeEnum.rewarded_video.getAdSourceType() == param.getAdSourceType()) {
+            updateUserTotal = false;
+        }
+        if (updateUserTotal) {
+            //修改用户信息, 广告次数+1  总收益 + revenue
+            if (param.getRevenue() == null) {
+                param.setRevenue(BigDecimal.ZERO);
+            }
+            appUserMapper.updateTotal(user.getUserId(), 1, param.getRevenue());
+        }
+        return adRecord.getRecordId();
+    }
+
+    @Override
+    public List<YtDyzAdRecord> queryRecordByIds(List<String> adRecordIds) {
+        return adRecordMapper.selectRecordByIds(AdRecordEnum.LOGIN_BEFORE.getCode(), adRecordIds);
+    }
+
+}

+ 176 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/AppUserServiceImpl.java

@@ -0,0 +1,176 @@
+package com.ytpm.novel.service.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import com.ytpm.agent.enums.UserStatusEnum;
+import com.ytpm.app.model.YtDyzLoginRecord;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.app.model.YtUser;
+import com.ytpm.app.param.WxLoginParam;
+import com.ytpm.app.view.WxLoginResult;
+import com.ytpm.app.view.WxUserInfo;
+import com.ytpm.constant.StrConstant;
+import com.ytpm.handle.CustomerException;
+import com.ytpm.novel.dao.AppUserMapper;
+import com.ytpm.novel.dao.LoginRecordMapper;
+import com.ytpm.novel.redis.RedisService;
+import com.ytpm.novel.service.AppUserService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.Objects;
+
+@Service
+@RefreshScope
+public class AppUserServiceImpl implements AppUserService {
+
+    @Resource
+    private AppUserMapper appUserMapper;
+    @Resource
+    private LoginRecordMapper loginRecordMapper;
+    @Resource
+    private RedisService redisService;
+    @Value("${risk.config.banned.tips}")
+    private String tips;
+
+    /**
+     * openid查询用户信息
+     */
+    @Override
+    public YtNovelUser selectByOpenid(String openid, Long ditchId) {
+        return appUserMapper.getYtAppUser(openid, ditchId);
+    }
+
+     @Override
+    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
+    public YtNovelUser crudForNewTrans(WxLoginParam param, WxUserInfo wxUserInfo, WxLoginResult loginResult) {
+        //处理用户注册或登录更新信息
+        YtNovelUser old = appUserMapper.getYtAppUser(loginResult.getOpenid(), param.getDitchId());
+        // 获取游客登陆信息
+        if (Objects.isNull(old)) {
+            old = appUserMapper.getByDeviceAndDitch(param.getDeviceId(), param.getDitchId());
+            if (Objects.nonNull(old)) {
+                old.setWxOpenId(wxUserInfo.getOpenid());
+                old.setNickName(wxUserInfo.getNickname());
+                old.setHeadImg(wxUserInfo.getHeadimgurl());
+                YtNovelUser newUser = new YtNovelUser();
+                newUser.setUserId(old.getUserId());
+                newUser.setWxOpenId(wxUserInfo.getOpenid());
+                newUser.setNickName(wxUserInfo.getNickname());
+                newUser.setHeadImg(wxUserInfo.getHeadimgurl());
+                appUserMapper.updateUser(newUser);
+            }
+        }
+        if (Objects.isNull(old)) {
+            old = registerByWx(param, wxUserInfo, loginResult);
+        } else {
+            deadWithUserCrud(old, wxUserInfo, param);
+        }
+        // 登陆成功初始化传递信息
+        setExtInfo(old, wxUserInfo.getHeadimgurl());
+        // 添加用户登录记录
+        addLoginRecord(param, old.getUserId());
+        return old;
+    }
+
+    /**
+     * 设置扩展信息
+     */
+    private void setExtInfo(YtNovelUser old, String headImgUrl) {
+        old.setHeadImg(headImgUrl);
+        old.setLoginRecordList(loginRecordMapper.getLoginRecords(old.getUserId()));
+    }
+
+    /**
+     * 增加用户登录记录
+     */
+    private void addLoginRecord(WxLoginParam param, String userId) {
+        YtDyzLoginRecord loginRecord = new YtDyzLoginRecord();
+        loginRecord.setRecordId(IdUtil.fastSimpleUUID());
+        loginRecord.setUserId(userId);
+        loginRecord.setLoginTime(new Date());
+        loginRecord.setDeviceBrand(param.getBrand());
+        loginRecord.setDeviceModel(param.getModel());
+        loginRecord.setLoginIp(param.getLoginIp());
+        loginRecord.setOperator(param.getIpOperator());
+        loginRecord.setIpAddr(param.getIpLocation());
+        loginRecord.setPhoneJson(param.getPhoneJson());
+        loginRecordMapper.insertOne(loginRecord);
+    }
+
+    private String getTipsMsg() {
+        String[] split = tips.split(",");
+        return split[RandomUtil.randomInt(split.length)];
+    }
+
+    /**
+     * 处理用户数据
+     */
+    private void deadWithUserCrud(YtNovelUser old, WxUserInfo wxUserInfo, WxLoginParam param) {
+        //处于风控状态的用户不允许登录
+        if (!old.getUserStatus().equals(UserStatusEnum.NORMAL.getCode())) {
+            throw new CustomerException(getTipsMsg());
+        }
+        YtNovelUser newUser = new YtNovelUser();
+        newUser.setUserId(old.getUserId());
+        newUser.setNickName(wxUserInfo.getNickname());
+        newUser.setHeadImg(wxUserInfo.getHeadimgurl());
+        newUser.setLastLoginTime(new Date());
+        newUser.setLastLoginIp(param.getLoginIp());
+        newUser.setPhone(param.getPhone());
+        newUser.setPhoneJson(param.getPhoneJson());
+        newUser.setDeviceId(param.getDeviceId());
+        YtDyzLoginRecord wxLoginRecord = loginRecordMapper.getLastLoginRecord(old.getUserId(), 0);
+        YtDyzLoginRecord visitorLoginRecord = loginRecordMapper.getLastLoginRecord(old.getUserId(), 1);
+        if (wxLoginRecord == null && visitorLoginRecord == null
+                || wxLoginRecord != null && wxLoginRecord.getLoginTime() != null
+                && DateUtil.compare(wxLoginRecord.getLoginTime(), newUser.getLastLoginTime(), "yyyy-MM-dd") < 0) {
+            newUser.setLoginDays(old.getLoginDays() + 1);
+        }
+        appUserMapper.updateUser(newUser);
+    }
+
+
+    /**
+     * 注册用户
+     */
+    public YtNovelUser registerByWx(WxLoginParam param, WxUserInfo wxUserInfo, WxLoginResult loginResult) {
+        YtNovelUser newUser = new YtNovelUser();
+        newUser.setUserId(redisService.getAppUserId());
+        newUser.setDitchId(param.getDitchId());
+        newUser.setAppId(param.getAppId());
+        newUser.setDeviceId(param.getDeviceId());
+        newUser.setPhone(param.getPhone());
+        newUser.setPhoneJson(param.getPhoneJson());
+        newUser.setLastLoginIp(param.getLoginIp());
+        newUser.setHeadImg(wxUserInfo.getHeadimgurl());
+        newUser.setNickName(wxUserInfo.getNickname());
+        newUser.setWxOpenId(loginResult.getOpenid());
+        initUser(newUser);
+        appUserMapper.addOne(newUser);
+        return newUser;
+    }
+
+
+    private void initUser(YtUser user) {
+        user.setLastLoginTime(new Date());
+        user.setRegistryTime(new Date());
+        user.setLoginDays(1);
+        user.setTotalIncome(BigDecimal.ZERO);
+        user.setSignDays(0);
+        user.setUserStatus(UserStatusEnum.NORMAL.getCode());
+
+        //根据设备ID获取平台ID 获取不到再生成平台ID
+        String platformId = appUserMapper.getByDeviceId(user.getDeviceId(), user.getWxOpenId());
+        user.setPlatformId(StrUtil.isBlank(platformId) ?
+                (StrConstant.PLATFORM_ID_PREFIX + IdUtil.getSnowflakeNextIdStr()) : platformId);
+    }
+}

+ 146 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/NovelServiceImpl.java

@@ -0,0 +1,146 @@
+package com.ytpm.novel.service.impl;
+
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSONArray;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.ytpm.app.model.YtNovel;
+import com.ytpm.app.model.YtNovelCategory;
+import com.ytpm.app.model.YtNovelChapter;
+import com.ytpm.app.model.YtNovelReadRecord;
+import com.ytpm.app.model.YtNovelShelf;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.handle.CustomerException;
+import com.ytpm.handle.ValidatedException;
+import com.ytpm.novel.dao.AppUserMapper;
+import com.ytpm.novel.dao.NovelMapper;
+import com.ytpm.novel.model.dto.YtNovelDto;
+import com.ytpm.novel.model.param.NovelPageParam;
+import com.ytpm.novel.model.param.NovelRecordParam;
+import com.ytpm.novel.model.param.NovelShelfParam;
+import com.ytpm.novel.redis.RedisService;
+import com.ytpm.novel.service.NovelService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025-09-30 14:46
+ */
+@Slf4j
+@Service("novelService")
+public class NovelServiceImpl implements NovelService {
+
+    @Resource
+    private RedisService redisService;
+    @Resource
+    private NovelMapper novelMapper;
+    @Resource
+    private AppUserMapper appUserMapper;
+
+    @Override
+    public PageInfo<YtNovelDto> selectNovelPage(NovelPageParam param) {
+        PageHelper.startPage(param.getPage(), param.getLimit());
+        List<YtNovelDto> list = novelMapper.selectNovelList(param);
+        return new PageInfo<>(list);
+    }
+
+    @Override
+    public YtNovelDto selectNovelById(String novelId, String userId) {
+        YtNovelDto dto = novelMapper.selectByNovelId(novelId);
+        List<YtNovelChapter> chapterList = novelMapper.selectNovelChapter(novelId);
+        dto.setChapterList(chapterList);
+        if (StrUtil.isNotEmpty(userId)) {
+            // 查询书籍是否在书架中
+            YtNovelShelf shelf = novelMapper.selectShelf(userId);
+            if (shelf != null && StrUtil.isNotEmpty(shelf.getNovelIds())) {
+                List<String> novels = JSONArray.parseArray(shelf.getNovelIds(), String.class);
+                dto.setIfSelfShelf(novels.contains(novelId) ? 1 : 0);
+            }
+            // 查询最后阅读章节
+            YtNovelReadRecord lastReadRecord = novelMapper.selectLastRecord(userId, novelId);
+            if (lastReadRecord != null && CollUtil.isNotEmpty(chapterList)) {
+                YtNovelChapter chapter = chapterList.stream()
+                        .filter(item -> StrUtil.equals(item.getChapterId(), lastReadRecord.getChapterId()))
+                        .findFirst().orElse(null);
+                dto.setLastReadChapter(chapter);
+            }
+        }
+        return dto;
+    }
+
+    @Override
+    public List<YtNovel> selectNovelShelf(String userId) {
+        List<YtNovel> list = new ArrayList<>();
+        YtNovelShelf shelf = novelMapper.selectShelf(userId);
+        if (shelf != null && StrUtil.isNotEmpty(shelf.getNovelIds())) {
+            List<String> novels = JSONArray.parseArray(shelf.getNovelIds(), String.class);
+            list = novelMapper.selectByNovelIds(novels);
+        }
+        return list;
+    }
+
+    @Override
+    public String saveNovelRecord(NovelRecordParam param) {
+        YtNovelUser ytNovelUser = appUserMapper.selectPrimaryKey(param.getUserId());
+        if (ytNovelUser == null) {
+            throw new CustomerException("未知用户");
+        }
+        YtNovelReadRecord readRecord = new YtNovelReadRecord();
+        readRecord.setRecordId(IdUtil.fastSimpleUUID());
+        readRecord.setUserId(param.getUserId());
+        readRecord.setNovelId(param.getNovelId());
+        readRecord.setChapterId(param.getChapterId());
+        readRecord.setNovelName(param.getNovelName());
+        readRecord.setNovelChapterName(param.getChapterName());
+        readRecord.setReadTime(new Date());
+        readRecord.setCreateTime(new Date());
+        readRecord.setUpdateTime(new Date());
+        readRecord.setCreateBy(param.getUserId());
+        readRecord.setUpdateBy(param.getUserId());
+        novelMapper.insertReadRecord(readRecord);
+        return "保存记录成功";
+    }
+
+    @Override
+    public String saveNovelShelf(NovelShelfParam param) {
+        if (StrUtil.isEmpty(param.getUserId())) {
+            throw new ValidatedException("用户ID不能为空");
+        }
+
+        YtNovelShelf shelf = novelMapper.selectShelf(param.getUserId());
+        if (shelf == null) {
+            shelf = new YtNovelShelf();
+            shelf.setShelfId(IdUtil.fastSimpleUUID());
+            shelf.setUserId(param.getUserId());
+            shelf.setShelfName(param.getShelfName());
+            shelf.setCreateTime(new Date());
+            shelf.setCreateBy(param.getUserId());
+            if (param.getNovelIds() != null) {
+                shelf.setNovelIds(JSONArray.toJSONString(param.getNovelIds()));
+            }
+            novelMapper.insertNovelShelf(shelf);
+        } else {
+            BeanUtil.copyProperties(param, shelf);
+            shelf.setUpdateTime(new Date());
+            shelf.setUpdateBy(param.getUserId());
+            novelMapper.updateNovelShelf(shelf);
+        }
+        return "更新书架成功";
+    }
+
+    @Override
+    public List<YtNovelCategory> selectCategoryList() {
+        return Collections.emptyList();
+    }
+}

+ 228 - 0
yt-novel/yt-novel-service/src/main/java/com/ytpm/novel/service/impl/VisitorLoginServiceImpl.java

@@ -0,0 +1,228 @@
+package com.ytpm.novel.service.impl;
+
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.ytpm.agent.enums.AdRecordEnum;
+import com.ytpm.agent.enums.UserStatusEnum;
+import com.ytpm.app.enums.LoginType;
+import com.ytpm.app.model.YtDyzLoginRecord;
+import com.ytpm.app.model.YtDyzUser;
+import com.ytpm.app.model.YtNovelUser;
+import com.ytpm.app.model.YtUser;
+import com.ytpm.app.param.DyzAdRecordParam;
+import com.ytpm.app.param.LoginParam;
+import com.ytpm.app.param.VisitorLoginParam;
+import com.ytpm.app.view.WxDefaultConfig;
+import com.ytpm.constant.StrConstant;
+import com.ytpm.feign.RiskFeign;
+import com.ytpm.general.RepMessage;
+import com.ytpm.general.Result;
+import com.ytpm.handle.AbstractLoginService;
+import com.ytpm.handle.CommonException;
+import com.ytpm.handle.CustomerException;
+import com.ytpm.novel.dao.AppUserMapper;
+import com.ytpm.novel.dao.LoginRecordMapper;
+import com.ytpm.novel.redis.RedisService;
+import com.ytpm.novel.service.AdService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 游客登陆 回调实现类
+ *
+ * @author lih
+ * @date 2025/9/4
+ */
+
+@Slf4j
+@Component
+public class VisitorLoginServiceImpl extends AbstractLoginService {
+
+    @Resource
+    private AppUserMapper appUserMapper;
+    @Resource
+    private LoginRecordMapper loginRecordMapper;
+    @Resource
+    private RiskFeign riskFeign;
+    @Resource
+    private RedisService redisService;
+    @Resource
+    private AdService adService;
+
+    @Override
+    protected WxDefaultConfig getLoginConfig(Integer appType) {
+        if (appType == null) {
+            throw new CommonException("appType is null");
+        }
+        return appUserMapper.getDefaultConfig(appType);
+    }
+
+    @Override
+    protected LoginType getLoginType() {
+        return LoginType.VISITOR;
+    }
+
+    @Override
+    protected void updateUserStatus(YtUser ytDyzUser, UserStatusEnum statusEnum, String reason) {
+        if (ytDyzUser != null) {
+            YtNovelUser newUser = new YtNovelUser();
+            newUser.setUserId(ytDyzUser.getUserId());
+            newUser.setUserStatus(statusEnum.getCode());
+            newUser.setRiskReason(reason);
+            if (UserStatusEnum.NORMAL == statusEnum) {
+                newUser.setRiskReason("");
+            }
+            appUserMapper.updateUser(newUser);
+        }
+    }
+
+    @Override
+    protected void validateParams(LoginParam loginParam, HttpServletRequest request) {
+        // deviceId空值处理
+        if (StrUtil.isEmpty(loginParam.getDeviceId())) {
+            log.warn("[visitor login validate]deviceId is empty!");
+            // 尝试从phoneJson中获取
+            if (StrUtil.isNotEmpty(loginParam.getPhoneJson())) {
+                JSONObject phoneJson = JSONObject.parseObject(loginParam.getPhoneJson());
+                if (phoneJson.get("device_basic") != null) {
+                    String deviveId = phoneJson.getJSONObject("device_basic").getString("device_id");
+                    loginParam.setDeviceId(deviveId);
+                }
+            }
+        }
+        if (StrUtil.isNotEmpty(loginParam.getRequestId())) {
+            log.info("[visitor login validate]requestId check...");
+            // 游客登陆幂等校验
+            String redisKey = StrUtil.format("visitor:requestId:{}:{}:{}", loginParam.getDitchId(),
+                    loginParam.getAppType(), loginParam.getDeviceId());
+            if (!redisService.hasKey(redisKey)) {
+                throw new IllegalArgumentException("重复请求");
+            }
+            String cacheKey = redisService.getStr(redisKey);
+            if (!StrUtil.equals(cacheKey, loginParam.getRequestId())) {
+                throw new IllegalArgumentException("重复请求");
+            }
+            redisService.del(redisKey);
+        }
+    }
+
+    @Override
+    protected YtUser loginHandler(LoginParam loginParam, Map<String, Object> paramMap) {
+        // 唯一性判断
+        YtNovelUser ytNovelUser = appUserMapper.getByDeviceAndDitch(loginParam.getDeviceId(), loginParam.getDitchId());
+        if (ytNovelUser == null) {
+            log.warn(StrUtil.format("visitor register[ deviceId:{}, ditchId: {}]",
+                    loginParam.getDeviceId(), loginParam.getDitchId()));
+            beforeRegistryUser(loginParam, paramMap);
+            ytNovelUser = registryUser(loginParam, paramMap);
+        } else {
+            log.warn(StrUtil.format("visitor login[ deviceId:{}, ditchId: {}]",
+                    loginParam.getDeviceId(), loginParam.getDitchId()));
+            //当前渠道已有用户,校验用户是否处于风控中 & 更新用户信息
+            beforeDeadWithUserCrud(loginParam, ytNovelUser, paramMap);
+            //如果当前登录是本日第一次登录则登录天数+1
+            //202550916 游客登陆不记录登陆信息
+            deadWithUserCrud(loginParam, ytNovelUser, paramMap);
+        }
+        // 保存游客广告信息
+        WxDefaultConfig defaultConfig = (WxDefaultConfig) paramMap.get("defaultConfig");
+        saveVisitorAdRecord(ytNovelUser, loginParam, defaultConfig);
+        ytNovelUser.setLoginType(LoginType.VISITOR);
+        return ytNovelUser;
+    }
+
+    private YtNovelUser registryUser(LoginParam loginParam, Map<String, Object> paramMap) {
+        YtNovelUser newUser = new YtNovelUser();
+        newUser.setUserId(redisService.getAppUserId());
+        newUser.setPhone(loginParam.getPhone());
+        newUser.setDeviceId(loginParam.getDeviceId());
+        newUser.setNickName("visitor_" + RandomUtil.randomString(10));
+        newUser.setLastLoginTime(new Date());
+        newUser.setRegistryTime(new Date());
+        newUser.setLastLoginIp(loginParam.getLoginIp());
+        newUser.setLoginDays(1);
+        newUser.setTotalIncome(BigDecimal.ZERO);
+        newUser.setDitchId(loginParam.getDitchId());
+        newUser.setSignDays(0);
+        newUser.setAppId(loginParam.getAppId());
+        newUser.setUserStatus(UserStatusEnum.NORMAL.getCode());
+        String platformId = appUserMapper.getPlatformByDeviceId(loginParam.getDeviceId());
+        newUser.setPlatformId(StrUtil.isBlank(platformId) ?
+                (StrConstant.PLATFORM_ID_PREFIX + IdUtil.getSnowflakeNextIdStr()) : platformId);
+        appUserMapper.addOne(newUser);
+        return newUser;
+    }
+
+    private void deadWithUserCrud(LoginParam loginParam, YtUser old, Map<String, Object> paramMap) {
+        YtNovelUser newUser = new YtNovelUser();
+        newUser.setUserId(old.getUserId());
+        newUser.setLastLoginTime(new Date());
+        newUser.setLastLoginIp(loginParam.getLoginIp());
+        newUser.setPhone(loginParam.getPhone());
+        newUser.setDeviceId(loginParam.getDeviceId());
+        appUserMapper.updateUser(newUser);
+    }
+
+    @Override
+    protected void saveLoginRecord(YtDyzLoginRecord loginRecord) {
+        loginRecordMapper.insertOne(loginRecord);
+    }
+
+    @Override
+    protected Result<?> remoteCheckLoginRisk(YtUser ytUser) {
+        return riskFeign.checkLoginRisk((YtDyzUser) ytUser);
+    }
+
+    /**
+     * 设置扩展信息
+     */
+    @Override
+    protected void setExtInfo(YtUser ytUser, Map<String, Object> paramMap) {
+        super.setExtInfo(ytUser, paramMap);
+        YtNovelUser dyzUser = (YtNovelUser) ytUser;
+        dyzUser.setLoginRecordList(loginRecordMapper.getLoginRecords(dyzUser.getUserId()));
+    }
+
+
+    // 仅保存登陆时的三条广告信息 并传入登陆实体用于后续 risk风控校验
+    private void saveVisitorAdRecord(YtNovelUser ytNovelUser, LoginParam loginParam, WxDefaultConfig defaultConfig) {
+        VisitorLoginParam visitorLoginParam = (VisitorLoginParam) loginParam;
+        List<DyzAdRecordParam> preAdList = visitorLoginParam.getPreAdList();
+        // 保存登陆前传递的广告记录
+        List<String> adRecordIds = new ArrayList<>();
+        if (CollUtil.isNotEmpty(preAdList)) {
+            BigDecimal totalRevenue = BigDecimal.ZERO;
+            for (DyzAdRecordParam adRecordParam : preAdList) {
+                if (adRecordParam == null) {
+                    continue;
+                }
+                adRecordParam.setUserId(ytNovelUser.getUserId());
+                adRecordParam.setNickName(ytNovelUser.getNickName());
+                adRecordParam.setAppId(ytNovelUser.getAppId());
+                adRecordParam.setLoginStatus(AdRecordEnum.LOGIN_BEFORE.getCode());
+                String recordId = adService.saveRecordAndChangeUser(adRecordParam, ytNovelUser);
+                adRecordIds.add(recordId);
+                totalRevenue = adRecordParam.getRevenue() == null ? totalRevenue : totalRevenue.add(adRecordParam.getRevenue());
+            }
+            log.info(StrUtil.format("[visitor adRecords] userId:{} recordCount:{} revenue:{} ",
+                    ytNovelUser.getUserId(), adRecordIds.size(), totalRevenue));
+        }
+        if (CollUtil.isEmpty(adRecordIds)) {
+            throw new CustomerException(StrUtil.emptyToDefault(defaultConfig.getLowValueTip(), RepMessage.RISK_VISITOR_LOWER_VALUE));
+        }
+        ytNovelUser.setPreAdRecordList(adService.queryRecordByIds(adRecordIds));
+    }
+
+}

+ 64 - 0
yt-novel/yt-novel-service/src/main/resources/bootstrap.yml

@@ -0,0 +1,64 @@
+spring:
+  main:
+    allow-bean-definition-overriding: true
+  application:
+    name: novel-service
+    name-zh: '小说服务'
+  profiles:
+    # 当前为本地模式
+    active: local
+server:
+  port: 25829
+yt:
+  port: 8848
+  file-extension: yml
+  # 本地环境
+  local:
+    addr: 127.0.0.1
+    namespace: 1aff43c9-b617-4d0b-8c3a-9d8b44a8beae
+  # 测试环境
+  dev:
+    addr: 127.0.0.1
+    namespace: ad14a1aa-fe06-473a-9050-9afa26ec0020
+  # 线上环境
+  prod:
+    addr: 119.45.117.147
+    namespace: 766c8aa7-cd4c-47c5-8540-3a4dcdbdb4ae
+---
+spring:
+  profiles: local
+  cloud:
+    nacos:
+      discovery:
+        server-addr: ${yt.local.addr}:${yt.port}
+        namespace: ${yt.local.namespace}
+      config:
+        server-addr: ${yt.local.addr}:${yt.port}
+        namespace: ${yt.local.namespace}
+        file-extension: ${yt.file-extension}
+---
+spring:
+  profiles: dev
+  cloud:
+    nacos:
+      discovery:
+        server-addr: ${yt.dev.addr}:${yt.port}
+        namespace: ${yt.dev.namespace}
+      config:
+        server-addr: ${yt.dev.addr}:${yt.port}
+        namespace: ${yt.dev.namespace}
+        file-extension: ${yt.file-extension}
+---
+spring:
+  profiles: prod
+  main:
+    allow-bean-definition-overriding: false
+  cloud:
+    nacos:
+      discovery:
+        server-addr: ${yt.prod.addr}:${yt.port}
+        namespace: ${yt.prod.namespace}
+      config:
+        server-addr: ${yt.prod.addr}:${yt.port}
+        namespace: ${yt.prod.namespace}
+        file-extension: ${yt.file-extension}

+ 280 - 0
yt-novel/yt-novel-service/src/main/resources/mapper/AdRecordMapper.xml

@@ -0,0 +1,280 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ytpm.novel.dao.AdRecordMapper">
+
+    <insert id="addOne">
+        insert into yt_dyz_ad_record
+        (
+            record_id,
+            user_id,
+            nick_name,
+            placement_id,
+            ad_source_id,
+            revenue,
+            network_form_id,
+            network_name,
+            network_placement_id,
+            begin_time,
+            finish_time,
+            result_json,
+            ad_source_index,
+            ad_source_type,
+            ecpm,
+            app_id
+        )
+        values
+            (
+                #{recordId},
+                #{userId},
+                #{nickName},
+                #{placementId},
+                #{adSourceId},
+                #{revenue},
+                #{networkFormId},
+                #{networkName},
+                #{networkPlacementId},
+                ifnull(#{beginTime}, now()),
+                ifnull(#{finishTime}, now()),
+                #{resultJson},
+                #{adSourceIndex},
+                #{adSourceType},
+                #{ecpm},
+                #{appId}
+            )
+    </insert>
+    <insert id="addOneVisitor">
+        insert into yt_dyz_ad_record_visitor
+        (
+            record_id,
+            user_id,
+            nick_name,
+            placement_id,
+            ad_source_id,
+            revenue,
+            network_form_id,
+            network_name,
+            network_placement_id,
+            begin_time,
+            finish_time,
+            result_json,
+            ad_source_index,
+            ad_source_type,
+            ecpm,
+            app_id
+        )
+        values
+            (
+                #{recordId},
+                #{userId},
+                #{nickName},
+                #{placementId},
+                #{adSourceId},
+                #{revenue},
+                #{networkFormId},
+                #{networkName},
+                #{networkPlacementId},
+                ifnull(#{beginTime}, now()),
+                ifnull(#{finishTime}, now()),
+                #{resultJson},
+                #{adSourceIndex},
+                #{adSourceType},
+                #{ecpm},
+                #{appId}
+            )
+    </insert>
+    <select id="countByAppIds" resultType="java.lang.Integer">
+        select
+        count(record_id)
+        from yt_dyz_ad_record
+        where ad_source_type = 1 and app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <select id="getTodayAggByNetworkHour" resultType="com.ytpm.novel.model.view.AgentNetworkAgg">
+        SELECT
+            network_form_id AS networkId,
+            MIN(network_name) AS networkName,
+            DATE_FORMAT(finish_time,'%Y-%m-%d %H:00:00') AS `time`,
+            COUNT(1) AS cnt,
+            SUM(revenue) AS revenue,
+            AVG(ecpm) AS ecpm
+        FROM yt_dyz_ad_record
+        WHERE app_id IN
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+        AND DATE(finish_time) = DATE(NOW())
+        GROUP BY network_form_id, DATE_FORMAT(finish_time,'%Y-%m-%d %H:00:00')
+    </select>
+    <select id="getYesterdayAggByNetwork" resultType="com.ytpm.novel.model.view.AgentNetworkAgg">
+        SELECT
+            network_form_id AS networkId,
+            MIN(network_name) AS networkName,
+            NULL AS `time`,
+            COUNT(1) AS cnt,
+            SUM(revenue) AS revenue,
+            AVG(ecpm) AS ecpm
+        FROM yt_dyz_ad_record
+        WHERE app_id IN
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+        AND DATE(finish_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+        GROUP BY network_form_id
+    </select>
+    <select id="getMonthAggByNetwork" resultType="com.ytpm.novel.model.view.AgentNetworkAgg">
+        SELECT
+            network_form_id AS networkId,
+            MIN(network_name) AS networkName,
+            NULL AS `time`,
+            COUNT(1) AS cnt,
+            SUM(revenue) AS revenue,
+            AVG(ecpm) AS ecpm
+        FROM yt_dyz_ad_record
+        WHERE app_id IN
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+        AND DATE_FORMAT(finish_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+        GROUP BY network_form_id
+    </select>
+    <select id="getByUserId" resultType="com.ytpm.app.model.YtDyzAdRecord">
+        select
+        record_id, user_id,app_id, nick_name, placement_id, ad_source_id, revenue, network_form_id, network_name,
+        network_placement_id, finish_time, begin_time,result_json,ad_source_type,ad_source_index,ecpm
+        from yt_dyz_ad_record
+        where user_id = #{userId} and DATE_FORMAT(finish_time, '%Y-%m-%d') = DATE_FORMAT(now(), '%Y-%m-%d')
+        <if test="adsourceType != null and adsourceType!=888">
+            and ad_source_type = #{adsourceType}
+        </if>
+        order by finish_time desc
+    </select>
+    <select id="getByUserByParam" resultType="com.ytpm.app.model.YtDyzAdRecord">
+        select
+        record_id, user_id,app_id, nick_name, placement_id, ad_source_id, revenue, network_form_id, network_name,
+        network_placement_id, finish_time, begin_time,result_json,ad_source_type,ad_source_index,ecpm,
+        ifnull(#{loginStatus}, 2) as loginStatus
+        <if test="loginStatus != null and loginStatus == 1">
+            from yt_dyz_ad_record_visitor
+        </if>
+        <if test="loginStatus == null or loginStatus != 1">
+            from yt_dyz_ad_record
+        </if>
+        where user_id = #{userId}
+        <if test="adsourceType != null and adsourceType!=888">
+            and ad_source_type = #{adsourceType}
+        </if>
+        order by finish_time desc
+    </select>
+    <select id="getByUserIds" resultType="com.ytpm.app.model.YtDyzAdRecord">
+        select
+        record_id, user_id,app_id, nick_name, placement_id, ad_source_id, revenue, network_form_id, network_name, network_placement_id, finish_time, begin_time,result_json,ad_source_type,ad_source_index,ecpm
+        from yt_dyz_ad_record
+        where user_id in
+        <foreach collection="userIds.split(',')" open="(" close=")" separator="," item="item">
+            #{item}
+        </foreach>
+        union
+        select
+        record_id, user_id,app_id, nick_name, placement_id, ad_source_id, revenue, network_form_id, network_name, network_placement_id, finish_time, begin_time,result_json,ad_source_type,ad_source_index,ecpm
+        from yt_dyz_ad_record_visitor
+        where ad_source_type != 1 and user_id in
+        <foreach collection="userIds.split(',')" open="(" close=")" separator="," item="item">
+            #{item}
+        </foreach>
+        order by finish_time
+    </select>
+    <select id="getHourRevenue" resultType="com.ytpm.middle.view.AppRevenueHourVO">
+        SELECT
+        HOUR(finish_time) `hour`,
+        DATE_FORMAT(finish_time,'%Y-%m-%d %H:00:00') as `time`,
+        sum( revenue ) revenue
+        FROM
+        yt_dyz_ad_record
+        WHERE
+        app_id = #{appId}
+        <if test="type != null and type ==1">
+            and DATE(finish_time) = DATE(now())
+        </if>
+        <if test="type != null and type ==2">
+            and DATE(finish_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+        </if>
+        <if test="type != null and type ==3">
+            and DATE_FORMAT(finish_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+        </if>
+        GROUP BY
+        `hour`
+    </select>
+    <select id="getRevenueByTime" resultType="java.math.BigDecimal">
+        select
+        sum(ar.revenue)
+        from yt_dyz_ad_record ar
+        join yt_dyz_user au on ar.user_id = au.user_id
+        <where>
+            <if test="userId != null and userId !=''">
+                and ar.user_id = #{userId}
+            </if>
+            <if test="nickName != null and nickName !=''">
+                and au.nick_name like concat('%',#{nickName},'%')
+            </if>
+            <if test="ditchId != null">
+                and au.ditch_id = #{ditchId}
+            </if>
+            <if test="userStatus != null">
+                and au.user_status = #{userStatus}
+            </if>
+            <if test="appIds != null and appIds != ''">
+                and ar.app_id in
+                <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+                    #{item}
+                </foreach>
+            </if>
+            <if test="registryTimeBegin != null ">
+                and au.registry_time <![CDATA[>=]]> #{registryTimeBegin}
+            </if>
+            <if test="registryTimeEnd != null ">
+                and au.registry_time <![CDATA[<=]]> #{registryTimeEnd}
+            </if>
+        </where>
+    </select>
+    <select id="getTodayRecord" resultType="com.ytpm.app.model.YtDyzAdRecord">
+        select
+        record_id, user_id, app_id, nick_name, placement_id, ad_source_id, ad_source_type, ad_source_index, revenue, ecpm, network_form_id, network_name, network_placement_id, finish_time, begin_time, result_json
+        from yt_dyz_ad_record
+        where app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+        <if test="firstDay != null">
+            and finish_time <![CDATA[>=]]> #{firstDay}
+        </if>
+        <if test="lastDay != null">
+            and finish_time <![CDATA[<=]]> #{lastDay}
+        </if>
+    </select>
+    <select id="selectRecordByIds" resultType="com.ytpm.app.model.YtDyzAdRecord">
+        select
+        record_id, user_id, app_id, nick_name, placement_id, ad_source_id, ad_source_type, ad_source_index,
+        revenue, ecpm, network_form_id, network_name, network_placement_id, finish_time, begin_time, result_json
+        <if test="loginStatus != null and loginStatus == 1">
+            from yt_dyz_ad_record_visitor
+        </if>
+        <if test="loginStatus == null or loginStatus != 1">
+            from yt_dyz_ad_record
+        </if>
+        where record_id in <foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach>
+    </select>
+    <select id="getRecordMonthCount" resultType="java.lang.Integer">
+        SELECT count(record_id) as counts
+        FROM yt_dyz_ad_record
+        where user_id= #{userId} and revenue > 0
+        <if test="adSourceType != null and adSourceType!=888">
+            and ad_source_type = #{adSourceType}
+        </if>
+        <if test="startTime != null">
+            and finish_time > #{startTime}
+        </if>
+    </select>
+
+</mapper>

+ 822 - 0
yt-novel/yt-novel-service/src/main/resources/mapper/AppUserMapper.xml

@@ -0,0 +1,822 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ytpm.novel.dao.AppUserMapper">
+    <insert id="addOne" parameterType="com.ytpm.app.model.YtNovelUser">
+        insert into yt_dyz_user
+        (user_id,
+         nick_name,
+         head_img,
+         registry_time,
+         last_login_time,
+         last_login_ip,
+         login_days,
+         total_video,
+         total_income,
+         nearly_income,
+         red_packet_balance,
+         red_packet_amount,
+         points_balance,
+         points_total,
+         withdraw_total,
+         sign_days,
+         user_status,
+         risk_reason,
+         wx_open_id,
+         ditch_id,
+         platform_id,
+         app_id,
+         power,
+         phone,
+         phone_json,
+         device_id)
+        values (#{userId},
+                #{nickName},
+                #{headImg},
+                #{registryTime},
+                #{lastLoginTime},
+                #{lastLoginIp},
+                #{loginDays},
+                #{totalVideo},
+                #{totalIncome},
+                #{nearlyIncome},
+                #{redPacketBalance},
+                #{redPacketAmount},
+                #{pointsBalance},
+                #{pointsTotal},
+                #{withdrawTotal},
+                #{signDays},
+                #{userStatus},
+                #{riskReason},
+                #{wxOpenId},
+                #{ditchId},
+                #{platformId},
+                #{appId},
+                #{power},
+                #{phone},
+                #{phoneJson},
+                #{deviceId})
+    </insert>
+    <insert id="addPowerRecord">
+        insert into yt_dyz_power_record
+        (
+            record_id, user_id, type, remark, add_time
+        )
+        values
+            (
+                #{recordId},#{userId},#{type},#{remark},#{addTime}
+            )
+    </insert>
+    <insert id="saveAppConfig">
+        insert into yt_app_default_config
+        (
+            config_name,
+            open_id,
+            secret,
+            app_id,
+            app_key,
+            app_type,
+            ditch_id
+        )
+        values
+            (
+                #{configName},
+                #{openId},
+                #{secret},
+                #{appId},
+                #{appKey},
+                #{appType},
+                #{ditchId}
+            )
+    </insert>
+    <update id="updateAppConfig">
+        update yt_app_default_config
+        <set>
+            <if test="canSimulator != null">
+                can_simulator = #{canSimulator},
+            </if>
+            <if test="configName != null">
+                config_name = #{configName},
+            </if>
+            <if test="openId != null">
+                open_id = #{openId},
+            </if>
+            <if test="secret != null">
+                secret = #{secret},
+            </if>
+            <if test="appKey != null">
+                app_key = #{appKey},
+            </if>
+            <if test="appType != null">
+                app_type = #{appType},
+            </if>
+            <if test="takuAppId != null">
+                taku_app_id = #{takuAppId},
+            </if>
+            <if test="takuKey != null">
+                taku_key = #{takuKey},
+            </if>
+            <if test="takuBannerPid != null">
+                taku_banner_pid = #{takuBannerPid},
+            </if>
+            <if test="takuNativePid != null">
+                taku_native_pid = #{takuNativePid},
+            </if>
+            <if test="takuRewardPid != null">
+                taku_reward_pid = #{takuRewardPid},
+            </if>
+            <if test="takuInterstitialPid != null">
+                taku_interstitial_pid = #{takuInterstitialPid},
+            </if>
+            <if test="canUseRoot != null">
+                can_use_root = #{canUseRoot},
+            </if>
+            <if test="canUseAdb != null">
+                can_use_adb = #{canUseAdb},
+            </if>
+            <if test="canUseFloat != null">
+                can_use_float = #{canUseFloat},
+            </if>
+            <if test="canAccumulation != null">
+                can_accumulation = #{canAccumulation},
+            </if>
+            <if test="ditchId != null">
+                ditch_id = #{ditchId},
+            </if>
+            <if test="powerWaitTime != null">
+                power_wait_time = #{powerWaitTime},
+            </if>
+            <if test="interstitialIntervalTime != null">
+                interstitial_interval_time = #{interstitialIntervalTime},
+            </if>
+            <if test="flowIntervalTime != null">
+                flow_interval_time = #{flowIntervalTime},
+            </if>
+            <if test="taskLimitTip != null">
+                task_limit_tip = #{taskLimitTip},
+            </if>
+            <if test="lowValueTip != null">
+                low_value_tip = #{lowValueTip},
+            </if>
+            <if test="brushTip != null">
+                brush_tip = #{brushTip},
+            </if>
+            <if test="startWaitTime != null">
+                start_wait_time = #{startWaitTime},
+            </if>
+            <if test="canCacheVideo != null">
+                can_cache_video = #{canCacheVideo},
+            </if>
+            <if test="canAllowAutoRefresh != null">
+                can_allow_auto_refresh = #{canAllowAutoRefresh}
+            </if>
+        </set>
+        where app_id = #{appId}
+    </update>
+    <update id="addOnePower">
+        update yt_dyz_user set power = power+1 where user_id = #{userId}
+    </update>
+    <update id="subOnePower">
+        update yt_dyz_user set power = power-1 where user_id = #{userId}
+    </update>
+    <update id="updateUser" parameterType="com.ytpm.app.model.YtNovelUser">
+        update yt_dyz_user
+        <set>
+            <if test="nickName != null">
+                nick_name = #{nickName},
+            </if>
+            <if test="phone != null">
+                phone = #{phone},
+            </if>
+            <if test="deviceId != null">
+                device_id = #{deviceId},
+            </if>
+            <if test="headImg != null">
+                head_img = #{headImg},
+            </if>
+            <if test="power != null">
+                power = #{power},
+            </if>
+            <if test="lastLoginTime != null">
+                last_login_time = #{lastLoginTime},
+            </if>
+            <if test="lastLoginIp != null">
+                last_login_ip = #{lastLoginIp},
+            </if>
+            <if test="loginDays != null">
+                login_days = #{loginDays},
+            </if>
+            <if test="totalVideo != null">
+                total_video = #{totalVideo},
+            </if>
+            <if test="totalIncome != null">
+                total_income = #{totalIncome},
+            </if>
+            <if test="nearlyIncome != null">
+                nearly_income = #{nearlyIncome},
+            </if>
+            <if test="redPacketBalance != null">
+                red_packet_balance = #{redPacketBalance},
+            </if>
+            <if test="redPacketAmount != null">
+                red_packet_amount = #{redPacketAmount},
+            </if>
+            <if test="pointsBalance != null">
+                points_balance = #{pointsBalance},
+            </if>
+            <if test="pointsTotal != null">
+                points_total = #{pointsTotal},
+            </if>
+            <if test="withdrawTotal != null">
+                withdraw_total = #{withdrawTotal},
+            </if>
+            <if test="signDays != null">
+                sign_days = #{signDays},
+            </if>
+            <if test="userStatus != null">
+                user_status = #{userStatus},
+            </if>
+            <if test="riskReason != null">
+                risk_reason = #{riskReason},
+            </if>
+            <if test="wxOpenId != null">
+                wx_open_id = #{wxOpenId},
+            </if>
+            <if test="phoneJson != null">
+                phone_json = #{phoneJson},
+            </if>
+            <if test="platformId != null">
+                platform_id = #{platformId}
+            </if>
+        </set>
+        where user_id = #{userId}
+    </update>
+    <delete id="delByAppId">
+        delete from yt_app_default_config where app_id = #{appId}
+    </delete>
+    <select id="getYtAppUser" resultType="com.ytpm.app.model.YtNovelUser">
+        select
+            user_id, app_id,phone,device_id, ditch_id, nick_name,head_img, power,
+            registry_time, last_login_time, last_login_ip, login_days, total_video, total_income, nearly_income,
+            red_packet_balance, red_packet_amount, points_balance, points_total, withdraw_total,
+            sign_days, user_status, risk_reason, wx_open_id, platform_id
+        from yt_dyz_user
+        where wx_open_id = #{openid}
+          and ditch_id = #{ditchId}
+    </select>
+    <select id="queryAll" resultType="com.ytpm.app.view.YtAppUserListView">
+        select
+        user_id,app_id,phone,device_id, ditch_id, nick_name,head_img, power,
+        registry_time, last_login_time, last_login_ip, login_days, total_video, total_income, nearly_income,
+        red_packet_balance, red_packet_amount, points_balance, points_total, withdraw_total,
+        sign_days, user_status, risk_reason, wx_open_id, platform_id
+        from yt_dyz_user
+        where 1 = 1
+        <if test="userId != null and userId !=''">
+            and user_id = #{userId}
+        </if>
+        <if test="nickName != null and nickName !=''">
+            and nick_name like concat('%',#{nickName},'%')
+        </if>
+        <if test="ditchId != null">
+            and ditch_id = #{ditchId}
+        </if>
+        <if test="userStatus != null">
+            and user_status = #{userStatus}
+        </if>
+        <if test="registryTimeBegin != null">
+            and DATE_FORMAT(registry_time, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{registryTimeBegin}, '%Y-%m-%d')
+        </if>
+        <if test="registryTimeEnd != null">
+            and DATE_FORMAT(registry_time, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{registryTimeEnd}, '%Y-%m-%d')
+        </if>
+        <if test="lastLoginTimeBegin != null">
+            and DATE_FORMAT(last_login_time, '%Y-%m-%d') <![CDATA[>=]]> DATE_FORMAT(#{lastLoginTimeBegin}, '%Y-%m-%d')
+        </if>
+        <if test="lastLoginTimeEnd != null">
+            and DATE_FORMAT(last_login_time, '%Y-%m-%d') <![CDATA[<=]]> DATE_FORMAT(#{lastLoginTimeEnd}, '%Y-%m-%d')
+        </if>
+        <if test="appIds != null and appIds != ''">
+            and app_id in
+            <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+                #{item}
+            </foreach>
+        </if>
+        order by user_id desc
+    </select>
+    <select id="selectPrimaryKey" resultType="com.ytpm.app.model.YtNovelUser">
+        select
+            user_id,app_id,phone,device_id, ditch_id, head_img, nick_name, registry_time, last_login_time, last_login_ip,
+            login_days, total_video, total_income, red_packet_balance, red_packet_amount, points_balance, points_total,
+            withdraw_total, sign_days, user_status, risk_reason, wx_open_id, platform_id, power, nearly_income
+        from yt_dyz_user
+        where user_id = #{userId}
+    </select>
+    <resultMap id="UserMap" type="com.ytpm.app.model.YtNovelUser">
+        <id column="user_id" property="userId" />
+        <result column="head_img" property="headImg" />
+        <result column="nick_name" property="nickName" />
+        <result column="phone" property="phone" />
+        <result column="device_id" property="deviceId" />
+        <result column="registry_time" property="registryTime" />
+        <result column="last_login_time" property="lastLoginTime" />
+        <result column="last_login_ip" property="lastLoginIp" />
+        <result column="login_days" property="loginDays" />
+        <result column="total_income" property="totalIncome" />
+        <result column="nearly_income" property="nearlyIncome" />
+        <result column="sign_days" property="signDays" />
+        <result column="user_status" property="userStatus" />
+        <result column="risk_reason" property="riskReason" />
+        <result column="wx_open_id" property="wxOpenId" />
+        <result column="ditch_id" property="ditchId" />
+        <result column="app_id" property="appId" />
+        <result column="platform_id" property="platformId" />
+        <collection property="loginRecordList" ofType="com.ytpm.app.model.YtDyzLoginRecord">
+            <result column="record_id" property="recordId" />
+            <result column="user_id" property="userId" />
+            <result column="login_time" property="loginTime" />
+            <result column="device_brand" property="deviceBrand" />
+            <result column="device_model" property="deviceModel" />
+            <result column="login_ip" property="loginIp" />
+            <result column="operator" property="operator" />
+            <result column="ip_addr" property="ipAddr" />
+        </collection>
+    </resultMap>
+    <select id="getUserList" resultMap="UserMap">
+        select
+        du.user_id,
+        du.head_img,
+        du.nick_name,
+        du.phone,
+        du.device_id,
+        du.registry_time,
+        du.last_login_time,
+        du.last_login_ip,
+        du.login_days,
+        du.total_video,
+        du.total_income,
+        du.nearly_income,
+        du.red_packet_balance,
+        du.red_packet_amount,
+        du.withdraw_total,
+        du.sign_days,
+        du.user_status,
+        du.risk_reason,
+        du.wx_open_id,
+        du.ditch_id,
+        du.app_id,
+        du.platform_id,
+        du.power,
+        lr.record_id,
+        lr.login_time,
+        lr.device_brand,
+        lr. device_model,
+        lr.login_ip,
+        lr.operator,
+        lr.ip_addr
+        from yt_dyz_user du
+        left join yt_dyz_login_record lr on du.user_id = lr.user_id
+        <where>
+            <if test="userIds != null and userIds != ''">
+                and du.user_id in
+                <foreach collection="userIds.split(',')" item="item" separator="," open="(" close=")">
+                    #{item}
+                </foreach>
+            </if>
+            <if test="loginTimeBegin != null">
+                and du.last_login_time <![CDATA[>=]]> #{loginTimeBegin}
+            </if>
+            <if test="loginTimeEnd != null">
+                and du.last_login_time <![CDATA[<=]]> #{loginTimeEnd}
+            </if>
+        </where>
+        group by du.user_id
+    </select>
+
+    <select id="queryAllByTime" resultType="com.ytpm.app.model.YtNovelUser">
+        SELECT
+        user_id,app_id,phone,device_id, nick_name, head_img, power,
+        registry_time,last_login_time, last_login_ip, login_days, total_video, total_income, nearly_income,
+        red_packet_balance, red_packet_amount, points_balance, points_total, withdraw_total,
+        sign_days, user_status, risk_reason, wx_open_id, platform_id
+        FROM yt_dyz_user
+        <where>
+            <if test="startTime != null">
+                AND registry_time >= #{startTime}
+            </if>
+            <if test="endTime != null">
+                AND registry_time &lt;= #{endTime}
+            </if>
+            <choose>
+                <when test="appIdList == null or appIdList.isEmpty()">
+                    AND 1 = 0
+                </when>
+                <otherwise>
+                    AND app_id IN
+                    <foreach collection="appIdList" item="item"
+                             open="(" close=")" separator=",">
+                        #{item}
+                    </foreach>
+                </otherwise>
+            </choose>
+        </where>
+        ORDER BY registry_time DESC
+    </select>
+    <select id="countUsersByHour" resultType="com.ytpm.app.view.HourCountView">
+        SELECT HOUR(registry_time) AS hour, COUNT(*) AS count
+        FROM yt_dyz_user
+        WHERE registry_time BETWEEN #{startTime} AND #{endTime}
+        <choose>
+            <when test="appIdList == null or appIdList.isEmpty()">
+                AND 1 = 0
+            </when>
+            <otherwise>
+                AND app_id IN
+                <foreach collection="appIdList" item="item"
+                         open="(" close=")" separator=",">
+                    #{item}
+                </foreach>
+            </otherwise>
+        </choose>
+        GROUP BY HOUR(registry_time)
+    </select>
+
+    <select id="queryTodayBanned" resultType="com.ytpm.app.model.YtNovelUser">
+        select user_id from yt_dyz_user
+        where registry_time BETWEEN #{startTime} AND #{endTime}
+        and user_id in
+        <foreach collection="userIdList" item="item"
+                 open="(" close=")" separator=",">
+            #{item}
+        </foreach>
+    </select>
+    <select id="getDefaultConfig" resultType="com.ytpm.app.view.WxDefaultConfig">
+        select
+            config_id, config_name, open_id as app_id, secret, app_id as platformAppId, app_key as platformAppSecret,
+            app_type,user_path, login_path,ad_path,answer_path,power_path,can_simulator,
+            taku_app_id, taku_key, taku_banner_pid, taku_native_pid, taku_reward_pid, taku_interstitial_pid,
+            can_use_root, can_use_adb, can_use_float, can_accumulation,
+            ditch_id, power_wait_time, interstitial_interval_time,
+            low_value_tip, brush_tip,flow_interval_time,task_limit_tip,start_wait_time,
+            can_cache_video,can_allow_auto_refresh
+        from yt_app_default_config
+        where app_type = #{appType}
+    </select>
+    <select id="getDefaultConfigByAppId" resultType="com.ytpm.app.view.WxDefaultConfig">
+        select
+            config_id, config_name, open_id, secret, app_id, app_key, app_type,user_path,login_path,ad_path,
+            answer_path,power_path,can_simulator, taku_app_id, taku_key, taku_banner_pid, taku_native_pid,
+            taku_reward_pid, taku_interstitial_pid, can_use_root, can_use_adb, can_use_float, can_accumulation,
+            ditch_id, power_wait_time, interstitial_interval_time,
+            low_value_tip, brush_tip,flow_interval_time,task_limit_tip,start_wait_time,
+            can_cache_video,can_allow_auto_refresh
+        from yt_app_default_config
+        where app_id = #{appId}
+    </select>
+    <select id="getSecretByAppId" resultType="java.lang.String">
+        select
+            config_id, config_name, open_id, secret, app_id, app_key, app_type,user_path,login_path,ad_path,
+            answer_path,power_path,can_simulator, taku_app_id, taku_key, taku_banner_pid, taku_native_pid,
+            taku_reward_pid, taku_interstitial_pid, can_use_root, can_use_adb, can_use_float, can_accumulation,
+            ditch_id, power_wait_time, interstitial_interval_time,
+            low_value_tip, brush_tip, flow_interval_time,task_limit_tip,start_wait_time,
+            can_cache_video,can_allow_auto_refresh
+        from yt_app_default_config
+        where app_id = #{appId}
+    </select>
+    <select id="queryByOpenid" resultType="com.ytpm.app.model.YtNovelUser">
+        select
+            user_id,phone,device_id, head_img, nick_name, registry_time, last_login_time, last_login_ip, login_days,
+            total_video, total_income, nearly_income, red_packet_balance, red_packet_amount, points_balance, points_total,
+            withdraw_total, sign_days, user_status, risk_reason, wx_open_id, ditch_id, app_id, platform_id, power
+        from yt_dyz_user
+        where wx_open_id = #{openid}
+    </select>
+    <select id="getByDeviceId" resultType="java.lang.String">
+        select platform_id
+        from yt_dyz_user
+        where device_id = #{deviceId} and wx_open_id = #{openid}
+        limit 1
+    </select>
+    <select id="getConfigByIds" resultType="com.ytpm.app.view.WxDefaultConfig">
+        select
+        config_id, config_name, open_id, secret, app_id, app_key, app_type, user_path, login_path, ad_path,
+        answer_path, power_path,taku_app_id, taku_key, taku_banner_pid, taku_native_pid, taku_reward_pid,
+        taku_interstitial_pid, can_use_root, can_use_adb, can_use_float, can_accumulation,can_simulator,
+        ditch_id, power_wait_time, interstitial_interval_time,
+        low_value_tip, brush_tip, flow_interval_time,task_limit_tip,start_wait_time,can_cache_video,can_allow_auto_refresh
+        from yt_app_default_config
+        where app_id in
+        <foreach collection="appIds.split(',')" item="item" separator="," open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <select id="getAdCount" resultType="java.lang.Integer">
+        select sum(total_video)
+        from yt_dyz_user
+        where app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <select id="getRevenueCount" resultType="java.math.BigDecimal">
+        select sum(total_income)
+        from yt_dyz_user
+        where app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <select id="getAppRanking" resultType="com.ytpm.middle.view.AppRankingListVO">
+        SELECT
+        u.app_id,
+        sum( ar.revenue ) totalRevenue
+        FROM
+        yt_dyz_ad_record ar
+        JOIN yt_dyz_user u ON ar.user_id = u.user_id
+        <where>
+            <if test="sortBy != null">
+                <choose>
+                    <when test="sortBy == 1">
+                        DATE_FORMAT( ar.finish_time, '%Y-%m-%d' ) = CURRENT_DATE()
+                    </when>
+                    <when test=" sortBy == 2">
+                        YEARWEEK( ar.finish_time, 1 ) = YEARWEEK(now(),1)
+                    </when>
+                    <otherwise>
+                        DATE_FORMAT( ar.finish_time, '%Y-%m' ) = DATE_FORMAT(NOW(), '%Y-%m')
+                    </otherwise>
+                </choose>
+            </if>
+        </where>
+        GROUP BY
+        u.app_id
+        ORDER BY
+        sum( ar.revenue ) DESC
+        LIMIT #{limit}
+    </select>
+    <select id="getUserRanking" resultType="com.ytpm.middle.view.UserRankingListVO">
+        SELECT
+        u.user_id,
+        u.nick_name,
+        u.head_img,
+        u.total_video,
+        sum( ar.revenue ) totalIncome
+        FROM
+        yt_dyz_ad_record ar
+        JOIN yt_dyz_user u ON ar.user_id = u.user_id
+        <where>
+            <if test="sortBy != null">
+                <choose>
+                    <when test="sortBy == 1">
+                        DATE_FORMAT( ar.finish_time, '%Y-%m-%d' ) = CURRENT_DATE()
+                    </when>
+                    <when test=" sortBy == 2">
+                        YEARWEEK( ar.finish_time, 1 ) = YEARWEEK(now(),1)
+                    </when>
+                    <otherwise>
+                        DATE_FORMAT( ar.finish_time, '%Y-%m' ) = DATE_FORMAT(NOW(), '%Y-%m')
+                    </otherwise>
+                </choose>
+            </if>
+        </where>
+        GROUP BY ar.user_id
+        ORDER BY
+        sum( ar.revenue ) DESC
+        limit #{limit}
+    </select>
+    <select id="countUserByAppIds" resultType="java.lang.Integer">
+        select
+        count(user_id)
+        from yt_dyz_user
+        where app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <select id="countRevenueByAppIds" resultType="java.math.BigDecimal">
+        select
+        sum(total_income)
+        from yt_dyz_user
+        where app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <select id="countBannedByAppIds" resultType="java.lang.Integer">
+        select
+        count(user_id)
+        from yt_dyz_user
+        where user_status > 1 and app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <select id="countRegistryUser" resultType="java.lang.Integer">
+        select
+        count(user_id)
+        from yt_dyz_user
+        where app_id = #{appId}
+        <if test="type != null and type ==1">
+            and DATE(registry_time) = DATE(now())
+        </if>
+        <if test="type != null and type ==2">
+            and DATE(registry_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+        </if>
+        <if test="type != null and type ==3">
+            and DATE_FORMAT(registry_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+        </if>
+    </select>
+    <select id="countLoginUser" resultType="java.lang.Integer">
+        select
+        count(user_id)
+        from yt_dyz_user
+        where app_id = #{appId}
+        <if test="type != null and type ==1">
+            and DATE(last_login_time) = DATE(now())
+        </if>
+        <if test="type != null and type ==2">
+            and DATE(last_login_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+        </if>
+        <if test="type != null and type ==3">
+            and DATE_FORMAT(last_login_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+        </if>
+    </select>
+    <select id="countRegistryHour" resultType="com.ytpm.middle.view.AppUserHourVO">
+        select
+        DATE_FORMAT(registry_time,'%Y-%m-%d %H:00:00') as `time`,
+        count(user_id) `count`
+        from yt_dyz_user
+        where app_id = #{appId}
+        <if test="type != null and type ==1">
+            and DATE(registry_time) = DATE(now())
+        </if>
+        <if test="type != null and type ==2">
+            and DATE(registry_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+        </if>
+        <if test="type != null and type ==3">
+            and DATE_FORMAT(registry_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+        </if>
+        group by `time`
+    </select>
+    <select id="countLoginHour" resultType="com.ytpm.middle.view.AppUserHourVO">
+        select
+        DATE_FORMAT(last_login_time,'%Y-%m-%d %H:00:00') as `time`,
+        count(user_id) `count`
+        from yt_dyz_user
+        where app_id = #{appId}
+        <if test="type != null and type ==1">
+            and DATE(last_login_time) = DATE(now())
+        </if>
+        <if test="type != null and type ==2">
+            and DATE(last_login_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)
+        </if>
+        <if test="type != null and type ==3">
+            and DATE_FORMAT(last_login_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
+        </if>
+        group by `time`
+    </select>
+    <select id="queryByUserIds" resultType="com.ytpm.app.model.YtNovelUser">
+        select
+        user_id, head_img, nick_name, registry_time, last_login_time, last_login_ip, login_days, total_video, total_income, red_packet_balance, red_packet_amount, points_balance, points_total, withdraw_total, sign_days, user_status, risk_reason, wx_open_id, ditch_id, app_id, platform_id, power, phone, device_id
+        from yt_dyz_user
+        where user_id in
+        <foreach collection="userIds.split(',')" separator="," item="item"  open="(" close=")">
+            #{item}
+        </foreach>
+    </select>
+    <resultMap id="UserAdMap" type="com.ytpm.agent.view.AgentAuditCheckVO">
+        <id column="user_id" property="userId" />
+        <result column="app_id" property="appId" />
+        <result column="nick_name" property="nickName" />
+        <collection property="adRecordList" ofType="com.ytpm.app.model.YtDyzAdRecord">
+            <result column="record_id" property="recordId" />
+            <result column="placement_id" property="placementId" />
+            <result column="ad_source_id" property="adSourceId" />
+            <result column="ad_source_type" property="adSourceType" />
+            <result column="ad_source_index" property="adSourceIndex" />
+            <result column="revenue" property="revenue" />
+            <result column="ecpm" property="ecpm" />
+            <result column="network_form_id" property="networkFormId" />
+            <result column="network_name" property="networkName" />
+            <result column="network_placement_id" property="networkPlacementId" />
+            <result column="finish_time" property="finishTime" />
+            <result column="begin_time" property="beginTime" />
+        </collection>
+    </resultMap>
+    <select id="queryTodayUserAd" resultMap="UserAdMap">
+        select
+        u.user_id,
+        u.app_id,
+        u.nick_name,
+        ar.record_id,
+        ar.placement_id,
+        ar.ad_source_id,
+        ar.ad_source_type,
+        ar.ad_source_index,
+        ar.revenue,
+        ar.ecpm,
+        ar.network_form_id,
+        ar.network_name,
+        ar.network_placement_id,
+        ar.finish_time,
+        ar.begin_time
+        from yt_dyz_ad_record ar
+        join yt_dyz_user u on ar.user_id = u.user_id
+        where ar.ad_source_type = #{adSourceType} and ar.app_id = #{appId} and ar.user_id in
+        <foreach collection="userIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+        and DATE_FORMAT(ar.finish_time, '%Y-%m-%d') = current_date()
+    </select>
+    <select id="getMonthRegistryUser" resultType="com.ytpm.app.model.YtNovelUser">
+        select
+        user_id, head_img, nick_name, registry_time, last_login_time, last_login_ip, login_days,
+        total_video, total_income, nearly_income, red_packet_balance, red_packet_amount, points_balance, points_total,
+        withdraw_total, sign_days, user_status, risk_reason, wx_open_id, ditch_id, app_id, platform_id,
+        power, phone, device_id
+        from yt_dyz_user
+        where app_id in
+        <foreach collection="appIds.split(',')" separator="," item="item" open="(" close=")">
+            #{item}
+        </foreach>
+        <if test="type != null and type == 1">
+            and DATE_FORMAT(registry_time, '%Y-%m') = DATE_FORMAT(now(),'%Y-%m')
+        </if>
+        <if test="type != null and type == 2">
+            and DATE_FORMAT(last_login_time, '%Y-%m') = DATE_FORMAT(now(),'%Y-%m')
+        </if>
+    </select>
+    <select id="countDitch" resultType="java.lang.Integer">
+        select
+            count( distinct ditch_id)
+        from yt_dyz_user
+        where device_id = #{deviceId}
+          and registry_time > (NOW() - INTERVAL #{hours} HOUR )
+    </select>
+    <select id="countLoginDitch" resultType="java.lang.Integer">
+        select
+            count( distinct ditch_id)
+        from yt_dyz_user
+        where device_id = #{deviceId}
+          and last_login_time > (NOW() - INTERVAL #{hours} HOUR )
+    </select>
+    <select id="getLastRegistryUser" resultType="com.ytpm.app.model.YtNovelUser">
+        select
+            user_id, head_img, nick_name, registry_time, last_login_time, last_login_ip, login_days,
+            total_video, total_income, nearly_income, red_packet_balance, red_packet_amount, points_balance, points_total,
+            withdraw_total, sign_days, user_status, risk_reason, wx_open_id, ditch_id, app_id, platform_id,
+            power, phone, device_id, phone_json
+        from yt_dyz_user
+        where device_id = #{deviceId}
+    </select>
+
+    <select id="getByDeviceAndDitch" resultType="com.ytpm.app.model.YtNovelUser">
+        select
+            user_id, head_img, nick_name, registry_time, last_login_time, last_login_ip, login_days,
+            total_video, total_income, nearly_income, red_packet_balance, red_packet_amount, points_balance, points_total,
+            withdraw_total, sign_days, user_status, risk_reason, wx_open_id, ditch_id, app_id, platform_id,
+            power, phone, device_id, phone_json
+        from yt_dyz_user
+        where device_id = #{deviceId} and ditch_id = #{ditchId} limit 1
+    </select>
+    <select id="getPlatformByDeviceId" resultType="java.lang.String">
+        select platform_id
+        from yt_dyz_user
+        where device_id = #{deviceId}
+        limit 1
+    </select>
+    <update id="unlockUser">
+        update yt_dyz_user
+        set user_status = 1
+        where user_status > 1
+        and user_id in
+        <foreach collection="userIds.split(',')" item="item" separator="," open="(" close=")">
+            #{item}
+        </foreach>
+    </update>
+    <update id="updateTotal">
+        UPDATE yt_dyz_user
+        SET
+        -- 处理近4天未登录 或间隔时间超过4天
+        nearly_income = (CASE
+        WHEN last_login_time <![CDATA[ < ]]> DATE_SUB(DATE(NOW()), INTERVAL 4 DAY)
+            OR IFNULL(nearly_begin_time, NOW()) <![CDATA[ < ]]> DATE_SUB(DATE(NOW()), INTERVAL 4 DAY)
+        THEN #{revenue}
+        ELSE COALESCE(nearly_income, 0) + #{revenue}
+        END),
+        nearly_begin_time = CASE
+        -- 优先处理重置场景
+        WHEN last_login_time <![CDATA[ < ]]> DATE_SUB(DATE(NOW()), INTERVAL 4 DAY)
+            OR IFNULL(nearly_begin_time, NOW()) <![CDATA[ < ]]> DATE_SUB(DATE(NOW()), INTERVAL 4 DAY)
+        THEN NOW()
+        -- 处理初始值为NULL的情况
+        WHEN nearly_begin_time IS NULL THEN NOW()
+        -- 其他情况保持不变
+        ELSE nearly_begin_time
+        END,
+        -- 累加统计值
+        total_video = COALESCE(total_video, 0) + #{videoCount},
+        total_income = COALESCE(total_income, 0) + #{revenue}
+        WHERE user_id = #{userId};
+    </update>
+</mapper>

+ 90 - 0
yt-novel/yt-novel-service/src/main/resources/mapper/LoginRecordMapper.xml

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ytpm.novel.dao.LoginRecordMapper">
+
+    <insert id="insertOne" parameterType="com.ytpm.app.model.YtDyzLoginRecord">
+        insert into yt_dyz_login_record
+        (record_id,
+         user_id,
+         login_time,
+         device_brand,
+         device_model,
+         login_ip,
+         operator,
+         ip_addr,
+         phone_json,
+         login_type)
+        values (#{recordId},
+                #{userId},
+                #{loginTime},
+                #{deviceBrand},
+                #{deviceModel},
+                #{loginIp},
+                #{operator},
+                #{ipAddr},
+                #{phoneJson},
+                #{loginType});
+    </insert>
+    <select id="getLoginRecords" resultType="com.ytpm.app.model.YtDyzLoginRecord">
+        select
+            record_id, user_id, login_time, device_brand, device_model, login_ip, operator, ip_addr
+        from yt_dyz_login_record
+        where user_id = #{userId}
+    </select>
+
+    <select id="queryLoginCount" resultType="String">
+        select ydlr.user_id
+        from yt_dyz_login_record ydlr
+        left join yt_dyz_user ydu on ydlr.user_id=ydu.user_id
+        <where>
+            <if test="startTime != null">
+                AND ydlr.login_time >= #{startTime}
+            </if>
+            <if test="endTime != null">
+                AND ydlr.login_time &lt;= #{endTime}
+            </if>
+            <choose>
+                <when test="appIdList == null or appIdList.isEmpty()">
+                    AND 1 = 0
+                </when>
+                <otherwise>
+                    AND ydu.app_id IN
+                    <foreach collection="appIdList" item="item"
+                             open="(" close=")" separator=",">
+                        #{item}
+                    </foreach>
+                </otherwise>
+            </choose>
+        </where>
+        group by user_id
+    </select>
+    <select id="getLoginRecordByIds" resultType="com.ytpm.app.model.YtDyzLoginRecord">
+        select
+            record_id, user_id, login_time, device_brand, device_model, login_ip, operator, ip_addr
+        from yt_dyz_login_record
+        where user_id in
+        <foreach collection="userIds.split(',')" open="(" close=")" separator="," item="item">
+            #{item}
+        </foreach>
+    </select>
+
+    <select id="getTodayLoginCount" resultType="java.lang.Integer">
+        select count(DISTINCT DATE( login_time ))
+        from yt_dyz_login_record
+        where user_id = #{userId} and DATE(login_time) = current_date()
+    </select>
+    <select id="getLastLoginRecord" resultType="com.ytpm.app.model.YtDyzLoginRecord">
+        select
+            record_id, user_id, login_time, device_brand, device_model, login_ip, operator, ip_addr
+        from yt_dyz_login_record
+        where user_id = #{userId}
+        <if test="isVisitor != null and isVisitor == 1">
+            and login_type = 'VISITOR'
+        </if>
+        <if test="isVisitor != null and isVisitor == 0">
+            and (login_type is null or login_type = '')
+        </if>
+        order by login_time desc
+        limit 1
+    </select>
+</mapper>

+ 119 - 0
yt-novel/yt-novel-service/src/main/resources/mapper/NovelMapper.xml

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ytpm.novel.dao.NovelMapper">
+    <insert id="insertReadRecord">
+        insert into yt_novel_read_record
+        (record_id,
+         user_id,
+         novel_id,
+         chapter_id,
+         novel_name,
+         novel_chapter_name,
+         read_time,
+         create_by,
+         create_time,
+         update_by,
+         update_time)
+        values (#{recordId},
+                #{userId},
+                #{novelId},
+                #{chapterId},
+                #{novelName},
+                #{novelChapterName},
+                #{readTime},
+                #{createBy},
+                #{createTime},
+                #{updateBy},
+                #{updateTime});
+    </insert>
+    <insert id="insertNovelShelf">
+        insert into yt_novel_shelf(shelf_id, user_id, shelf_name, novel_ids, create_by, create_time)
+        values (#{shelfId}, #{userId},#{shelfName}, #{novelIds}, #{createBy}, #{createTime});
+    </insert>
+    <update id="updateNovelShelf">
+        update yt_novel_shelf set
+        <if test="shelfName != null and shelfName != ''">
+            shelf_name=#{shelfName},
+        </if>
+        <if test="novelIds != null and novelIds != ''">
+            novel_ids=#{novelIds},
+        </if>
+        update_by = #{updateBy},
+        update_time = #{updateTime}
+        where user_id =#{userId} and deleted =0;
+    </update>
+
+
+    <select id="selectNovelList" resultType="com.ytpm.novel.model.dto.YtNovelDto">
+        select n.novel_id, novel_name, novel_author, novel_introduction, novel_words, novel_reads, novel_img, novel_url,
+        book_id, status, state, deleted, create_by, create_time, update_by, update_time, revision
+        from yt_novel n
+        where n.deleted = 0
+        <if test="param.novelName != null and param.novelName != ''">
+            and n.novel_name like CONCAT('%', #{param.novelName}, '%')
+        </if>
+    </select>
+    <select id="selectByNovelId" resultType="com.ytpm.novel.model.dto.YtNovelDto">
+        select n.novel_id, novel_name, novel_author, novel_introduction, novel_words, novel_reads, novel_img, novel_url,
+               book_id, status, state, deleted, create_by, create_time, update_by, update_time, revision
+        from yt_novel n
+        where n.deleted = 0 and n.novel_id=#{novelId}
+    </select>
+    <select id="selectNovelChapter" resultType="com.ytpm.app.model.YtNovelChapter">
+        select nc.chapter_id,
+               novel_id,
+               chapter,
+               chapter_name,
+               content,
+               content_text,
+               deleted,
+               revision,
+               create_by,
+               create_time,
+               update_by,
+               update_time
+        from yt_novel_chapter nc
+        where nc.deleted=0 and nc.novel_id=#{novelId}
+        order by chapter
+    </select>
+    <select id="selectShelf" resultType="com.ytpm.app.model.YtNovelShelf">
+        select yns.shelf_id,
+               user_id,
+               novel_ids,
+               revision,
+               deleted,
+               create_by,
+               create_time,
+               update_by,
+               update_time
+        from yt_novel_shelf yns
+        where yns.deleted = 0 and yns.user_id = #{userId}
+    </select>
+    <select id="selectLastRecord" resultType="com.ytpm.app.model.YtNovelReadRecord">
+        select ynrr.record_id,
+               user_id,
+               novel_id,
+               chapter_id,
+               novel_name,
+               novel_chapter_name,
+               deleted,
+               read_time,
+               create_by,
+               create_time,
+               update_by,
+               update_time
+        from yt_novel_read_record ynrr
+        where user_id=#{userId} and novel_id=#{novelId} and deleted = 0
+        order by read_time desc
+        limit 1
+    </select>
+    <select id="selectByNovelIds" resultType="com.ytpm.app.model.YtNovel">
+        select n.novel_id, novel_name, novel_author, novel_introduction, novel_words, novel_reads, novel_img, novel_url,
+               book_id, status, state, deleted, create_by, create_time, update_by, update_time, revision
+        from yt_novel n
+        where n.deleted = 0
+        <foreach collection="novelIds" item="id" open="and n.novel_id in (" separator="," close=")">#{id}</foreach>
+
+    </select>
+
+</mapper>