diff --git a/sql/ddl.sql b/sql/ddl.sql index 4d64dd9..91071d1 100644 --- a/sql/ddl.sql +++ b/sql/ddl.sql @@ -183,6 +183,7 @@ create table if not exists user userPassword varchar(512) not null comment '密码', isVip int default 0 not null comment '是否会员:0-非会员,1-会员', vipExpire datetime null comment '会员到期时间', +# vipNum int not null comment '会员编号', `status` tinyint DEFAULT '0' COMMENT '状态0正常1不正常', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', @@ -209,7 +210,7 @@ CREATE TABLE IF NOT EXISTS `predict_record` ( `predictTime` datetime default CURRENT_TIMESTAMP not null comment '预测时间', `bonus` BIGINT default 0 NOT NULL COMMENT '奖金' ) ENGINE = InnoDB - DEFAULT CHARSET = utf8mb4 COMMENT = '彩票开奖信息表'; + DEFAULT CHARSET = utf8mb4 COMMENT = '双色球推测记录表'; CREATE TABLE IF NOT EXISTS `vip_code` ( `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, @@ -225,8 +226,6 @@ CREATE TABLE IF NOT EXISTS `vip_code` ( `updateTime` datetime NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员码表'; - - CREATE TABLE IF NOT EXISTS `vip_exchange_record` ( `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, `userId` bigint NOT NULL COMMENT '用户ID', @@ -264,5 +263,178 @@ CREATE TABLE IF NOT EXISTS `chat_message` ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT ='聊天消息表'; +CREATE TABLE IF NOT EXISTS `dlt_draw_record` ( + `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + `drawId` VARCHAR(50) NOT NULL COMMENT '开奖期号', + `drawDate` DATE NOT NULL COMMENT '开奖日期', + `frontBall1` INT NOT NULL COMMENT '前区1', + `frontBall2` INT NOT NULL COMMENT '前区2', + `frontBall3` INT NOT NULL COMMENT '前区3', + `frontBall4` INT NOT NULL COMMENT '前区4', + `frontBall5` INT NOT NULL COMMENT '前区5', + `backBall1` INT NOT NULL COMMENT '后区1', + `backBall2` INT NOT NULL COMMENT '后区2' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透开奖信息表'; + +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_all` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + frequencyPercentage FLOAT NULL COMMENT '出现频率%', + averageHiddenAppear INT NULL COMMENT '平均隐现期(次)', + maxHiddenInterval INT NULL COMMENT '最长隐现期(次)', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区全部历史数据表'; + +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + averageHiddenAppear FLOAT NULL COMMENT '平均隐现期(次)', + currentHiddenInterval INT NULL COMMENT '当前隐现期', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区最近100期数据表'; + +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_top` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区历史数据排行表'; + +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_top_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区百期数据排行表'; + +CREATE TABLE IF NOT EXISTS `dlt_backend_history_all` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + frequencyPercentage FLOAT NULL COMMENT '出现频率%', + averageHiddenAppear INT NULL COMMENT '平均隐现期(次)', + maxHiddenInterval INT NULL COMMENT '最长隐现期(次)', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区全部历史数据表'; + +CREATE TABLE IF NOT EXISTS `dlt_backend_history_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + averageHiddenAppear FLOAT NULL COMMENT '平均隐现期(次)', + currentHiddenInterval INT NULL COMMENT '当前隐现期', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区最近100期数据表'; + +CREATE TABLE IF NOT EXISTS `dlt_backend_history_top` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区历史数据排行表'; + +CREATE TABLE IF NOT EXISTS `dlt_backend_history_top_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区百期数据排行表'; +CREATE TABLE IF NOT EXISTS `d5` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd5表'; + +CREATE TABLE IF NOT EXISTS `d6` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd6表'; + +CREATE TABLE IF NOT EXISTS `d7` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd7表'; + +CREATE TABLE IF NOT EXISTS `d8` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd8表'; + +CREATE TABLE IF NOT EXISTS `d9` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd9表'; + +CREATE TABLE IF NOT EXISTS `d10` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd10表'; +CREATE TABLE IF NOT EXISTS `d11` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd11表'; + +CREATE TABLE IF NOT EXISTS `d12` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + masterBallNumber INT NULL COMMENT '主球', + slaveBallNumber INT NULL COMMENT '从球', + coefficient FLOAT NULL COMMENT '系数' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = 'd12表'; + + +CREATE TABLE IF NOT EXISTS `dlt_predict_record` ( + `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + `userId` BIGINT NOT NULL COMMENT '用户ID', + `drawId` BIGINT NOT NULL COMMENT '开奖期号' , + `drawDate` DATE NOT NULL COMMENT '开奖日期', + `frontendBall1` INT NOT NULL COMMENT '前区1', + `frontendBall2` INT NOT NULL COMMENT '前区2', + `frontendBall3` INT NOT NULL COMMENT '前区3', + `frontendBall4` INT NOT NULL COMMENT '前区4', + `frontendBall5` INT NOT NULL COMMENT '前区5', + `backendBall1` INT NOT NULL COMMENT '后区1', + `backendBall2` INT NOT NULL COMMENT '后区2', + `predictStatus` VARCHAR(100) default '待开奖' NOT NULL COMMENT '预测状态(待开奖/已开奖)', + `predictResult` VARCHAR(100) default '待开奖' NOT NULL COMMENT '预测结果(未中奖/三等奖/二等奖/一等奖)', + `predictTime` datetime default CURRENT_TIMESTAMP not null comment '预测时间', + `bonus` BIGINT default 0 NOT NULL COMMENT '奖金' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透推测记录表'; diff --git a/src/main/java/com/xy/xyaicpzs/common/ResultUtils.java b/src/main/java/com/xy/xyaicpzs/common/ResultUtils.java index 5c8e1a7..437e214 100644 --- a/src/main/java/com/xy/xyaicpzs/common/ResultUtils.java +++ b/src/main/java/com/xy/xyaicpzs/common/ResultUtils.java @@ -25,7 +25,7 @@ public class ResultUtils { * @return */ public static ApiResponse error(ErrorCode errorCode) { - return ApiResponse.error(errorCode.getMessage()); + return ApiResponse.error(errorCode.getCode(), errorCode.getMessage()); } /** @@ -36,7 +36,7 @@ public class ResultUtils { * @return */ public static ApiResponse error(int code, String message) { - return ApiResponse.error(message); + return ApiResponse.error(code, message); } /** @@ -46,6 +46,6 @@ public class ResultUtils { * @return */ public static ApiResponse error(ErrorCode errorCode, String message) { - return ApiResponse.error(message); + return ApiResponse.error(errorCode.getCode(), message); } } \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/BackBallPredictionRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/BackBallPredictionRequest.java new file mode 100644 index 0000000..beb46f7 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/BackBallPredictionRequest.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; +import java.util.List; + +@Data +public class BackBallPredictionRequest { + private String level; + private List nextFrontBalls; + private List previousFrontBalls; + private List previousBackBalls; + private List nextBackBalls; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/DltPredictRecordQueryRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/DltPredictRecordQueryRequest.java new file mode 100644 index 0000000..2eaf0db --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/DltPredictRecordQueryRequest.java @@ -0,0 +1,31 @@ +package com.xy.xyaicpzs.common.requset; + +import com.xy.xyaicpzs.common.PageRequest; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 大乐透预测记录查询请求 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class DltPredictRecordQueryRequest extends PageRequest { + + /** + * 用户ID + */ + private Long userId; + + /** + * 预测状态(待开奖/已开奖) + */ + private String predictStatus; + + /** + * 预测结果(未中奖/三等奖/二等奖/一等奖) + */ + private String predictResult; +} + + + diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/FirstBallPredictionRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/FirstBallPredictionRequest.java new file mode 100644 index 0000000..5eeb51e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/FirstBallPredictionRequest.java @@ -0,0 +1,11 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; +import java.util.List; + +@Data +public class FirstBallPredictionRequest { + private String level; + private List redBalls; + private List blueBalls; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/FollowBackBallPredictionRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/FollowBackBallPredictionRequest.java new file mode 100644 index 0000000..ab55bb0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/FollowBackBallPredictionRequest.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; +import java.util.List; + +@Data +public class FollowBackBallPredictionRequest { + private String level; + private Integer backFirstBall; + private List nextFrontBalls; + private List previousFrontBalls; + private List previousBackBalls; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/requset/FollowerBallPredictionRequest.java b/src/main/java/com/xy/xyaicpzs/common/requset/FollowerBallPredictionRequest.java new file mode 100644 index 0000000..8e50a69 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/common/requset/FollowerBallPredictionRequest.java @@ -0,0 +1,12 @@ +package com.xy.xyaicpzs.common.requset; + +import lombok.Data; +import java.util.List; + +@Data +public class FollowerBallPredictionRequest { + private String level; + private List wellRegardedBalls; + private List previousFrontBalls; + private List previousBackBalls; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/common/response/ApiResponse.java b/src/main/java/com/xy/xyaicpzs/common/response/ApiResponse.java index b09c6bf..cb4345e 100644 --- a/src/main/java/com/xy/xyaicpzs/common/response/ApiResponse.java +++ b/src/main/java/com/xy/xyaicpzs/common/response/ApiResponse.java @@ -7,12 +7,14 @@ import lombok.Data; */ @Data public class ApiResponse { + private Integer code; private boolean success; private String message; private T data; public static ApiResponse success(T data) { ApiResponse response = new ApiResponse<>(); + response.code = 0; response.success = true; response.message = "操作成功"; response.data = data; @@ -21,6 +23,15 @@ public class ApiResponse { public static ApiResponse error(String message) { ApiResponse response = new ApiResponse<>(); + response.code = 1; + response.success = false; + response.message = message; + return response; + } + + public static ApiResponse error(Integer code, String message) { + ApiResponse response = new ApiResponse<>(); + response.code = code; response.success = false; response.message = message; return response; diff --git a/src/main/java/com/xy/xyaicpzs/controller/BallAnalysisController.java b/src/main/java/com/xy/xyaicpzs/controller/BallAnalysisController.java index 996bbe1..605011b 100644 --- a/src/main/java/com/xy/xyaicpzs/controller/BallAnalysisController.java +++ b/src/main/java/com/xy/xyaicpzs/controller/BallAnalysisController.java @@ -22,7 +22,6 @@ import com.xy.xyaicpzs.domain.entity.T3; import com.xy.xyaicpzs.domain.entity.T4; import com.xy.xyaicpzs.domain.entity.T5; import com.xy.xyaicpzs.domain.entity.T6; -import com.xy.xyaicpzs.exception.BusinessException; import com.xy.xyaicpzs.service.BallAnalysisService; import com.xy.xyaicpzs.service.LotteryDrawsService; import com.xy.xyaicpzs.service.PredictRecordService; @@ -47,6 +46,9 @@ import com.xy.xyaicpzs.domain.vo.PrizeEstimateVO; import com.xy.xyaicpzs.domain.vo.BallCombinationAnalysisVO; import com.xy.xyaicpzs.domain.vo.BallPersistenceAnalysisVO; import com.xy.xyaicpzs.domain.vo.RedBallHitRateVO; +import com.xy.xyaicpzs.domain.vo.BallAnalysisResultVO; +import com.xy.xyaicpzs.domain.vo.FollowBallAnalysisResultVO; +import com.xy.xyaicpzs.domain.vo.BlueBallAnalysisResultVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -407,8 +409,8 @@ public class BallAnalysisController { * @return 分析结果:出现频率最高的前11位数字 */ @PostMapping("/analyze") - @Operation(summary = "首球算法", description = "根据输入的级别、红球和蓝球,分析出现频率最高的前11位数字") - public ApiResponse> analyzeBalls( + @Operation(summary = "首球算法", description = "根据输入的级别、红球和蓝球,分析出现频率最高的前11位数字及筛选过程说明") + public ApiResponse analyzeBalls( @Parameter(description = "用户ID,例如:1001", required = true) @RequestParam Long userId, @@ -438,10 +440,10 @@ public class BallAnalysisController { // 解析红球号码 List redBallList = parseRedBalls(redBalls, 6, "红球"); - // 调用分析服务 - List result = ballAnalysisService.analyzeBalls(level, redBallList, blueBall); + // 调用分析服务(包含筛选过程说明) + BallAnalysisResultVO result = ballAnalysisService.analyzeBallsWithProcess(level, redBallList, blueBall); - log.info("球号分析完成,结果:{}", result); + log.info("球号分析完成,结果:{},过程说明:{}", result.getResult(), result.getFilteringProcess()); return ResultUtils.success(result); } catch (Exception e) { @@ -457,11 +459,11 @@ public class BallAnalysisController { * @param firstThreeRedBalls 前3个红球号码,用逗号分隔 * @param lastSixRedBalls 后6个红球号码,用逗号分隔 * @param blueBall 蓝球号码 - * @return 分析结果:出现频率最高的前8位数字 + * @return 分析结果:出现频率最高的前8位数字及筛选过程说明 */ @PostMapping("/fallow") - @Operation(summary = "跟随球号分析算法", description = "根据输入的级别、前3个红球、后6个红球和蓝球,分析出现频率最高的前8位数字") - public ApiResponse> fallowBallAnalysis( + @Operation(summary = "跟随球号分析算法", description = "根据输入的级别、前3个红球、后6个红球和蓝球,分析出现频率最高的前8位数字及筛选过程说明") + public ApiResponse fallowBallAnalysis( @Parameter(description = "用户ID,例如:1001", required = true) @RequestParam Long userId, @@ -496,10 +498,10 @@ public class BallAnalysisController { List firstThreeRedBallList = parseRedBalls(firstThreeRedBalls, 3, "前3个红球"); List lastSixRedBallList = parseRedBalls(lastSixRedBalls, 6, "后6个红球"); - // 调用分析服务 - List result = ballAnalysisService.fallowBallAnalysis(level, firstThreeRedBallList, lastSixRedBallList, blueBall); + // 调用分析服务(包含筛选过程说明) + FollowBallAnalysisResultVO result = ballAnalysisService.fallowBallAnalysisWithProcess(level, firstThreeRedBallList, lastSixRedBallList, blueBall); - log.info("跟随球号分析完成,结果:{}", result); + log.info("跟随球号分析完成,结果:{},过程说明:{}", result.getResult(), result.getFilteringProcess()); return ResultUtils.success(result); } catch (Exception e) { @@ -549,11 +551,11 @@ public class BallAnalysisController { * @param predictedBlueBalls 2个预测蓝球号码,用逗号分隔 * @param lastRedBalls 6个上期红球号码,用逗号分隔 * @param lastBlueBall 上期蓝球号码 - * @return 分析结果:频率最高的前4个蓝球号码 + * @return 分析结果:频率最高的前4个蓝球号码及其筛选过程说明 */ @PostMapping("/blue-ball") - @Operation(summary = "蓝球分析算法", description = "根据输入的级别、预测红球、预测蓝球、上期红球和上期蓝球,分析出频率最高的前4个蓝球号码") - public ApiResponse> blueBallAnalysis( + @Operation(summary = "蓝球分析算法", description = "根据输入的级别、预测红球、预测蓝球、上期红球和上期蓝球,分析出频率最高的前4个蓝球号码,并返回详细的筛选过程说明") + public ApiResponse blueBallAnalysis( @Parameter(description = "用户ID,例如:1001", required = true) @RequestParam Long userId, @@ -592,11 +594,11 @@ public class BallAnalysisController { List predictedBlueBallList = parseBlueBalls(predictedBlueBalls, 2, "预测蓝球"); List lastRedBallList = parseRedBalls(lastRedBalls, 6, "上期红球"); - // 调用分析服务 - List result = ballAnalysisService.blueBallAnalysis( + // 调用分析服务(带过程说明) + BlueBallAnalysisResultVO result = ballAnalysisService.blueBallAnalysisWithProcess( level, predictedRedBallList, predictedBlueBallList, lastRedBallList, lastBlueBall); - log.info("蓝球分析完成,结果:{}", result); + log.info("蓝球分析完成,结果:{},过程说明:{}", result.getResult(), result.getFilteringProcess()); return ResultUtils.success(result); } catch (Exception e) { diff --git a/src/main/java/com/xy/xyaicpzs/controller/DltBallActiveAnalysisController.java b/src/main/java/com/xy/xyaicpzs/controller/DltBallActiveAnalysisController.java new file mode 100644 index 0000000..ce185cb --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/DltBallActiveAnalysisController.java @@ -0,0 +1,292 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryAll; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistory100; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop100; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryAll; +import com.xy.xyaicpzs.domain.entity.DltBackendHistory100; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop100; +import com.xy.xyaicpzs.service.DltFrontendHistoryAllService; +import com.xy.xyaicpzs.service.DltFrontendHistory100Service; +import com.xy.xyaicpzs.service.DltFrontendHistoryTopService; +import com.xy.xyaicpzs.service.DltFrontendHistoryTop100Service; +import com.xy.xyaicpzs.service.DltBackendHistoryAllService; +import com.xy.xyaicpzs.service.DltBackendHistory100Service; +import com.xy.xyaicpzs.service.DltBackendHistoryTopService; +import com.xy.xyaicpzs.service.DltBackendHistoryTop100Service; +import com.xy.xyaicpzs.util.UserAuthValidator; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 大乐透球号活跃分析控制器 + * 提供大乐透前区和后区历史数据查询API接口 + */ +@Slf4j +@RestController +@RequestMapping("/dlt/ball-active-analysis") +@Tag(name = "大乐透球号活跃分析", description = "大乐透前区和后区历史数据查询API") +public class DltBallActiveAnalysisController { + + @Autowired + private DltFrontendHistoryAllService dltFrontendHistoryAllService; + + @Autowired + private DltFrontendHistory100Service dltFrontendHistory100Service; + + @Autowired + private DltFrontendHistoryTopService dltFrontendHistoryTopService; + + @Autowired + private DltFrontendHistoryTop100Service dltFrontendHistoryTop100Service; + + @Autowired + private DltBackendHistoryAllService dltBackendHistoryAllService; + + @Autowired + private DltBackendHistory100Service dltBackendHistory100Service; + + @Autowired + private DltBackendHistoryTopService dltBackendHistoryTopService; + + @Autowired + private DltBackendHistoryTop100Service dltBackendHistoryTop100Service; + + @Autowired + private UserAuthValidator userAuthValidator; + + /** + * 获取前区历史数据全部记录 + * @return 前区历史数据全部记录列表 + */ + @GetMapping("/frontend-history-all") + @Operation(summary = "获取前区历史数据全部记录", description = "获取dlt_frontend_history_all表中的所有前区历史数据记录") + public ApiResponse> getFrontendHistoryAll(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取前区历史数据全部记录请求"); + + // 调用服务获取前区全部历史数据 + List result = dltFrontendHistoryAllService.list(); + + log.info("获取前区历史数据全部记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取前区历史数据全部记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取前区历史数据全部记录失败:" + e.getMessage()); + } + } + + /** + * 获取前区最近100期数据记录 + * @return 前区最近100期数据记录列表 + */ + @GetMapping("/frontend-history-100") + @Operation(summary = "获取前区最近100期数据记录", description = "获取dlt_frontend_history_100表中的所有前区最近100期数据记录") + public ApiResponse> getFrontendHistory100(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取前区最近100期数据记录请求"); + + // 调用服务获取前区最近100期数据 + List result = dltFrontendHistory100Service.list(); + + log.info("获取前区最近100期数据记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取前区最近100期数据记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取前区最近100期数据记录失败:" + e.getMessage()); + } + } + + /** + * 获取前区历史数据排行记录 + * @return 前区历史数据排行记录列表 + */ + @GetMapping("/frontend-history-top") + @Operation(summary = "获取前区历史数据排行记录", description = "获取dlt_frontend_history_top表中的所有前区历史数据排行记录") + public ApiResponse> getFrontendHistoryTop(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取前区历史数据排行记录请求"); + + // 调用服务获取前区历史数据排行 + List result = dltFrontendHistoryTopService.list(); + + log.info("获取前区历史数据排行记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取前区历史数据排行记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取前区历史数据排行记录失败:" + e.getMessage()); + } + } + + /** + * 获取前区100期数据排行记录 + * @return 前区100期数据排行记录列表 + */ + @GetMapping("/frontend-history-top-100") + @Operation(summary = "获取前区100期数据排行记录", description = "获取dlt_frontend_history_top_100表中的所有前区100期数据排行记录") + public ApiResponse> getFrontendHistoryTop100(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取前区100期数据排行记录请求"); + + // 调用服务获取前区100期数据排行 + List result = dltFrontendHistoryTop100Service.list(); + + log.info("获取前区100期数据排行记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取前区100期数据排行记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取前区100期数据排行记录失败:" + e.getMessage()); + } + } + + /** + * 获取后区历史数据全部记录 + * @return 后区历史数据全部记录列表 + */ + @GetMapping("/backend-history-all") + @Operation(summary = "获取后区历史数据全部记录", description = "获取dlt_backend_history_all表中的所有后区历史数据记录") + public ApiResponse> getBackendHistoryAll(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取后区历史数据全部记录请求"); + + // 调用服务获取后区全部历史数据 + List result = dltBackendHistoryAllService.list(); + + log.info("获取后区历史数据全部记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取后区历史数据全部记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取后区历史数据全部记录失败:" + e.getMessage()); + } + } + + /** + * 获取后区最近100期数据记录 + * @return 后区最近100期数据记录列表 + */ + @GetMapping("/backend-history-100") + @Operation(summary = "获取后区最近100期数据记录", description = "获取dlt_backend_history_100表中的所有后区最近100期数据记录") + public ApiResponse> getBackendHistory100(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取后区最近100期数据记录请求"); + + // 调用服务获取后区最近100期数据 + List result = dltBackendHistory100Service.list(); + + log.info("获取后区最近100期数据记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取后区最近100期数据记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取后区最近100期数据记录失败:" + e.getMessage()); + } + } + + /** + * 获取后区历史数据排行记录 + * @return 后区历史数据排行记录列表 + */ + @GetMapping("/backend-history-top") + @Operation(summary = "获取后区历史数据排行记录", description = "获取dlt_backend_history_top表中的所有后区历史数据排行记录") + public ApiResponse> getBackendHistoryTop(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取后区历史数据排行记录请求"); + + // 调用服务获取后区历史数据排行 + List result = dltBackendHistoryTopService.list(); + + log.info("获取后区历史数据排行记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取后区历史数据排行记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取后区历史数据排行记录失败:" + e.getMessage()); + } + } + + /** + * 获取后区100期数据排行记录 + * @return 后区100期数据排行记录列表 + */ + @GetMapping("/backend-history-top-100") + @Operation(summary = "获取后区100期数据排行记录", description = "获取dlt_backend_history_top_100表中的所有后区100期数据排行记录") + public ApiResponse> getBackendHistoryTop100(HttpServletRequest request) { + // 权限验证 + ApiResponse> authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到获取后区100期数据排行记录请求"); + + // 调用服务获取后区100期数据排行 + List result = dltBackendHistoryTop100Service.list(); + + log.info("获取后区100期数据排行记录完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取后区100期数据排行记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取后区100期数据排行记录失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/controller/DltBallAnalysisController.java b/src/main/java/com/xy/xyaicpzs/controller/DltBallAnalysisController.java new file mode 100644 index 0000000..8d07c37 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/DltBallAnalysisController.java @@ -0,0 +1,493 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.requset.BackBallPredictionRequest; +import com.xy.xyaicpzs.common.requset.FirstBallPredictionRequest; +import com.xy.xyaicpzs.common.requset.FollowBackBallPredictionRequest; +import com.xy.xyaicpzs.common.requset.FollowerBallPredictionRequest; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.entity.DltPredictRecord; +import com.xy.xyaicpzs.domain.vo.BallCombinationAnalysisVO; +import com.xy.xyaicpzs.domain.vo.BallPersistenceAnalysisVO; +import com.xy.xyaicpzs.domain.vo.FirstBallPredictionResultVO; +import com.xy.xyaicpzs.domain.vo.FollowerBallPredictionResultVO; +import com.xy.xyaicpzs.domain.vo.BackBallPredictionResultVO; +import com.xy.xyaicpzs.domain.vo.FollowBackBallPredictionResultVO; +import com.xy.xyaicpzs.dlt.BackBallPredictor; +import com.xy.xyaicpzs.dlt.FirstBallPredictor; +import com.xy.xyaicpzs.dlt.FirstBallPredictor.FirstBallPredictionResult; +import com.xy.xyaicpzs.dlt.FollowBackBallPredictor; +import com.xy.xyaicpzs.dlt.FollowerBallPredictor; +import com.xy.xyaicpzs.service.DltCombinationAnalysisService; +import com.xy.xyaicpzs.service.DltPersistenceAnalysisService; +import com.xy.xyaicpzs.service.DltPredictRecordService; +import com.xy.xyaicpzs.util.UserAuthValidator; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import jakarta.servlet.http.HttpServletRequest; + +@Slf4j +@RestController +@RequestMapping("/dlt/ball-analysis") +@Tag(name = "大乐透球号分析", description = "大乐透球号分析算法API") +public class DltBallAnalysisController { + + @Autowired + private FirstBallPredictor firstBallPredictor; + + @Autowired + private FollowerBallPredictor followerBallPredictor; + + @Autowired + private BackBallPredictor backBallPredictor; + + @Autowired + private FollowBackBallPredictor followBackBallPredictor; + + @Autowired + private DltPredictRecordService dltPredictRecordService; + + @Autowired + private DltCombinationAnalysisService dltCombinationAnalysisService; + + @Autowired + private DltPersistenceAnalysisService dltPersistenceAnalysisService; + + @Autowired + private UserAuthValidator userAuthValidator; + + @PostMapping("/predict-first-ball") + @Operation(summary = "前区首球预测", description = "根据输入的级别和号码,预测前区首球") + public ApiResponse predictFirstBall(@RequestBody FirstBallPredictionRequest request) { + FirstBallPredictionResult predictionResult = firstBallPredictor.predictFirstBallWithProcess( + request.getLevel(), request.getRedBalls(), request.getBlueBalls()); + + FirstBallPredictionResultVO resultVO = FirstBallPredictionResultVO.builder() + .result(predictionResult.getResult()) + .filteringProcess(predictionResult.getFilteringProcess()) + .build(); + + return ResultUtils.success(resultVO); + } + + @PostMapping("/predict-follower-ball") + @Operation(summary = "前区随球预测", description = "根据输入的级别和号码,预测前区随球") + public ApiResponse predictFollowerBall(@RequestBody FollowerBallPredictionRequest request) { + FollowerBallPredictor.FollowerBallPredictionResult result = followerBallPredictor.predictFollowerBallWithProcess(request.getLevel(), request.getWellRegardedBalls(), request.getPreviousFrontBalls(), request.getPreviousBackBalls()); + FollowerBallPredictionResultVO vo = FollowerBallPredictionResultVO.builder() + .result(result.getResult()) + .filteringProcess(result.getFilteringProcess()) + .build(); + return ResultUtils.success(vo); + } + + @PostMapping("/predict-back-ball") + @Operation(summary = "后区首球预测", description = "根据输入的级别和号码,预测后区首球") + public ApiResponse predictBackBall(@RequestBody BackBallPredictionRequest request) { + BackBallPredictor.BackBallPredictionResult result = backBallPredictor.predictBackBallWithProcess(request.getLevel(), request.getNextFrontBalls(), request.getPreviousFrontBalls(), request.getPreviousBackBalls(), request.getNextBackBalls()); + BackBallPredictionResultVO vo = BackBallPredictionResultVO.builder() + .result(result.getResult()) + .filteringProcess(result.getFilteringProcess()) + .build(); + return ResultUtils.success(vo); + } + + @PostMapping("/predict-follow-back-ball") + @Operation(summary = "后区随球预测", description = "根据输入的级别和号码,预测后区随球") + public ApiResponse predictFollowBackBall(@RequestBody FollowBackBallPredictionRequest request) { + FollowBackBallPredictor.FollowBackBallPredictionResult result = followBackBallPredictor.predictFollowBackBallWithProcess(request.getLevel(), request.getBackFirstBall(), request.getNextFrontBalls(), request.getPreviousFrontBalls(), request.getPreviousBackBalls()); + FollowBackBallPredictionResultVO vo = FollowBackBallPredictionResultVO.builder() + .result(result.getResult()) + .filteringProcess(result.getFilteringProcess()) + .build(); + return ResultUtils.success(vo); + } + + /** + * 创建大乐透预测记录 + * @param userId 用户ID + * @param drawId 开奖期号 + * @param drawDate 开奖日期 + * @param frontBalls 5个前区球号码,用逗号分隔 + * @param backBalls 2个后区球号码,用逗号分隔 + * @return 创建的预测记录 + */ + @PostMapping("/create-dlt-predict") + @Operation(summary = "创建大乐透预测记录", description = "向dlt_predict_record表插入一条大乐透预测记录数据") + public ApiResponse createDltPredictRecord( + @Parameter(description = "用户ID,例如:1001", required = true) + @RequestParam Long userId, + + @Parameter(description = "开奖期号,例如:25001", required = true) + @RequestParam Long drawId, + + @Parameter(description = "开奖日期,格式:yyyy-MM-dd", required = false) + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date drawDate, + + @Parameter(description = "5个前区球号码,用逗号分隔,例如:1,5,12,18,25", required = true) + @RequestParam String frontBalls, + + @Parameter(description = "2个后区球号码,用逗号分隔,例如:8,12", required = true) + @RequestParam String backBalls) { + + try { + log.info("接收到创建大乐透预测记录请求:用户ID={},期号={},开奖日期={},前区球={},后区球={}", + userId, drawId, drawDate, frontBalls, backBalls); + + // 解析前区球号码 + List frontBallList = parseDltBalls(frontBalls, 5, "前区球", 1, 35); + + // 解析后区球号码 + List backBallList = parseDltBalls(backBalls, 2, "后区球", 1, 12); + + // 调用服务创建大乐透预测记录 + DltPredictRecord result = dltPredictRecordService.createDltPredictRecord(userId, drawId, drawDate, frontBallList, backBallList); + + log.info("创建大乐透预测记录完成,用户ID:{},记录ID:{}", userId, result.getId()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("创建大乐透预测记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "创建大乐透预测记录失败:" + e.getMessage()); + } + } + + /** + * 解析大乐透球号码字符串 + */ + private List parseDltBalls(String balls, int expectedCount, String ballType, int minValue, int maxValue) { + if (balls == null || balls.trim().isEmpty()) { + throw new IllegalArgumentException(ballType + "号码不能为空"); + } + + try { + String[] parts = balls.split(","); + if (parts.length != expectedCount) { + throw new IllegalArgumentException(ballType + "数量必须为" + expectedCount + "个,实际:" + parts.length); + } + + List result = Arrays.stream(parts) + .map(String::trim) + .map(Integer::parseInt) + .collect(java.util.stream.Collectors.toList()); + + // 验证球号码范围 + for (Integer ball : result) { + if (ball < minValue || ball > maxValue) { + throw new IllegalArgumentException(ballType + "号码必须在" + minValue + "-" + maxValue + "范围内,错误值:" + ball); + } + } + + return result; + + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ballType + "号码格式错误,请使用逗号分隔的数字"); + } + } + + /** + * 前区与前区的组合性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(前区) + * @return 系数分析结果 + */ + @GetMapping("/front-front-combination-analysis") + @Operation(summary = "前区与前区的组合性分析", description = "根据前区主球和前区随球号码,查询D5表获取系数,并计算主球与其他球号的组合情况") + public ApiResponse frontFrontCombinationAnalysis( + @Parameter(description = "主球号码(前区),例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(前区),例如:12", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到前区与前区的组合性分析请求:主球(前区)={},随球(前区)={}", masterBall, slaveBall); + + BallCombinationAnalysisVO result = dltCombinationAnalysisService.analyzeFrontFrontCombination(masterBall, slaveBall); + + log.info("前区与前区的组合性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("前区与前区的组合性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "前区与前区的组合性分析失败:" + e.getMessage()); + } + } + + /** + * 前区与后区的组合性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(后区) + * @return 系数分析结果 + */ + @GetMapping("/front-back-combination-analysis") + @Operation(summary = "前区与后区的组合性分析", description = "根据前区主球和后区随球号码,查询D6表获取系数,并返回扩展分析") + public ApiResponse frontBackCombinationAnalysis( + @Parameter(description = "主球号码(前区),例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(后区),例如:8", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到前区与后区的组合性分析请求:主球(前区)={},随球(后区)={}", masterBall, slaveBall); + + BallCombinationAnalysisVO result = dltCombinationAnalysisService.analyzeFrontBackCombination(masterBall, slaveBall); + + log.info("前区与后区的组合性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("前区与后区的组合性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "前区与后区的组合性分析失败:" + e.getMessage()); + } + } + + /** + * 后区与后区的组合性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(后区) + * @return 系数分析结果 + */ + @GetMapping("/back-back-combination-analysis") + @Operation(summary = "后区与后区的组合性分析", description = "根据后区主球和后区随球号码,查询D7表获取系数,并返回扩展分析") + public ApiResponse backBackCombinationAnalysis( + @Parameter(description = "主球号码(后区),例如:8", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(后区),例如:12", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到后区与后区的组合性分析请求:主球(后区)={},随球(后区)={}", masterBall, slaveBall); + + BallCombinationAnalysisVO result = dltCombinationAnalysisService.analyzeBackBackCombination(masterBall, slaveBall); + + log.info("后区与后区的组合性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("后区与后区的组合性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "后区与后区的组合性分析失败:" + e.getMessage()); + } + } + + /** + * 后区与前区的组合性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(前区) + * @return 系数分析结果 + */ + @GetMapping("/back-front-combination-analysis") + @Operation(summary = "后区与前区的组合性分析", description = "根据后区主球和前区随球号码,查询D8表获取系数,并返回扩展分析") + public ApiResponse backFrontCombinationAnalysis( + @Parameter(description = "主球号码(后区),例如:8", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(前区),例如:5", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到后区与前区的组合性分析请求:主球(后区)={},随球(前区)={}", masterBall, slaveBall); + + BallCombinationAnalysisVO result = dltCombinationAnalysisService.analyzeBackFrontCombination(masterBall, slaveBall); + + log.info("后区与前区的组合性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("后区与前区的组合性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "后区与前区的组合性分析失败:" + e.getMessage()); + } + } + + /** + * 前区与前区的持续性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(前区) + * @return 线系数分析结果 + */ + @GetMapping("/front-front-persistence-analysis") + @Operation(summary = "前区与前区的持续性分析", description = "根据前区主球和前区随球号码,查询D9表获取线系数,并返回扩展分析") + public ApiResponse frontFrontPersistenceAnalysis( + @Parameter(description = "主球号码(前区),例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(前区),例如:12", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到前区与前区的持续性分析请求:主球(前区)={},随球(前区)={}", masterBall, slaveBall); + + BallPersistenceAnalysisVO result = dltPersistenceAnalysisService.analyzeFrontFrontPersistence(masterBall, slaveBall); + + log.info("前区与前区的持续性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("前区与前区的持续性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "前区与前区的持续性分析失败:" + e.getMessage()); + } + } + + /** + * 后区与后区的持续性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(后区) + * @return 线系数分析结果 + */ + @GetMapping("/back-back-persistence-analysis") + @Operation(summary = "后区与后区的持续性分析", description = "根据后区主球和后区随球号码,查询D11表获取线系数,并返回扩展分析") + public ApiResponse backBackPersistenceAnalysis( + @Parameter(description = "主球号码(后区),例如:8", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(后区),例如:12", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到后区与后区的持续性分析请求:主球(后区)={},随球(后区)={}", masterBall, slaveBall); + + BallPersistenceAnalysisVO result = dltPersistenceAnalysisService.analyzeBackBackPersistence(masterBall, slaveBall); + + log.info("后区与后区的持续性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("后区与后区的持续性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "后区与后区的持续性分析失败:" + e.getMessage()); + } + } + + /** + * 前区与后区的持续性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(后区) + * @return 线系数分析结果 + */ + @GetMapping("/front-back-persistence-analysis") + @Operation(summary = "前区与后区的持续性分析", description = "根据前区主球和后区随球号码,查询D10表获取线系数,并返回扩展分析") + public ApiResponse frontBackPersistenceAnalysis( + @Parameter(description = "主球号码(前区),例如:5", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(后区),例如:8", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到前区与后区的持续性分析请求:主球(前区)={},随球(后区)={}", masterBall, slaveBall); + + BallPersistenceAnalysisVO result = dltPersistenceAnalysisService.analyzeFrontBackPersistence(masterBall, slaveBall); + + log.info("前区与后区的持续性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("前区与后区的持续性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "前区与后区的持续性分析失败:" + e.getMessage()); + } + } + + /** + * 后区与前区的持续性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(前区) + * @return 线系数分析结果 + */ + @GetMapping("/back-front-persistence-analysis") + @Operation(summary = "后区与前区的持续性分析", description = "根据后区主球和前区随球号码,查询D12表获取线系数,并返回扩展分析") + public ApiResponse backFrontPersistenceAnalysis( + @Parameter(description = "主球号码(后区),例如:8", required = true) + @RequestParam Integer masterBall, + + @Parameter(description = "随球号码(前区),例如:5", required = true) + @RequestParam Integer slaveBall, + + HttpServletRequest request) { + + // 权限验证 + ApiResponse authResult = userAuthValidator.validateUserAuth(request); + if (authResult != null) { + return authResult; + } + + try { + log.info("接收到后区与前区的持续性分析请求:主球(后区)={},随球(前区)={}", masterBall, slaveBall); + + BallPersistenceAnalysisVO result = dltPersistenceAnalysisService.analyzeBackFrontPersistence(masterBall, slaveBall); + + log.info("后区与前区的持续性分析完成:{}", result); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("后区与前区的持续性分析失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "后区与前区的持续性分析失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/controller/DltDrawController.java b/src/main/java/com/xy/xyaicpzs/controller/DltDrawController.java new file mode 100644 index 0000000..a905c20 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/DltDrawController.java @@ -0,0 +1,305 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.service.DltDrawRecordService; +import com.xy.xyaicpzs.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 大乐透开奖记录控制器 + * 提供大乐透开奖记录查询的API接口 + */ +@Slf4j +@RestController +@RequestMapping("/dlt-draw") +@Tag(name = "大乐透开奖记录", description = "大乐透开奖记录查询API") +public class DltDrawController { + + @Autowired + private DltDrawRecordService dltDrawRecordService; + + @Autowired + private UserService userService; + + /** + * 获取近期大乐透开奖信息 + * @param limit 获取条数,可选参数,默认10条 + * @return 近期开奖信息列表 + */ + @GetMapping("/recent-draws") + @Operation(summary = "获取近期大乐透开奖信息", description = "获取最近的大乐透开奖信息,默认返回10条,按开奖日期倒序排列") + public ApiResponse> getRecentDraws( + @Parameter(description = "获取条数,默认10条", required = false) + @RequestParam(required = false, defaultValue = "10") Integer limit, HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + try { + log.info("接收到获取近期大乐透开奖信息请求:条数={}", limit); + + // 调用服务获取近期开奖信息,按开奖日期倒序排列 + List result = dltDrawRecordService.lambdaQuery() + .orderByDesc(DltDrawRecord::getDrawDate) + .last("LIMIT " + limit) + .list(); + + log.info("获取近期大乐透开奖信息完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取近期大乐透开奖信息失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取近期大乐透开奖信息失败:" + e.getMessage()); + } + } + + /** + * 根据日期范围查询大乐透开奖信息 + * @param startDate 开始日期(可选,格式:yyyy-MM-dd) + * @param endDate 结束日期(可选,格式:yyyy-MM-dd) + * @return 开奖信息列表 + */ + @GetMapping("/query-draws") + @Operation(summary = "按日期范围查询大乐透开奖信息", description = "根据日期范围查询大乐透开奖信息,支持单边日期查询") + public ApiResponse> queryDraws( + @Parameter(description = "开始日期,格式:yyyy-MM-dd,例如:2025-01-01", required = false) + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate, + + @Parameter(description = "结束日期,格式:yyyy-MM-dd,例如:2025-01-31", required = false) + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate, + HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + try { + log.info("接收到按日期范围查询大乐透开奖信息请求:开始日期={},结束日期={}", startDate, endDate); + + // 日期范围验证 + if (startDate != null && endDate != null && startDate.after(endDate)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "开始日期不能晚于结束日期"); + } + + // 构建查询条件 + var queryWrapper = dltDrawRecordService.lambdaQuery(); + + if (startDate != null) { + queryWrapper.ge(DltDrawRecord::getDrawDate, startDate); + } + + if (endDate != null) { + queryWrapper.le(DltDrawRecord::getDrawDate, endDate); + } + + // 按开奖日期倒序排列 + List result = queryWrapper + .orderByDesc(DltDrawRecord::getDrawDate) + .list(); + + log.info("按日期范围查询大乐透开奖信息完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("按日期范围查询大乐透开奖信息失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询大乐透开奖信息失败:" + e.getMessage()); + } + } + + /** + * 根据期号精准查询单条大乐透开奖信息 + * @param drawId 开奖期号 + * @return 开奖信息 + */ + @GetMapping("/draw/{drawId}") + @Operation(summary = "根据期号查询大乐透开奖信息", description = "根据期号精准查询单条大乐透开奖信息") + public ApiResponse getDrawById( + @Parameter(description = "开奖期号,例如:07001", required = true) + @PathVariable String drawId, + HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + try { + log.info("接收到根据期号查询大乐透开奖信息请求:期号={}", drawId); + + // 调用服务查询开奖信息 + DltDrawRecord result = dltDrawRecordService.lambdaQuery() + .eq(DltDrawRecord::getDrawId, drawId) + .one(); + + if (result == null) { + log.warn("未找到期号为{}的大乐透开奖信息", drawId); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "未找到期号为" + drawId + "的大乐透开奖信息"); + } + + log.info("根据期号查询大乐透开奖信息完成:{}", result.getDrawId()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("根据期号查询大乐透开奖信息失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询大乐透开奖信息失败:" + e.getMessage()); + } + } + + /** + * 根据开奖期号查询大乐透开奖球号 + * @param drawId 开奖期号 + * @return 7个中奖球号(5个前区+2个后区) + */ + @GetMapping("/draw/{drawId}/numbers") + @Operation(summary = "根据开奖期号查询大乐透开奖球号", description = "根据开奖期号查询7个中奖球号(5个前区+2个后区)") + public ApiResponse> getDrawNumbersById( + @Parameter(description = "开奖期号,例如:07001", required = true) + @PathVariable String drawId, + HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + try { + log.info("接收到根据期号查询大乐透开奖球号请求:期号={}", drawId); + + // 调用服务查询开奖信息 + DltDrawRecord result = dltDrawRecordService.lambdaQuery() + .eq(DltDrawRecord::getDrawId, drawId) + .one(); + + if (result == null) { + log.warn("未找到期号为{}的大乐透开奖信息", drawId); + return ResultUtils.error(ErrorCode.NOT_FOUND_ERROR, "未找到期号为" + drawId + "的大乐透开奖信息"); + } + + List winningNumbers = new ArrayList<>(); + // 前区5个球号 + winningNumbers.add(result.getFrontBall1()); + winningNumbers.add(result.getFrontBall2()); + winningNumbers.add(result.getFrontBall3()); + winningNumbers.add(result.getFrontBall4()); + winningNumbers.add(result.getFrontBall5()); + // 后区2个球号 + winningNumbers.add(result.getBackBall1()); + winningNumbers.add(result.getBackBall2()); + + log.info("根据期号查询大乐透开奖球号完成:{},球号:{}", drawId, winningNumbers); + return ResultUtils.success(winningNumbers); + + } catch (Exception e) { + log.error("根据期号查询大乐透开奖球号失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询大乐透开奖球号失败:" + e.getMessage()); + } + } + + /** + * 获取近10期大乐透开奖期号 + * @return 近10期开奖期号列表 + */ + @GetMapping("/recent-10-draw-ids") + @Operation(summary = "获取近10期大乐透开奖期号", description = "获取dlt_draw_record表中最新的10期开奖期号,按开奖日期倒序排列") + public ApiResponse> getRecent10DrawIds(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + try { + log.info("接收到获取近10期大乐透开奖期号请求"); + + // 查询最近10期开奖期号,按开奖日期倒序 + List drawIds = dltDrawRecordService.lambdaQuery() + .select(DltDrawRecord::getDrawId) + .orderByDesc(DltDrawRecord::getDrawDate) + .last("LIMIT 10") + .list() + .stream() + .map(DltDrawRecord::getDrawId) + .collect(java.util.stream.Collectors.toList()); + + log.info("获取近10期大乐透开奖期号完成,返回{}条记录", drawIds.size()); + return ResultUtils.success(drawIds); + + } catch (Exception e) { + log.error("获取近10期大乐透开奖期号失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取近10期大乐透开奖期号失败:" + e.getMessage()); + } + } + + /** + * 获取近10期大乐透开奖信息 + * @return 近10期开奖信息列表 + */ + @GetMapping("/recent-10-draws") + @Operation(summary = "获取近10期大乐透开奖信息", description = "获取dlt_draw_record表中最新的10期开奖信息,按开奖日期倒序排列") + public ApiResponse> getRecent10Draws(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + try { + log.info("接收到获取近10期大乐透开奖信息请求"); + + // 调用服务获取近10期开奖信息,按开奖日期倒序 + List result = dltDrawRecordService.lambdaQuery() + .orderByDesc(DltDrawRecord::getDrawDate) + .last("LIMIT 10") + .list(); + + log.info("获取近10期大乐透开奖信息完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取近10期大乐透开奖信息失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取近10期大乐透开奖信息失败:" + e.getMessage()); + } + } + + /** + * 获取近100期大乐透开奖信息 + * @return 近100期开奖信息列表 + */ + @GetMapping("/recent-100-draws") + @Operation(summary = "获取近100期大乐透开奖信息", description = "获取dlt_draw_record表中最新的100期开奖信息,按开奖日期倒序排列") + public ApiResponse> getRecent100Draws(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + try { + log.info("接收到获取近100期大乐透开奖信息请求"); + + // 调用服务获取近100期开奖信息,按开奖日期倒序 + List result = dltDrawRecordService.lambdaQuery() + .orderByDesc(DltDrawRecord::getDrawDate) + .last("LIMIT 100") + .list(); + + log.info("获取近100期大乐透开奖信息完成,返回{}条记录", result.size()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取近100期大乐透开奖信息失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取近100期大乐透开奖信息失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/DltImportController.java b/src/main/java/com/xy/xyaicpzs/controller/DltImportController.java new file mode 100644 index 0000000..07c8a03 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/DltImportController.java @@ -0,0 +1,197 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.service.OperationHistoryService; +import com.xy.xyaicpzs.service.UserService; +import com.xy.xyaicpzs.util.DltDataImporter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +/** + * 大乐透数据导入控制器 + */ +@Slf4j +@RestController +@RequestMapping("/dlt") +@Tag(name = "大乐透数据导入", description = "大乐透数据导入相关接口") +public class DltImportController { + + @Autowired + private DltDataImporter dltDataImporter; + + @Autowired + private UserService userService; + + @Autowired + private OperationHistoryService operationHistoryService; + + /** + * 上传Excel文件并完整导入大乐透数据(D3-D12工作表) + */ + @PostMapping("/upload") + @Operation(summary = "上传Excel文件完整导入大乐透数据", description = "上传包含D3-D12工作表的Excel文件,将前区历史数据、后区历史数据和系数数据导入到对应表中") + public ApiResponse uploadDltFile( + @Parameter(description = "包含D3-D12工作表的Excel文件(.xlsx格式)", required = true) + @RequestParam("file") MultipartFile file, HttpServletRequest httpServletRequest) { + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + String fileName = file.getOriginalFilename(); + log.info("接收到大乐透完整数据上传请求,文件名:{}", fileName); + + // 验证文件格式 + if (fileName == null || !fileName.toLowerCase().endsWith(".xlsx")) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "只支持.xlsx格式的Excel文件"); + } + + try { + // 创建临时文件 + Path tempFile = Files.createTempFile("dlt_upload_", ".xlsx"); + Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING); + + // 导入完整数据(D3-D12) + dltDataImporter.importDltCompleteData(tempFile.toString()); + + // 删除临时文件 + Files.deleteIfExists(tempFile); + + String message = "大乐透完整数据导入成功"; + + // 记录操作历史 - 成功 + String resultMessage = String.format("%s成功上传并导入大乐透完整数据文件:%s", userName, fileName); + operationHistoryService.recordOperation(userId, "大乐透完整数据导入", 1, "成功", resultMessage); + + return ResultUtils.success(message); + + } catch (Exception e) { + log.error("大乐透完整数据导入失败,文件名:{},错误:{}", fileName, e.getMessage(), e); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%s大乐透完整数据导入失败,文件名:%s,错误原因:%s", userName, fileName, e.getMessage()); + operationHistoryService.recordOperation(userId, "大乐透完整数据导入", 1, "失败", resultMessage); + + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "大乐透完整数据导入失败:" + e.getMessage()); + } + } + + /** + * 上传Excel文件并导入大乐透开奖数据(覆盖) + */ + @PostMapping("/upload-draw-data") + @Operation(summary = "上传Excel文件导入大乐透开奖数据", description = "上传包含D1工作表的Excel文件,将大乐透开奖数据导入到dlt_draw_record表中(覆盖现有数据)") + public ApiResponse uploadDltDrawData( + @Parameter(description = "包含D1工作表的Excel文件(.xlsx格式)", required = true) + @RequestParam("file") MultipartFile file, HttpServletRequest httpServletRequest) { + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + String fileName = file.getOriginalFilename(); + log.info("接收到大乐透开奖数据上传请求,文件名:{}", fileName); + + // 验证文件格式 + if (fileName == null || !fileName.toLowerCase().endsWith(".xlsx")) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "只支持.xlsx格式的Excel文件"); + } + + try { + // 创建临时文件 + Path tempFile = Files.createTempFile("dlt_draw_upload_", ".xlsx"); + Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING); + + // 导入数据 + dltDataImporter.importDltDrawData(tempFile.toString()); + + // 删除临时文件 + Files.deleteIfExists(tempFile); + + String message = "大乐透开奖数据导入成功"; + + // 记录操作历史 - 成功 + String resultMessage = String.format("%s成功上传并导入大乐透开奖数据文件:%s", userName, fileName); + operationHistoryService.recordOperation(userId, "大乐透开奖数据导入", 1, "成功", resultMessage); + + return ResultUtils.success(message); + } catch (Exception e) { + log.error("大乐透开奖数据导入失败,文件名:{},错误:{}", fileName, e.getMessage(), e); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%s大乐透开奖数据导入失败,文件名:%s,错误原因:%s", userName, fileName, e.getMessage()); + operationHistoryService.recordOperation(userId, "大乐透开奖数据导入", 1, "失败", resultMessage); + + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "大乐透开奖数据导入失败:" + e.getMessage()); + } + } + + /** + * 上传Excel文件并追加导入大乐透开奖数据 + */ + @PostMapping("/append-draw-data") + @Operation(summary = "追加导入大乐透开奖数据", description = "上传包含D1工作表的Excel文件,将大乐透开奖数据追加导入到dlt_draw_record表中(不清空现有数据,跳过重复记录)") + public ApiResponse appendDltDrawData( + @Parameter(description = "包含D1工作表的Excel文件(.xlsx格式)", required = true) + @RequestParam("file") MultipartFile file, HttpServletRequest httpServletRequest) { + User loginUser = userService.getLoginUser(httpServletRequest); + if (!userService.isAdmin(loginUser)){ + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无权限"); + } + Long userId = loginUser.getId(); + String userName = loginUser.getUserName(); + + String fileName = file.getOriginalFilename(); + log.info("接收到大乐透开奖数据追加导入请求,文件名:{}", fileName); + + // 验证文件格式 + if (fileName == null || !fileName.toLowerCase().endsWith(".xlsx")) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "只支持.xlsx格式的Excel文件"); + } + + try { + // 创建临时文件 + Path tempFile = Files.createTempFile("dlt_append_", ".xlsx"); + Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING); + + // 追加导入数据 + dltDataImporter.appendDltDrawData(tempFile.toString()); + + // 删除临时文件 + Files.deleteIfExists(tempFile); + + String message = "大乐透开奖数据追加导入成功"; + + // 记录操作历史 - 成功 + String resultMessage = String.format("%s成功追加导入大乐透开奖数据文件:%s", userName, fileName); + operationHistoryService.recordOperation(userId, "大乐透开奖数据追加导入", 1, "成功", resultMessage); + + return ResultUtils.success(message); + } catch (Exception e) { + log.error("大乐透开奖数据追加导入失败,文件名:{},错误:{}", fileName, e.getMessage(), e); + + // 记录操作历史 - 失败 + String resultMessage = String.format("%s大乐透开奖数据追加导入失败,文件名:%s,错误原因:%s", userName, fileName, e.getMessage()); + operationHistoryService.recordOperation(userId, "大乐透开奖数据追加导入", 1, "失败", resultMessage); + + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "大乐透开奖数据追加导入失败:" + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/controller/DltPredictController.java b/src/main/java/com/xy/xyaicpzs/controller/DltPredictController.java new file mode 100644 index 0000000..6050afa --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/DltPredictController.java @@ -0,0 +1,436 @@ +package com.xy.xyaicpzs.controller; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.requset.DltPredictRecordQueryRequest; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.common.response.PageResponse; +import com.xy.xyaicpzs.domain.entity.DltPredictRecord; +import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.domain.vo.PrizeEstimateVO; +import com.xy.xyaicpzs.domain.vo.RedBallHitRateVO; +import com.xy.xyaicpzs.domain.vo.UserPredictStatVO; +import com.xy.xyaicpzs.service.DataAnalysisService; +import com.xy.xyaicpzs.service.DltDataAnalysisService; +import com.xy.xyaicpzs.service.DltPredictRecordService; +import com.xy.xyaicpzs.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; + +/** + * 大乐透预测记录控制器 + * 提供大乐透预测记录查询相关的API接口 + */ +@Slf4j +@RestController +@RequestMapping("/dlt-predict") +@Tag(name = "大乐透预测记录", description = "大乐透预测记录查询API") +public class DltPredictController { + + @Autowired + private DltPredictRecordService dltPredictRecordService; + + @Autowired + private UserService userService; + + @Autowired + private DltDataAnalysisService dltDataAnalysisService; + + @Autowired + private DataAnalysisService dataAnalysisService; + + /** + * 根据用户ID获取大乐透预测记录 + * @param userId 用户ID + * @return 用户的所有大乐透预测记录列表 + */ + @GetMapping("/predict-records/{userId}") + @Operation(summary = "获取用户大乐透预测记录", description = "根据用户ID分页获取该用户的大乐透预测记录,按预测时间倒序排列,每页5条") + public ApiResponse> getDltPredictRecordsByUserId( + @Parameter(description = "用户ID,例如:1001", required = true) + @PathVariable Long userId, + @Parameter(description = "页码,从1开始,默认为1", required = false) + @RequestParam(value = "page", defaultValue = "1") Integer page, + HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录"); + } + + // 检查VIP权限 + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + try { + log.info("接收到获取用户大乐透预测记录请求:用户ID={},页码={}", userId, page); + + // 参数校验 + if (page < 1) { + page = 1; + } + + // 获取总记录数 + Long total = dltPredictRecordService.getDltPredictRecordsCountByUserId(userId); + + // 调用服务获取用户预测记录(分页,每页5条) + List records = dltPredictRecordService.getDltPredictRecordsByUserIdWithPaging(userId, page, 5); + + // 创建分页响应对象 + PageResponse pageResponse = PageResponse.of(records, total, page, 5); + + log.info("获取用户大乐透预测记录完成,用户ID:{},页码:{},返回{}条记录,总记录数:{}", userId, page, records.size(), total); + return ResultUtils.success(pageResponse); + + } catch (Exception e) { + log.error("获取用户大乐透预测记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取用户大乐透预测记录失败:" + e.getMessage()); + } + } + + /** + * 按条件查询大乐透预测记录 + * @param request 查询条件 + * @return 分页预测记录 + */ + @PostMapping("/query-predict-records") + @Operation(summary = "按条件查询大乐透预测记录", description = "根据用户ID和预测状态(待开奖/已开奖)以及预测结果筛选大乐透预测记录,支持分页查询") + public ApiResponse> queryDltPredictRecords(@RequestBody DltPredictRecordQueryRequest request, + HttpServletRequest httpRequest) { + + User loginUser = userService.getLoginUser(httpRequest); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录"); + } + + try { + log.info("接收到按条件查询大乐透预测记录请求:userId={}, predictStatus={}, predictResult={}, current={}, pageSize={}", + request.getUserId(), request.getPredictStatus(), request.getPredictResult(), + request.getCurrent(), request.getPageSize()); + + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + + // 添加用户ID筛选条件 + if (request.getUserId() != null) { + queryWrapper.eq("userId", request.getUserId()); + } + + // 添加预测状态筛选条件 + if (StringUtils.isNotBlank(request.getPredictStatus())) { + queryWrapper.eq("predictStatus", request.getPredictStatus()); + } + + // 添加预测结果筛选条件 + if (StringUtils.isNotBlank(request.getPredictResult())) { + queryWrapper.eq("predictResult", request.getPredictResult()); + } + + // 按预测时间降序排序 + queryWrapper.orderByDesc("predictTime"); + + // 执行分页查询 + Page page = new Page<>(request.getCurrent(), request.getPageSize()); + Page resultPage = dltPredictRecordService.page(page, queryWrapper); + + // 构建分页响应对象 + PageResponse result = PageResponse.of( + resultPage.getRecords(), + resultPage.getTotal(), + (int) resultPage.getCurrent(), + (int) resultPage.getSize() + ); + + log.info("按条件查询大乐透预测记录完成,总记录数:{}", result.getTotal()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("按条件查询大乐透预测记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "查询大乐透预测记录失败:" + e.getMessage()); + } + } + + /** + * 获取用户大乐透预测统计数据 + * @param userId 用户ID + * @return 用户大乐透预测统计数据 + */ + @GetMapping("/user-predict-stat/{userId}") + @Operation(summary = "获取用户大乐透预测统计数据", description = "根据用户ID获取该用户的大乐透预测次数、待开奖次数、命中次数、命中率等统计数据") + public ApiResponse getUserDltPredictStat( + @Parameter(description = "用户ID,例如:1001", required = true) + @PathVariable Long userId, + HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录"); + } + + // 检查VIP权限 + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + try { + log.info("接收到获取用户大乐透预测统计数据请求:用户ID={}", userId); + + // 调用服务获取用户大乐透预测统计数据 + UserPredictStatVO result = dltDataAnalysisService.getUserDltPredictStat(userId); + + log.info("获取用户大乐透预测统计数据完成,用户ID:{},预测次数:{},待开奖次数:{},命中次数:{},命中率:{}", + userId, result.getPredictCount(), result.getPendingCount(), + result.getHitCount(), result.getHitRate()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取用户大乐透预测统计数据失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取用户大乐透预测统计数据失败:" + e.getMessage()); + } + } + + /** + * 大乐透奖金统计 + * @return 奖金统计信息(各中奖等级、中奖次数、单奖金额、奖金合计) + */ + @GetMapping("/prize-statistics") + @Operation(summary = "大乐透奖金统计", description = "统计用户所有大乐透中奖记录,按等级汇总各等级的中奖次数和奖金") + public ApiResponse getDltPrizeStatistics(HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + // 检查VIP权限 + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + try { + log.info("接收到大乐透奖金统计请求"); + + // 调用服务进行大乐透奖金统计 + PrizeEstimateVO result = dltDataAnalysisService.getDltPrizeStatistics(loginUser.getId(), null); + + log.info("大乐透奖金统计完成,总奖金:{}", result.getTotalPrize()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("大乐透奖金统计失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "大乐透奖金统计失败:" + e.getMessage()); + } + } + + /** + * 手动触发处理待开奖记录(双色球+大乐透) + * @return 处理结果 + */ + @PostMapping("/process-pending") + @Operation(summary = "手动处理待开奖记录", description = "手动触发处理双色球和大乐透的待开奖预测记录,匹配开奖结果并更新中奖状态") + public ApiResponse processPendingPredictions(HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null){ + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "用户未登录"); + } + + // 检查管理员权限 + if (!userService.isAdmin(loginUser)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "无管理员权限"); + } + + try { + log.info("接收到手动处理待开奖记录请求(双色球+大乐透)"); + + int ssqProcessedCount = 0; + int dltProcessedCount = 0; + + // 处理双色球待开奖记录 + try { + log.info("开始处理双色球待开奖记录"); + ssqProcessedCount = dataAnalysisService.processPendingPredictions(); + log.info("双色球待开奖记录处理完成,共处理{}条", ssqProcessedCount); + } catch (Exception e) { + log.error("处理双色球待开奖记录时发生错误:{}", e.getMessage(), e); + } + + // 处理大乐透待开奖记录 + try { + log.info("开始处理大乐透待开奖记录"); + dltProcessedCount = dltDataAnalysisService.processPendingDltPredictions(); + log.info("大乐透待开奖记录处理完成,共处理{}条", dltProcessedCount); + } catch (Exception e) { + log.error("处理大乐透待开奖记录时发生错误:{}", e.getMessage(), e); + } + + int totalProcessedCount = ssqProcessedCount + dltProcessedCount; + String message = String.format("待开奖记录处理完成!双色球:%d条,大乐透:%d条,总计:%d条", + ssqProcessedCount, dltProcessedCount, totalProcessedCount); + log.info("手动处理待开奖记录完成:{}", message); + + return ResultUtils.success(message); + + } catch (Exception e) { + log.error("手动处理待开奖记录失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "处理待开奖记录失败:" + e.getMessage()); + } + } + + /** + * 获取大乐透前区首球命中率统计 + * @return 前区首球命中次数和命中率统计 + */ + @GetMapping("/front-first-ball-hit-rate") + @Operation(summary = "获取大乐透前区首球命中率统计", description = "统计用户的大乐透前区首球命中次数和命中率") + public ApiResponse getFrontFirstBallHitRate(HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + // 检查VIP权限 + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + try { + log.info("接收到获取大乐透前区首球命中率统计请求"); + + // 调用服务获取前区首球命中率 + RedBallHitRateVO result = dltDataAnalysisService.getFrontFirstBallHitRate(loginUser.getId()); + + log.info("获取大乐透前区首球命中率统计完成,命中总数:{},预测总数:{},命中率:{}", + result.getTotalHitCount(), result.getTotalPredictedCount(), result.getHitRate()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取大乐透前区首球命中率统计失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取大乐透前区首球命中率统计失败:" + e.getMessage()); + } + } + + /** + * 获取大乐透前区球号命中率统计 + * @return 前区球号命中总数和命中率统计 + */ + @GetMapping("/front-ball-hit-rate") + @Operation(summary = "获取大乐透前区球号命中率统计", description = "统计用户的大乐透前区球号命中总数和命中率") + public ApiResponse getFrontBallHitRate(HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + // 检查VIP权限 + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + try { + log.info("接收到获取大乐透前区球号命中率统计请求"); + + // 调用服务获取前区球号命中率 + RedBallHitRateVO result = dltDataAnalysisService.getFrontBallHitRate(loginUser.getId()); + + log.info("获取大乐透前区球号命中率统计完成,命中总数:{},预测总数:{},命中率:{}", + result.getTotalHitCount(), result.getTotalPredictedCount(), result.getHitRate()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取大乐透前区球号命中率统计失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取大乐透前区球号命中率统计失败:" + e.getMessage()); + } + } + + /** + * 获取大乐透后区首球命中率统计 + * @return 后区首球命中次数和命中率统计 + */ + @GetMapping("/back-first-ball-hit-rate") + @Operation(summary = "获取大乐透后区首球命中率统计", description = "统计用户的大乐透后区首球命中次数和命中率") + public ApiResponse getBackFirstBallHitRate(HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + // 检查VIP权限 + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + try { + log.info("接收到获取大乐透后区首球命中率统计请求"); + + // 调用服务获取后区首球命中率 + RedBallHitRateVO result = dltDataAnalysisService.getBackFirstBallHitRate(loginUser.getId()); + + log.info("获取大乐透后区首球命中率统计完成,命中总数:{},预测总数:{},命中率:{}", + result.getTotalHitCount(), result.getTotalPredictedCount(), result.getHitRate()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取大乐透后区首球命中率统计失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取大乐透后区首球命中率统计失败:" + e.getMessage()); + } + } + + /** + * 获取大乐透后区球号命中率统计 + * @return 后区球号命中次数和命中率统计 + */ + @GetMapping("/back-ball-hit-rate") + @Operation(summary = "获取大乐透后区球号命中率统计", description = "统计用户的大乐透后区球号命中次数和命中率") + public ApiResponse getBackBallHitRate(HttpServletRequest request) { + + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + // 检查VIP权限 + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + try { + log.info("接收到获取大乐透后区球号命中率统计请求"); + + // 调用服务获取后区球号命中率 + RedBallHitRateVO result = dltDataAnalysisService.getBackBallHitRate(loginUser.getId()); + + log.info("获取大乐透后区球号命中率统计完成,命中总数:{},预测总数:{},命中率:{}", + result.getTotalHitCount(), result.getTotalPredictedCount(), result.getHitRate()); + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("获取大乐透后区球号命中率统计失败:{}", e.getMessage(), e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "获取大乐透后区球号命中率统计失败:" + e.getMessage()); + } + } +} + diff --git a/src/main/java/com/xy/xyaicpzs/controller/ExcelImportController.java b/src/main/java/com/xy/xyaicpzs/controller/ExcelImportController.java index fac6f55..3c2da02 100644 --- a/src/main/java/com/xy/xyaicpzs/controller/ExcelImportController.java +++ b/src/main/java/com/xy/xyaicpzs/controller/ExcelImportController.java @@ -56,18 +56,18 @@ public class ExcelImportController { String message = excelImportService.importExcelFile(file); // 记录操作历史 - 成功 - String resultMessage = String.format("%s成功上传并导入Excel文件:%s,导入结果:%s", userName, fileName, message); - operationHistoryService.recordOperation(userId, "Excel数据导入", 1, "成功", resultMessage); + String resultMessage = String.format("%s成功上传并导入双色球Excel文件:%s,导入结果:%s", userName, fileName, message); + operationHistoryService.recordOperation(userId, "双色球Excel数据导入", 1, "成功", resultMessage); return ResultUtils.success(message); } catch (Exception e) { - log.error("Excel文件导入失败,文件名:{},错误:{}", fileName, e.getMessage(), e); + log.error("双色球Excel文件导入失败,文件名:{},错误:{}", fileName, e.getMessage(), e); // 记录操作历史 - 失败 - String resultMessage = String.format("%sExcel文件导入失败:%s,文件名:%s,错误原因:%s", userName, fileName, fileName, e.getMessage()); - operationHistoryService.recordOperation(userId, "Excel数据导入", 1, "失败", resultMessage); + String resultMessage = String.format("%s双色球Excel文件导入失败:%s,文件名:%s,错误原因:%s", userName, fileName, fileName, e.getMessage()); + operationHistoryService.recordOperation(userId, "双色球Excel数据导入", 1, "失败", resultMessage); - return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "Excel文件导入失败:" + e.getMessage()); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "双色球Excel文件导入失败:" + e.getMessage()); } } @@ -93,18 +93,18 @@ public class ExcelImportController { String message = excelImportService.importLotteryDrawsFile(file); // 记录操作历史 - 成功 - String resultMessage = String.format("%s成功上传并导入开奖数据文件:%s,导入结果:%s", userName, fileName, message); - operationHistoryService.recordOperation(userId, "开奖数据导入", 1, "成功", resultMessage); + String resultMessage = String.format("%s成功上传并导入双色球开奖数据文件:%s,导入结果:%s", userName, fileName, message); + operationHistoryService.recordOperation(userId, "双色球开奖数据导入", 1, "成功", resultMessage); return ResultUtils.success(message); } catch (Exception e) { log.error("开奖数据文件导入失败,文件名:{},错误:{}", fileName, e.getMessage(), e); // 记录操作历史 - 失败 - String resultMessage = String.format("%s开奖数据文件导入失败:%s,文件名:%s,错误原因:%s", userName, fileName, fileName, e.getMessage()); - operationHistoryService.recordOperation(userId, "开奖数据导入", 1, "失败", resultMessage); + String resultMessage = String.format("%s双色球开奖数据文件导入失败:%s,文件名:%s,错误原因:%s", userName, fileName, fileName, e.getMessage()); + operationHistoryService.recordOperation(userId, "双色球开奖数据导入", 1, "失败", resultMessage); - return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "开奖数据文件导入失败:" + e.getMessage()); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "双色球开奖数据文件导入失败:" + e.getMessage()); } } @@ -130,18 +130,18 @@ public class ExcelImportController { String message = excelImportService.appendLotteryDrawsFile(file); // 记录操作历史 - 成功 - String resultMessage = String.format("%s成功追加导入开奖数据文件:%s,导入结果:%s", userName, fileName, message); - operationHistoryService.recordOperation(userId, "开奖数据追加导入", 1, "成功", resultMessage); + String resultMessage = String.format("%s成功追加导入双色球开奖数据文件:%s,导入结果:%s", userName, fileName, message); + operationHistoryService.recordOperation(userId, "双色球开奖数据追加导入", 1, "成功", resultMessage); return ResultUtils.success(message); } catch (Exception e) { log.error("开奖数据追加导入失败,文件名:{},错误:{}", fileName, e.getMessage(), e); // 记录操作历史 - 失败 - String resultMessage = String.format("%s开奖数据追加导入失败:%s,文件名:%s,错误原因:%s", userName, fileName, fileName, e.getMessage()); - operationHistoryService.recordOperation(userId, "开奖数据追加导入", 1, "失败", resultMessage); + String resultMessage = String.format("%s双色球开奖数据追加导入失败:%s,文件名:%s,错误原因:%s", userName, fileName, fileName, e.getMessage()); + operationHistoryService.recordOperation(userId, "双色球开奖数据追加导入", 1, "失败", resultMessage); - return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "开奖数据追加导入失败:" + e.getMessage()); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "双色球开奖数据追加导入失败:" + e.getMessage()); } } diff --git a/src/main/java/com/xy/xyaicpzs/controller/JtDltController.java b/src/main/java/com/xy/xyaicpzs/controller/JtDltController.java new file mode 100644 index 0000000..6b5064c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/JtDltController.java @@ -0,0 +1,398 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.vo.DLTFirstStepResultVO; +import com.xy.xyaicpzs.domain.vo.DLTSecondStepResultVO; +import com.xy.xyaicpzs.domain.vo.DLTThirdStepResultVO; +import com.xy.xyaicpzs.domain.vo.DLTFourthStepResultVO; +import com.xy.xyaicpzs.jt.jtdlt.FirstFrontBallAnalysis; +import com.xy.xyaicpzs.jt.jtdlt.FollowFrontBallAnalysis; +import com.xy.xyaicpzs.jt.jtdlt.BackBallAnalysis; +import com.xy.xyaicpzs.jt.jtdlt.FollowBackendBallAnalysis; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/jtdlt/analysis") +@Tag(name = "精推大乐透分析", description = "精推大乐透分析算法API") +public class JtDltController { + + @Autowired + private FirstFrontBallAnalysis firstFrontBallAnalysis; + + @Autowired + private FollowFrontBallAnalysis followFrontBallAnalysis; + + @Autowired + private BackBallAnalysis backBallAnalysis; + + @Autowired + private FollowBackendBallAnalysis followBackendBallAnalysis; + + /** + * 精推大乐透第一步分析 + */ + @PostMapping("/first-step") + @Operation(summary = "精推大乐透第一步分析", description = "根据前区5个球号和后区2个球号进行第一步分析") + public ApiResponse firstStepAnalysis( + @Parameter(description = "策略级别(H-高位/M-中位/L-低位)", required = true) + @RequestParam String level, + @Parameter(description = "前区球号列表(逗号分隔,如:1,5,12,18,22)", required = true) + @RequestParam String frontBalls, + @Parameter(description = "后区球号列表(逗号分隔,如:3,7)", required = true) + @RequestParam String backBalls) { + + try { + // 参数验证 + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "策略级别必须是H/M/L之一"); + } + + // 解析前区球号 + String[] frontBallArray = frontBalls.split(","); + if (frontBallArray.length != 5) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "前区球号必须为5个"); + } + + List frontBallList = java.util.Arrays.stream(frontBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 35) { + throw new IllegalArgumentException("前区球号范围应为1-35"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("前区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + // 解析后区球号 + String[] backBallArray = backBalls.split(","); + if (backBallArray.length != 2) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "后区球号必须为2个"); + } + + List backBallList = java.util.Arrays.stream(backBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 12) { + throw new IllegalArgumentException("后区球号范围应为1-12"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("后区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + log.info("精推大乐透第一步分析请求:策略={}, 前区={}, 后区={}", level, frontBallList, backBallList); + + // 调用分析算法 + DLTFirstStepResultVO result = firstFrontBallAnalysis.analyze(level, frontBallList, backBallList); + + log.info("精推大乐透第一步分析完成,返回{}个结果", result.getResults().size()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("精推大乐透第一步分析失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "分析失败:" + e.getMessage()); + } + } + + /** + * 精推大乐透第二步分析 + */ + @PostMapping("/second-step") + @Operation(summary = "精推大乐透第二步分析", description = "根据上期前区5个球号、上期后区2个球号和本期首球进行第二步分析") + public ApiResponse secondStepAnalysis( + @Parameter(description = "策略级别(H-高位/M-中位/L-低位)", required = true) + @RequestParam String level, + @Parameter(description = "上期前区球号列表(逗号分隔,如:1,5,12,18,22)", required = true) + @RequestParam String previousFrontBalls, + @Parameter(description = "上期后区球号列表(逗号分隔,如:3,7)", required = true) + @RequestParam String previousBackBalls, + @Parameter(description = "本期首球号码", required = true) + @RequestParam Integer currentFirstBall) { + + try { + // 参数验证 + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "策略级别必须是H/M/L之一"); + } + + if (currentFirstBall == null || currentFirstBall < 1 || currentFirstBall > 35) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "本期首球号码范围应为1-35"); + } + + // 解析上期前区球号 + String[] previousFrontBallArray = previousFrontBalls.split(","); + if (previousFrontBallArray.length != 5) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "上期前区球号必须为5个"); + } + + List previousFrontBallList = java.util.Arrays.stream(previousFrontBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 35) { + throw new IllegalArgumentException("上期前区球号范围应为1-35"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("上期前区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + // 解析上期后区球号 + String[] previousBackBallArray = previousBackBalls.split(","); + if (previousBackBallArray.length != 2) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "上期后区球号必须为2个"); + } + + List previousBackBallList = java.util.Arrays.stream(previousBackBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 12) { + throw new IllegalArgumentException("上期后区球号范围应为1-12"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("上期后区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + log.info("精推大乐透第二步分析请求:策略={}, 上期前区={}, 上期后区={}, 本期首球={}", + level, previousFrontBallList, previousBackBallList, currentFirstBall); + + // 调用分析算法 + DLTSecondStepResultVO result = followFrontBallAnalysis.analyze(level, previousFrontBallList, + previousBackBallList, currentFirstBall); + + log.info("精推大乐透第二步分析完成,返回{}个结果", result.getResults().size()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("精推大乐透第二步分析失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "分析失败:" + e.getMessage()); + } + } + + /** + * 精推大乐透第三步分析 + */ + @PostMapping("/third-step") + @Operation(summary = "精推大乐透第三步分析", description = "根据上期前区5个球号、上期后区2个球号和本期前区5个球号进行第三步分析") + public ApiResponse thirdStepAnalysis( + @Parameter(description = "策略级别(H-高位/M-中位/L-低位)", required = true) + @RequestParam String level, + @Parameter(description = "上期前区球号列表(逗号分隔,如:1,5,12,18,22)", required = true) + @RequestParam String previousFrontBalls, + @Parameter(description = "上期后区球号列表(逗号分隔,如:3,7)", required = true) + @RequestParam String previousBackBalls, + @Parameter(description = "本期前区球号列表(逗号分隔,如:2,9,16,23,29)", required = true) + @RequestParam String currentFrontBalls) { + + try { + // 参数验证 + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "策略级别必须是H/M/L之一"); + } + + // 解析上期前区球号 + String[] previousFrontBallArray = previousFrontBalls.split(","); + if (previousFrontBallArray.length != 5) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "上期前区球号必须为5个"); + } + + List previousFrontBallList = java.util.Arrays.stream(previousFrontBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 35) { + throw new IllegalArgumentException("上期前区球号范围应为1-35"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("上期前区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + // 解析上期后区球号 + String[] previousBackBallArray = previousBackBalls.split(","); + if (previousBackBallArray.length != 2) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "上期后区球号必须为2个"); + } + + List previousBackBallList = java.util.Arrays.stream(previousBackBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 12) { + throw new IllegalArgumentException("上期后区球号范围应为1-12"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("上期后区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + // 解析本期前区球号 + String[] currentFrontBallArray = currentFrontBalls.split(","); + if (currentFrontBallArray.length != 5) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "本期前区球号必须为5个"); + } + + List currentFrontBallList = java.util.Arrays.stream(currentFrontBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 35) { + throw new IllegalArgumentException("本期前区球号范围应为1-35"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("本期前区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + log.info("精推大乐透第三步分析请求:策略={}, 上期前区={}, 上期后区={}, 本期前区={}", + level, previousFrontBallList, previousBackBallList, currentFrontBallList); + + // 调用分析算法 + DLTThirdStepResultVO result = backBallAnalysis.analyze(level, previousFrontBallList, + previousBackBallList, currentFrontBallList); + + log.info("精推大乐透第三步分析完成,返回{}个结果", result.getResults().size()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("精推大乐透第三步分析失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "分析失败:" + e.getMessage()); + } + } + + /** + * 精推大乐透第四步分析 + */ + @PostMapping("/fourth-step") + @Operation(summary = "精推大乐透第四步分析", description = "根据上期前区5个球号、上期后区2个球号、本期前区5个球号和本期后区首球进行第四步分析") + public ApiResponse fourthStepAnalysis( + @Parameter(description = "策略级别(H-高位/M-中位/L-低位)", required = true) + @RequestParam String level, + @Parameter(description = "上期前区球号列表(逗号分隔,如:1,5,12,18,22)", required = true) + @RequestParam String previousFrontBalls, + @Parameter(description = "上期后区球号列表(逗号分隔,如:3,7)", required = true) + @RequestParam String previousBackBalls, + @Parameter(description = "本期前区球号列表(逗号分隔,如:2,9,16,23,29)", required = true) + @RequestParam String currentFrontBalls, + @Parameter(description = "本期后区首球号码", required = true) + @RequestParam Integer currentBackFirstBall) { + + try { + // 参数验证 + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "策略级别必须是H/M/L之一"); + } + + if (currentBackFirstBall == null || currentBackFirstBall < 1 || currentBackFirstBall > 12) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "本期后区首球号码范围应为1-12"); + } + + // 解析上期前区球号 + String[] previousFrontBallArray = previousFrontBalls.split(","); + if (previousFrontBallArray.length != 5) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "上期前区球号必须为5个"); + } + + List previousFrontBallList = java.util.Arrays.stream(previousFrontBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 35) { + throw new IllegalArgumentException("上期前区球号范围应为1-35"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("上期前区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + // 解析上期后区球号 + String[] previousBackBallArray = previousBackBalls.split(","); + if (previousBackBallArray.length != 2) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "上期后区球号必须为2个"); + } + + List previousBackBallList = java.util.Arrays.stream(previousBackBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 12) { + throw new IllegalArgumentException("上期后区球号范围应为1-12"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("上期后区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + // 解析本期前区球号 + String[] currentFrontBallArray = currentFrontBalls.split(","); + if (currentFrontBallArray.length != 5) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "本期前区球号必须为5个"); + } + + List currentFrontBallList = java.util.Arrays.stream(currentFrontBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 35) { + throw new IllegalArgumentException("本期前区球号范围应为1-35"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("本期前区球号格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + log.info("精推大乐透第四步分析请求:策略={}, 上期前区={}, 上期后区={}, 本期前区={}, 本期后区首球={}", + level, previousFrontBallList, previousBackBallList, currentFrontBallList, currentBackFirstBall); + + // 调用分析算法 + DLTFourthStepResultVO result = followBackendBallAnalysis.analyze(level, previousFrontBallList, + previousBackBallList, currentFrontBallList, currentBackFirstBall); + + log.info("精推大乐透第四步分析完成,返回{}个结果", result.getResults().size()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("精推大乐透第四步分析失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "分析失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/controller/JtSsqCotroller.java b/src/main/java/com/xy/xyaicpzs/controller/JtSsqCotroller.java new file mode 100644 index 0000000..e62090b --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/controller/JtSsqCotroller.java @@ -0,0 +1,239 @@ +package com.xy.xyaicpzs.controller; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.vo.SSQFirstStepResultVO; +import com.xy.xyaicpzs.domain.vo.SSQSecondStepResultVO; +import com.xy.xyaicpzs.domain.vo.SSQThirdStepResultVO; +import com.xy.xyaicpzs.jt.jtssq.FirstBallAnalysis; +import com.xy.xyaicpzs.jt.jtssq.FollowBallAnalysis; +import com.xy.xyaicpzs.jt.jtssq.BlueBallAnalysis; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RestController +@RequestMapping("/jtssq/analysis") +@Tag(name = "精推双色球分析", description = "精推双色球三步分析算法API") +public class JtSsqCotroller { + + @Autowired + private FirstBallAnalysis firstBallAnalysis; + + @Autowired + private FollowBallAnalysis followBallAnalysis; + + @Autowired + private BlueBallAnalysis blueBallAnalysis; + + /** + * 精推双色球第一步分析 + */ + @PostMapping("/first-step") + @Operation(summary = "精推双色球第一步分析", description = "根据红球和蓝球号码进行第一步分析") + public ApiResponse firstStepAnalysis( + @Parameter(description = "策略级别(H-高位/M-中位/L-低位)", required = true) + @RequestParam String level, + @Parameter(description = "红球号码列表(逗号分隔,如:1,5,12,18,22,30)", required = true) + @RequestParam String redBalls, + @Parameter(description = "蓝球号码", required = true) + @RequestParam Integer blueBall) { + + try { + // 参数验证 + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "策略级别必须是H/M/L之一"); + } + + if (blueBall == null || blueBall < 1 || blueBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "蓝球号码范围应为1-16"); + } + + // 解析红球号码 + String[] redBallArray = redBalls.split(","); + if (redBallArray.length != 6) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "红球号码必须为6个"); + } + + List redBallList = java.util.Arrays.stream(redBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 33) { + throw new IllegalArgumentException("红球号码范围应为1-33"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("红球号码格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + log.info("精推双色球第一步分析请求:策略={}, 红球={}, 蓝球={}", level, redBallList, blueBall); + + // 调用分析算法 + SSQFirstStepResultVO result = firstBallAnalysis.analyze(level, redBallList, blueBall); + + log.info("精推双色球第一步分析完成,返回{}个结果", result.getResults().size()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("精推双色球第一步分析失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "分析失败:" + e.getMessage()); + } + } + + /** + * 精推双色球第二步分析 + */ + @PostMapping("/second-step") + @Operation(summary = "精推双色球第二步分析", description = "根据红球、蓝球和下期首球号码进行第二步分析") + public ApiResponse secondStepAnalysis( + @Parameter(description = "策略级别(H-高位/M-中位/L-低位)", required = true) + @RequestParam String level, + @Parameter(description = "红球号码列表(逗号分隔,如:1,5,12,18,22,30)", required = true) + @RequestParam String redBalls, + @Parameter(description = "蓝球号码", required = true) + @RequestParam Integer blueBall, + @Parameter(description = "下期首球号码", required = true) + @RequestParam Integer nextFirstBall) { + + try { + // 参数验证 + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "策略级别必须是H/M/L之一"); + } + + if (blueBall == null || blueBall < 1 || blueBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "蓝球号码范围应为1-16"); + } + + if (nextFirstBall == null || nextFirstBall < 1 || nextFirstBall > 33) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "下期首球号码范围应为1-33"); + } + + // 解析红球号码 + String[] redBallArray = redBalls.split(","); + if (redBallArray.length != 6) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "红球号码必须为6个"); + } + + List redBallList = java.util.Arrays.stream(redBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 33) { + throw new IllegalArgumentException("红球号码范围应为1-33"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("红球号码格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + log.info("精推双色球第二步分析请求:策略={}, 红球={}, 蓝球={}, 下期首球={}", + level, redBallList, blueBall, nextFirstBall); + + // 调用分析算法 + SSQSecondStepResultVO result = followBallAnalysis.analyze(level, redBallList, blueBall, nextFirstBall); + + log.info("精推双色球第二步分析完成,返回{}个结果", result.getResults().size()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("精推双色球第二步分析失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "分析失败:" + e.getMessage()); + } + } + + /** + * 精推双色球第三步分析 + */ + @PostMapping("/third-step") + @Operation(summary = "精推双色球第三步分析", description = "根据红球、蓝球和下期红球号码进行第三步分析") + public ApiResponse thirdStepAnalysis( + @Parameter(description = "策略级别(H-高位/M-中位/L-低位)", required = true) + @RequestParam String level, + @Parameter(description = "红球号码列表(逗号分隔,如:1,5,12,18,22,30)", required = true) + @RequestParam String redBalls, + @Parameter(description = "蓝球号码", required = true) + @RequestParam Integer blueBall, + @Parameter(description = "下期红球号码列表(逗号分隔,如:2,9,16,23,29,32)", required = true) + @RequestParam String nextRedBalls) { + + try { + // 参数验证 + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "策略级别必须是H/M/L之一"); + } + + if (blueBall == null || blueBall < 1 || blueBall > 16) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "蓝球号码范围应为1-16"); + } + + // 解析红球号码 + String[] redBallArray = redBalls.split(","); + if (redBallArray.length != 6) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "红球号码必须为6个"); + } + + List redBallList = java.util.Arrays.stream(redBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 33) { + throw new IllegalArgumentException("红球号码范围应为1-33"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("红球号码格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + // 解析下期红球号码 + String[] nextRedBallArray = nextRedBalls.split(","); + if (nextRedBallArray.length != 6) { + return ResultUtils.error(ErrorCode.PARAMS_ERROR, "下期红球号码必须为6个"); + } + + List nextRedBallList = java.util.Arrays.stream(nextRedBallArray) + .map(s -> { + try { + int ball = Integer.parseInt(s.trim()); + if (ball < 1 || ball > 33) { + throw new IllegalArgumentException("下期红球号码范围应为1-33"); + } + return ball; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("下期红球号码格式错误"); + } + }) + .collect(java.util.stream.Collectors.toList()); + + log.info("精推双色球第三步分析请求:策略={}, 红球={}, 蓝球={}, 下期红球={}", + level, redBallList, blueBall, nextRedBallList); + + // 调用分析算法 + SSQThirdStepResultVO result = blueBallAnalysis.analyze(level, redBallList, blueBall, nextRedBallList); + + log.info("精推双色球第三步分析完成,返回{}个结果", result.getResults().size()); + + return ResultUtils.success(result); + + } catch (Exception e) { + log.error("精推双色球第三步分析失败", e); + return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "分析失败:" + e.getMessage()); + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/controller/SpeechRecognitionController.java b/src/main/java/com/xy/xyaicpzs/controller/SpeechRecognitionController.java index ee5b360..58a987b 100644 --- a/src/main/java/com/xy/xyaicpzs/controller/SpeechRecognitionController.java +++ b/src/main/java/com/xy/xyaicpzs/controller/SpeechRecognitionController.java @@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; diff --git a/src/main/java/com/xy/xyaicpzs/controller/UserController.java b/src/main/java/com/xy/xyaicpzs/controller/UserController.java index c09e0c1..76d015c 100644 --- a/src/main/java/com/xy/xyaicpzs/controller/UserController.java +++ b/src/main/java/com/xy/xyaicpzs/controller/UserController.java @@ -9,11 +9,12 @@ import com.xy.xyaicpzs.common.requset.VipCodeActivateRequest; import com.xy.xyaicpzs.common.response.ApiResponse; import com.xy.xyaicpzs.domain.dto.user.*; import com.xy.xyaicpzs.domain.entity.User; -import com.xy.xyaicpzs.domain.vo.UserVO; +import com.xy.xyaicpzs.domain.vo.*; import com.xy.xyaicpzs.exception.BusinessException; import com.xy.xyaicpzs.service.UserService; import com.xy.xyaicpzs.service.VipCodeService; import com.xy.xyaicpzs.service.SmsService; +import com.xy.xyaicpzs.util.UserAuthValidator; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; @@ -23,6 +24,8 @@ import org.springframework.web.bind.annotation.*; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,6 +48,9 @@ public class UserController { @Resource private SmsService smsService; + + @Resource + private UserAuthValidator userAuthValidator; // region 登录相关 @@ -117,6 +123,12 @@ public class UserController { @PostMapping("/add") @Operation(summary = "创建用户", description = "管理员创建用户") public ApiResponse addUser(@RequestBody UserAddRequest userAddRequest, HttpServletRequest request) { + // 验证超级管理员权限 + ApiResponse authResult = userAuthValidator.validateSuperAdminAuth(request); + if (authResult != null) { + return authResult; + } + if (userAddRequest == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } @@ -169,6 +181,12 @@ public class UserController { @Operation(summary = "修改用户状态", description = "管理员修改用户状态(正常/封禁)") public ApiResponse updateUserStatus(@RequestBody UserStatusUpdateRequest userStatusUpdateRequest, HttpServletRequest request) { + // 验证超级管理员权限 + ApiResponse authResult = userAuthValidator.validateSuperAdminAuth(request); + if (authResult != null) { + return authResult; + } + if (userStatusUpdateRequest == null || userStatusUpdateRequest.getId() == null || userStatusUpdateRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户ID不正确"); @@ -182,12 +200,6 @@ public class UserController { throw new BusinessException(ErrorCode.PARAMS_ERROR, "状态值不正确,应为0(正常)或1(封禁)"); } - // 确认操作人员是否为管理员 - User loginUser = userService.getLoginUser(request); - if (!userService.isAdmin(loginUser)) { - throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无管理员权限"); - } - // 检查目标用户是否存在 User user = userService.getById(id); if (user == null) { @@ -214,6 +226,12 @@ public class UserController { @PostMapping("/delete") @Operation(summary = "删除用户", description = "管理员删除用户") public ApiResponse deleteUser(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) { + // 验证超级管理员权限 + ApiResponse authResult = userAuthValidator.validateSuperAdminAuth(request); + if (authResult != null) { + return authResult; + } + if (deleteRequest == null || deleteRequest.getId() <= 0) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } @@ -231,10 +249,25 @@ public class UserController { @PostMapping("/update") @Operation(summary = "更新用户", description = "更新用户信息") public ApiResponse updateUser(@RequestBody UserUpdateRequest userUpdateRequest, HttpServletRequest request) { + // 验证登录权限 + ApiResponse authResult = userAuthValidator.validateLoginAuth(request); + if (authResult != null) { + return authResult; + } + if (userUpdateRequest == null || userUpdateRequest.getId() == null) { throw new BusinessException(ErrorCode.PARAMS_ERROR); } + // 获取当前登录用户 + User currentUser = userAuthValidator.getCurrentUser(request); + Long targetUserId = userUpdateRequest.getId(); + + // 权限检查:用户只能修改自己的信息,管理员可以修改任何用户 + if (!currentUser.getId().equals(targetUserId) && !userAuthValidator.isAdmin(currentUser)) { + throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限修改其他用户信息"); + } + // 参数校验 String userPassword = userUpdateRequest.getUserPassword(); String password = userUpdateRequest.getPassword(); @@ -574,5 +607,181 @@ public class UserController { return response; } + /** + * 检查当前登录用户会员是否过期 + * + * @param request HTTP请求 + * @return 是否过期,true-已过期,false-未过期 + */ + @GetMapping("/check-vip-expire") + @Operation(summary = "检查会员是否过期", description = "检查当前登录用户的会员时间是否过期") + public ApiResponse checkVipExpire(HttpServletRequest request) { + // 获取当前登录用户 + User user = userService.getLoginUser(request); + +// // 检查是否为会员 +// if (user.getIsVip() == null || user.getIsVip() != 1) { +// // 非会员直接返回已过期 +// return ResultUtils.success(true); +// } + + // 检查会员到期时间 + Date vipExpire = user.getVipExpire(); + if (vipExpire == null) { + // 会员到期时间为空,视为已过期 + return ResultUtils.success(true); + } + + // 比较当前时间与到期时间 + Date currentTime = new Date(); + boolean isExpired = currentTime.after(vipExpire); + + return ResultUtils.success(isExpired); + } + + /** + * 根据时间段获取新增用户统计 + * + * @param startDate 开始日期 (格式: yyyy-MM-dd) + * @param endDate 结束日期 (格式: yyyy-MM-dd) + * @param request HTTP请求 + * @return 新增用户统计数据 + */ + @GetMapping("/statistics/new-users") + @Operation(summary = "获取新增用户统计", description = "管理员根据时间段获取新增用户统计数据") + public ApiResponse getNewUsersStatistics( + @RequestParam String startDate, + @RequestParam String endDate, + HttpServletRequest request) { + + // 验证管理员权限 + ApiResponse authResult = userAuthValidator.validateAdminAuth(request); + if (authResult != null) { + return authResult; + } + + if (StringUtils.isAnyBlank(startDate, endDate)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "开始日期和结束日期不能为空"); + } + + UserStatisticsVO result = userService.getNewUsersStatistics(startDate, endDate); + return ResultUtils.success(result); + } + + /** + * 根据时间段获取新增会员统计 + * + * @param startDate 开始日期 (格式: yyyy-MM-dd) + * @param endDate 结束日期 (格式: yyyy-MM-dd) + * @param request HTTP请求 + * @return 新增会员统计数据 + */ + @GetMapping("/statistics/new-vips") + @Operation(summary = "获取新增会员统计", description = "管理员根据时间段获取新增会员统计数据") + public ApiResponse getNewVipsStatistics( + @RequestParam String startDate, + @RequestParam String endDate, + HttpServletRequest request) { + + // 验证管理员权限 + ApiResponse authResult = userAuthValidator.validateAdminAuth(request); + if (authResult != null) { + return authResult; + } + + if (StringUtils.isAnyBlank(startDate, endDate)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "开始日期和结束日期不能为空"); + } + + VipStatisticsVO result = userService.getNewVipsStatistics(startDate, endDate); + return ResultUtils.success(result); + } + + /** + * 根据时间段获取用户注册趋势 + * + * @param startDate 开始日期 (格式: yyyy-MM-dd) + * @param endDate 结束日期 (格式: yyyy-MM-dd) + * @param granularity 时间粒度 (day/week/month) + * @param request HTTP请求 + * @return 用户注册趋势数据 + */ + @GetMapping("/statistics/registration-trend") + @Operation(summary = "获取用户注册趋势", description = "管理员根据时间段和粒度获取用户注册趋势") + public ApiResponse getRegistrationTrend( + @RequestParam String startDate, + @RequestParam String endDate, + @RequestParam(defaultValue = "day") String granularity, + HttpServletRequest request) { + + // 验证管理员权限 + ApiResponse authResult = userAuthValidator.validateAdminAuth(request); + if (authResult != null) { + return authResult; + } + + if (StringUtils.isAnyBlank(startDate, endDate)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "开始日期和结束日期不能为空"); + } + + if (!Arrays.asList("day", "week", "month").contains(granularity)) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "时间粒度只能是day、week或month"); + } + + RegistrationTrendVO result = userService.getRegistrationTrend(startDate, endDate, granularity); + return ResultUtils.success(result); + } + + /** + * 获取即将到期的会员列表 + * + * @param days 提前多少天提醒 (默认7天) + * @param current 当前页 + * @param pageSize 页大小 + * @param request HTTP请求 + * @return 即将到期的会员列表 + */ + @GetMapping("/statistics/expiring-vips") + @Operation(summary = "获取即将到期的会员", description = "管理员获取即将到期的会员列表") + public ApiResponse> getExpiringVips( + @RequestParam(defaultValue = "7") Integer days, + @RequestParam(defaultValue = "1") Long current, + @RequestParam(defaultValue = "10") Long pageSize, + HttpServletRequest request) { + + // 验证管理员权限 + ApiResponse> authResult = userAuthValidator.validateAdminAuth(request); + if (authResult != null) { + return authResult; + } + + if (days <= 0) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "提前天数必须大于0"); + } + + Page result = userService.getExpiringVips(days, current, pageSize); + return ResultUtils.success(result); + } + + /** + * 获取会员状态分布统计 + * + * @param request HTTP请求 + * @return 会员状态分布数据 + */ + @GetMapping("/statistics/vip-distribution") + @Operation(summary = "获取会员状态分布", description = "管理员获取会员状态分布统计") + public ApiResponse getVipDistribution(HttpServletRequest request) { + + // 验证管理员权限 + ApiResponse authResult = userAuthValidator.validateAdminAuth(request); + if (authResult != null) { + return authResult; + } + + VipDistributionVO result = userService.getVipDistribution(); + return ResultUtils.success(result); + } + // endregion } \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/dlt/BackBallPredictor.java b/src/main/java/com/xy/xyaicpzs/dlt/BackBallPredictor.java new file mode 100644 index 0000000..8166369 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/dlt/BackBallPredictor.java @@ -0,0 +1,1247 @@ +package com.xy.xyaicpzs.dlt; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 后区首球预测器 + */ +@Slf4j +@Component +public class BackBallPredictor { + + @Autowired + private D6Mapper d6Mapper; + + @Autowired + private D10Mapper d10Mapper; + + @Autowired + private D11Mapper d11Mapper; + + @Autowired + private DltBackendHistoryTop100Mapper dltBackendHistoryTop100Mapper; + + @Autowired + private DltBackendHistoryTopMapper dltBackendHistoryTopMapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + + @Override + public String toString() { + return "Ball{" + + "ballNumber=" + ballNumber + + ", coefficient=" + coefficient + + ", masterBallNumber=" + masterBallNumber + + '}'; + } + } + + /** + * 后区首球预测结果类 + */ + @Data + public static class BackBallPredictionResult { + private List result; + private String filteringProcess; + + public BackBallPredictionResult(List result, String filteringProcess) { + this.result = result; + this.filteringProcess = filteringProcess; + } + } + + /** + * 后区首球预测(带过程) + * @param level 位置级别(high/middle/low) + * @param nextFrontBalls 下期前区5个号码 + * @param previousFrontBalls 上期前区5个号码 + * @param previousBackBalls 上期后区2个号码 + * @param nextBackBalls 下期后区2个号码 + * @return 预测的4个后区首球号码和筛选过程 + */ + public BackBallPredictionResult predictBackBallWithProcess(String level, List nextFrontBalls, List previousFrontBalls, + List previousBackBalls, List nextBackBalls) { + // 参数验证 + validateInputParams(level, nextFrontBalls, previousFrontBalls, previousBackBalls, nextBackBalls); + + // 存放D6表中对应的系数,用于后续筛选 + Map> d6CoefficientMap = new HashMap<>(); + + // 所有候选球列表 + List allCandidateBalls = new ArrayList<>(); + + // 步骤一:处理下期前区5个号码 + for (Integer nextFrontBall : nextFrontBalls) { + List ballsWithCoefficients; + + if ("high".equalsIgnoreCase(level)) { + ballsWithCoefficients = getHighLevelBallsFromD6(nextFrontBall); + } else if ("middle".equalsIgnoreCase(level)) { + ballsWithCoefficients = getMiddleLevelBallsFromD6(nextFrontBall); + } else { // low + ballsWithCoefficients = getLowLevelBallsFromD6(nextFrontBall); + } + + for (BallWithCoefficient ball : ballsWithCoefficients) { + d6CoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤二:处理上期前区5个号码 + for (Integer previousFrontBall : previousFrontBalls) { + List ballsFromD10 = getTop10BallsFromD10(previousFrontBall); + for (BallWithCoefficient ball : ballsFromD10) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤三:处理上期后区2个号码 + for (Integer previousBackBall : previousBackBalls) { + List ballsFromD11 = getTop10BallsFromD11(previousBackBall); + for (BallWithCoefficient ball : ballsFromD11) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤四:从历史排行表获取前2名 + List top2FromHistoryTop = getTop2FromBackendHistoryTop(); + allCandidateBalls.addAll(top2FromHistoryTop); + + // 步骤五:从百期排行表获取前2名 + List top2FromHistoryTop100 = getTop2FromBackendHistoryTop100(); + allCandidateBalls.addAll(top2FromHistoryTop100); + + return selectFinal4BallsWithProcess(allCandidateBalls, d6CoefficientMap); + } + + /** + * 后区首球预测主方法 + * @param level 位置级别(high/middle/low) + * @param nextFrontBalls 下期前区5个号码 + * @param previousFrontBalls 上期前区5个号码 + * @param previousBackBalls 上期后区2个号码 + * @param nextBackBalls 下期后区2个号码 + * @return 预测的4个后区首球号码 + */ + public List predictBackBall(String level, List nextFrontBalls, List previousFrontBalls, + List previousBackBalls, List nextBackBalls) { + // 参数验证 + validateInputParams(level, nextFrontBalls, previousFrontBalls, previousBackBalls, nextBackBalls); + + // 存放D6表中对应的系数,用于后续筛选 + Map> d6CoefficientMap = new HashMap<>(); + + // 所有候选球列表 + List allCandidateBalls = new ArrayList<>(); + + // 步骤一:处理下期前区5个号码 + for (Integer nextFrontBall : nextFrontBalls) { + List ballsWithCoefficients; + + if ("high".equalsIgnoreCase(level)) { + ballsWithCoefficients = getHighLevelBallsFromD6(nextFrontBall); + } else if ("middle".equalsIgnoreCase(level)) { + ballsWithCoefficients = getMiddleLevelBallsFromD6(nextFrontBall); + } else { // low + ballsWithCoefficients = getLowLevelBallsFromD6(nextFrontBall); + } + + for (BallWithCoefficient ball : ballsWithCoefficients) { + d6CoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤二:处理上期前区5个号码 + for (Integer prevFrontBall : previousFrontBalls) { + List prevFrontBallCandidates = getTop10BallsFromD10(prevFrontBall); + for (BallWithCoefficient ball : prevFrontBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤三:从历史排行表获取最大值向下的2个球 + List top2HistoryTop = getTop2FromBackendHistoryTop(); + allCandidateBalls.addAll(top2HistoryTop); + + // 步骤四:从百期排行表获取最大值向下的2个球 + List top2HistoryTop100 = getTop2FromBackendHistoryTop100(); + allCandidateBalls.addAll(top2HistoryTop100); + + // 步骤五:处理上期后区2个号码 + for (Integer prevBackBall : previousBackBalls) { + List prevBackBallCandidates = getTop10BallsFromD11(prevBackBall); + for (BallWithCoefficient ball : prevBackBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤六:加入下期后区2个号码 + for (Integer nextBackBall : nextBackBalls) { + allCandidateBalls.add(nextBackBall); + } + + // 最终筛选4个球 + return selectFinal4Balls(allCandidateBalls, d6CoefficientMap); + } + + /** + * 高位策略:从D6表中获取最大值向下的5个球号 + */ + private List getHighLevelBallsFromD6(Integer masterBallNumber) { + List d6List = d6Mapper.selectList( + new LambdaQueryWrapper() + .eq(D6::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d6List)) { + log.warn("No D6 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d6List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 5 && index < d6List.size()) { + double currentCoefficient = d6List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d6List.size() && + d6List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d6List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(5 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 中位策略:从D6表中获取平均值及其附近的5个球号 + */ + private List getMiddleLevelBallsFromD6(Integer masterBallNumber) { + List d6List = d6Mapper.selectList( + new LambdaQueryWrapper() + .eq(D6::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d6List)) { + log.warn("No D6 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d6List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 计算平均系数 + double avgCoefficient = d6List.stream() + .mapToDouble(D6::getCoefficient) + .average() + .orElse(0.0); + + // 找到比平均值大且最接近平均值的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d6List.size() - 1; i >= 0; i--) { + double diff = d6List.get(i).getCoefficient() - avgCoefficient; + if (diff >= 0) { + avgPosition = i; + break; // 找到第一个比平均值大的位置就停止 + } + } + + // 如果没找到大于平均值的系数,取第一个 + if (avgPosition == -1) { + avgPosition = 0; + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d6List.get(avgPosition).getSlaveBallNumber(), + d6List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取2个球号(使用高位策略的毛边处理逻辑) + List upperBalls = getUpperBallsFromAverageD6(d6List, avgPosition, 2, masterBallNumber); + result.addAll(upperBalls); + + // 向下取2个球号(使用低位策略的毛边处理逻辑) + List lowerBalls = getLowerBallsFromAverageD6(d6List, avgPosition, 2, masterBallNumber); + result.addAll(lowerBalls); + + return result; + } + + /** + * 从D6表平均值位置向上取指定数量的球号 + */ + private List getUpperBallsFromAverageD6(List d6List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition - 1; // 从平均值位置的上一个开始 + int addedCount = 0; + + // 从平均值向上遍历(使用高位策略的逻辑) + while (addedCount < needCount && index >= 0) { + double currentCoefficient = d6List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d6List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d6List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向上使用高位策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D6表平均值位置向下取指定数量的球号 + */ + private List getLowerBallsFromAverageD6(List d6List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition + 1; // 从平均值位置的下一个开始 + int addedCount = 0; + + // 从平均值向下遍历(使用低位策略的逻辑) + while (addedCount < needCount && index < d6List.size()) { + double currentCoefficient = d6List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向后收集) + int tempIndex = index; + while (tempIndex < d6List.size() && + d6List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d6List.get(tempIndex).getSlaveBallNumber()); + tempIndex++; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向下使用后区策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 低位策略:从D6表中获取最小值向上5个球号 + */ + private List getLowLevelBallsFromD6(Integer masterBallNumber) { + List d6List = d6Mapper.selectList( + new LambdaQueryWrapper() + .eq(D6::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d6List)) { + log.warn("No D6 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d6List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 找到最小值的位置 + int minPosition = d6List.size() - 1; + + List result = new ArrayList<>(); + int index = minPosition; + int addedCount = 0; + + // 从最小值开始向上遍历 + while (addedCount < 5 && index >= 0) { + double currentCoefficient = d6List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d6List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d6List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needCount = Math.min(5 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D10表中获取最大值向下的10个球号 + */ + private List getTop10BallsFromD10(Integer masterBallNumber) { + List d10List = d10Mapper.selectList( + new LambdaQueryWrapper() + .eq(D10::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d10List)) { + log.warn("No D10 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d10List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 10 && index < d10List.size()) { + double currentCoefficient = d10List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d10List.size() && + d10List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d10List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(10 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D11表中获取最大值向下的10个球号 + */ + private List getTop10BallsFromD11(Integer masterBallNumber) { + List d11List = d11Mapper.selectList( + new LambdaQueryWrapper() + .eq(D11::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d11List)) { + log.warn("No D11 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d11List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 10 && index < d11List.size()) { + double currentCoefficient = d11List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d11List.size() && + d11List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d11List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(10 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 获取后区历史排行表中最大值向下的2位 + */ + private List getTop2FromBackendHistoryTop() { + List historyTopList = dltBackendHistoryTopMapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltBackendHistoryTop::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTopList)) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 2 && index < historyTopList.size()) { + double currentCoefficient = historyTopList.get(index).getActiveCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTopList.size() && + historyTopList.get(index).getActiveCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTopList.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(2 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 获取后区百期排行表中最大值向下的2位 + */ + private List getTop2FromBackendHistoryTop100() { + List historyTop100List = dltBackendHistoryTop100Mapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltBackendHistoryTop100::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTop100List)) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 2 && index < historyTop100List.size()) { + double currentCoefficient = historyTop100List.get(index).getActiveCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTop100List.size() && + historyTop100List.get(index).getActiveCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTop100List.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(2 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 处理多个边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleMultipleBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 1. 先从dlt_backend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop100 record = dltBackendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 按系数降序排序 + List> sortedByTop100 = top100Coefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + + // 按系数分组处理 + int currentIndex = 0; + while (result.size() < selectCount && currentIndex < sortedByTop100.size()) { + Double currentCoefficient = sortedByTop100.get(currentIndex).getValue(); + List sameTop100CoefficientBalls = new ArrayList<>(); + + // 收集相同系数的球号 + while (currentIndex < sortedByTop100.size() && + sortedByTop100.get(currentIndex).getValue().equals(currentCoefficient)) { + sameTop100CoefficientBalls.add(sortedByTop100.get(currentIndex).getKey()); + currentIndex++; + } + + int needFromThisGroup = Math.min(selectCount - result.size(), sameTop100CoefficientBalls.size()); + + if (sameTop100CoefficientBalls.size() == 1 || needFromThisGroup == sameTop100CoefficientBalls.size()) { + // 只有一个球或者需要全部选择 + for (int i = 0; i < needFromThisGroup; i++) { + result.add(sameTop100CoefficientBalls.get(i)); + } + } else { + // 需要进一步筛选,使用dlt_backend_history_top表 + List selectedFromTop = selectFromBackendHistoryTop(sameTop100CoefficientBalls, needFromThisGroup); + result.addAll(selectedFromTop); + } + } + + return result; + } + + /** + * 从dlt_backend_history_top表中选择指定数量的球号 + */ + private List selectFromBackendHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在dlt_backend_history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 处理边界冲突(毛边) + */ + private Integer handleBoundaryConflicts(List candidateBalls) { + if (CollectionUtils.isEmpty(candidateBalls)) { + return null; + } + + if (candidateBalls.size() == 1) { + return candidateBalls.get(0); + } + + // 1. 先从dlt_backend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop100 record = dltBackendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 找出系数最大的球 + Optional> maxTop100Entry = top100Coefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTop100Entry.isPresent()) { + Double maxCoefficient = maxTop100Entry.get().getValue(); + List maxCoefficientBalls = top100Coefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxCoefficientBalls.size() == 1) { + return maxCoefficientBalls.get(0); + } + + // 2. 如果仍有多个球系数相同,从dlt_backend_history_top比较 + Map topCoefficients = new HashMap<>(); + for (Integer ball : maxCoefficientBalls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + Optional> maxTopEntry = topCoefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTopEntry.isPresent()) { + Double maxTopCoefficient = maxTopEntry.get().getValue(); + List maxTopCoefficientBalls = topCoefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxTopCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxTopCoefficientBalls.size() == 1) { + return maxTopCoefficientBalls.get(0); + } + } + } + + // 3. 如果仍无法区分,默认选择第一位 + return candidateBalls.get(0); + } + + /** + * 从所有候选球中筛选最终的4个球(带过程) + * @param allCandidateBalls 所有候选球 + * @param d6CoefficientMap D6表中的系数信息 + * @return 最终选出的4个球和筛选过程 + */ + private BackBallPredictionResult selectFinal4BallsWithProcess(List allCandidateBalls, Map> d6CoefficientMap) { + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>((f1, f2) -> f2.compareTo(f1)); // 降序 + for (Map.Entry entry : ballFrequencyMap.entrySet()) { + frequencyGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + StringBuilder processBuilder = new StringBuilder(); + List resultBalls = new ArrayList<>(); + List directSelected = new ArrayList<>(); + List needFurtherSelection = new ArrayList<>(); + boolean hasSecondarySelection = false; + String selectionSteps = ""; + String detailedCoefficientInfo = ""; + + // 首先确定实际参与筛选的最低频率 + int currentSelected = 0; + int minSelectedFrequency = 0; + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + int frequency = frequencyGroup.getKey(); + int ballsInGroup = frequencyGroup.getValue().size(); + + if (currentSelected + ballsInGroup <= 4) { + // 这个频率组的球全部入选 + currentSelected += ballsInGroup; + minSelectedFrequency = frequency; + } else { + // 这个频率组只有部分球入选 + minSelectedFrequency = frequency; + break; + } + } + + // 生成频率分布描述 + List frequencyDescriptions = new ArrayList<>(); + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + int frequency = frequencyGroup.getKey(); + if (frequency >= minSelectedFrequency) { + List ballsInGroup = new ArrayList<>(frequencyGroup.getValue()); + Collections.sort(ballsInGroup); + String ballsStr = ballsInGroup.stream().map(String::valueOf).collect(Collectors.joining(", ")); + frequencyDescriptions.add(ballsStr + "(出现" + frequency + "次)"); + } + } + processBuilder.append("参与筛选的候选球号按频率分布为").append(String.join(",", frequencyDescriptions)).append(";"); + + // 按频率从高到低处理每个频率组 + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + if (resultBalls.size() >= 4) { + break; + } + + List ballsInGroup = frequencyGroup.getValue(); + int needCount = Math.min(4 - resultBalls.size(), ballsInGroup.size()); + + if (ballsInGroup.size() <= needCount) { + // 如果这个频率组的球数量 <= 需要的数量,全部加入 + resultBalls.addAll(ballsInGroup); + directSelected.addAll(ballsInGroup); + } else { + // 需要从这个频率组中选择部分球号,使用D6系数筛选 + List selectedFromGroup = selectBallsByD6Coefficient(ballsInGroup, d6CoefficientMap, needCount); + resultBalls.addAll(selectedFromGroup); + needFurtherSelection.addAll(ballsInGroup); + hasSecondarySelection = true; + selectionSteps = "通过频率筛选确定部分球号,通过D6系数和筛选确定剩余球号,无需进行百期排位、历史排位"; + + // 生成系数详情信息 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : ballsInGroup) { + List coefficients = d6CoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream().mapToDouble(BallWithCoefficient::getCoefficient).sum(); + ballCoefficientSum.put(ball, sum); + } + + List coefficientDetails = new ArrayList<>(); + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + coefficientDetails.add(entry.getKey() + "(系数和" + String.format("%.2f", entry.getValue()) + ")"); + } + Collections.sort(coefficientDetails); + detailedCoefficientInfo = "D6系数和详情:" + String.join(",", coefficientDetails); + + break; // 只处理需要筛选的第一组 + } + } + + // 生成筛选过程说明 + if (hasSecondarySelection) { + processBuilder.append("无法直接筛选出前4个,其中"); + Collections.sort(directSelected); + String directSelectedStr = directSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(directSelectedStr).append("直接入选,"); + + Collections.sort(needFurtherSelection); + String needFurtherStr = needFurtherSelection.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(needFurtherStr).append("需要进行二次筛选,"); + + List finalSelected = new ArrayList<>(resultBalls); + finalSelected.removeAll(directSelected); + Collections.sort(finalSelected); + String finalSelectedStr = finalSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("最终筛选出").append(finalSelectedStr).append(","); + + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("组成前4个球号:").append(allResultStr).append("。"); + + processBuilder.append("筛选步骤:").append(selectionSteps).append("。"); + if (!detailedCoefficientInfo.isEmpty()) { + processBuilder.append(" ").append(detailedCoefficientInfo).append("。"); + } + } else { + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("直接筛选出前4个球号:").append(allResultStr).append("。"); + } + + // 无论是否有二次筛选,都要显示筛选步骤 + if (!hasSecondarySelection || selectionSteps.isEmpty()) { + processBuilder.append("筛选步骤:通过频率筛选确定所有球号,无需进行D6系数筛选、百期排位、历史排位。"); + } + + return new BackBallPredictionResult(resultBalls, processBuilder.toString()); + } + + /** + * 从所有候选球中筛选最终的4个球 + * @param allCandidateBalls 所有候选球 + * @param d6CoefficientMap D6表中的系数信息 + * @return 最终选出的4个球 + */ + private List selectFinal4Balls(List allCandidateBalls, Map> d6CoefficientMap) { + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 按出现频率分组 + Map> frequencyGroups = new TreeMap<>((f1, f2) -> f2.compareTo(f1)); // 频率从高到低 + for (Map.Entry entry : ballFrequencyMap.entrySet()) { + frequencyGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + List resultBalls = new ArrayList<>(); + + // 按频率组从高到低处理 + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + if (resultBalls.size() >= 4) { + break; + } + + List ballsInGroup = frequencyGroup.getValue(); + int needCount = Math.min(4 - resultBalls.size(), ballsInGroup.size()); + + if (ballsInGroup.size() == 1) { + // 只有一个球号,直接加入 + resultBalls.add(ballsInGroup.get(0)); + } else if (needCount == ballsInGroup.size()) { + // 需要全部球号,全部加入 + resultBalls.addAll(ballsInGroup); + } else { + // 需要从多个球号中选择部分,按D6系数排序 + List selectedBalls = selectBallsByD6Coefficient(ballsInGroup, d6CoefficientMap, needCount); + resultBalls.addAll(selectedBalls); + } + } + + return resultBalls; + } + + /** + * 根据D6系数选择指定数量的球号 + */ + private List selectBallsByD6Coefficient(List candidateBalls, Map> d6CoefficientMap, int needCount) { + // 计算每个球号的D6系数总和 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : candidateBalls) { + List coefficients = d6CoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSum.put(ball, sum); + } + + // 按系数从大到小排序 + List> sortedByCoefficient = ballCoefficientSum.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < needCount && index < sortedByCoefficient.size()) { + double currentCoefficient = sortedByCoefficient.get(index).getValue(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < sortedByCoefficient.size() && + sortedByCoefficient.get(index).getValue().equals(currentCoefficient)) { + sameCoefficientBalls.add(sortedByCoefficient.get(index).getKey()); + index++; + } + + // 计算还需要多少个球 + int currentNeedCount = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (currentNeedCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,按百期排行筛选 + List selectedBalls = selectBallsByBackendHistoryRanking(sameCoefficientBalls, currentNeedCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 根据后区历史排行选择指定数量的球号 + */ + private List selectBallsByBackendHistoryRanking(List candidateBalls, int needCount) { + // 先尝试按百期排行筛选 + Map top100Rankings = getBackendHistoryTop100Rankings(candidateBalls); + + List> sortedByTop100 = top100Rankings.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < needCount && index < sortedByTop100.size()) { + double currentCoefficient = sortedByTop100.get(index).getValue(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < sortedByTop100.size() && + sortedByTop100.get(index).getValue().equals(currentCoefficient)) { + sameCoefficientBalls.add(sortedByTop100.get(index).getKey()); + index++; + } + + // 计算还需要多少个球 + int currentNeedCount = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (currentNeedCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,按历史排行筛选 + Map topRankings = getBackendHistoryTopRankings(sameCoefficientBalls); + + List> sortedByTop = topRankings.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + for (int i = 0; i < Math.min(currentNeedCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + addedCount++; + } + } + } + + return result; + } + + /** + * 获取球号在后区百期排行表中的系数 + */ + private Map getBackendHistoryTop100Rankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltBackendHistoryTop100 record = dltBackendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop100::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 获取球号在后区历史排行表中的系数 + */ + private Map getBackendHistoryTopRankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 验证输入参数 + */ + private void validateInputParams(String level, List nextFrontBalls, List previousFrontBalls, + List previousBackBalls, List nextBackBalls) { + if (!"high".equalsIgnoreCase(level) && !"middle".equalsIgnoreCase(level) && !"low".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是high/middle/low之一"); + } + + if (CollectionUtils.isEmpty(nextFrontBalls) || nextFrontBalls.size() != 5) { + throw new IllegalArgumentException("下期前区号码必须为5个"); + } + + if (CollectionUtils.isEmpty(previousFrontBalls) || previousFrontBalls.size() != 5) { + throw new IllegalArgumentException("上期前区号码必须为5个"); + } + + if (CollectionUtils.isEmpty(previousBackBalls) || previousBackBalls.size() != 2) { + throw new IllegalArgumentException("上期后区号码必须为2个"); + } + + if (CollectionUtils.isEmpty(nextBackBalls) || nextBackBalls.size() != 2) { + throw new IllegalArgumentException("下期后区号码必须为2个"); + } + + for (Integer frontBall : nextFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("前区号码范围应为1-35"); + } + } + + for (Integer frontBall : previousFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("前区号码范围应为1-35"); + } + } + + for (Integer backBall : previousBackBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("后区号码范围应为1-12"); + } + } + + for (Integer backBall : nextBackBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("后区号码范围应为1-12"); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/dlt/FirstBallPredictor.java b/src/main/java/com/xy/xyaicpzs/dlt/FirstBallPredictor.java new file mode 100644 index 0000000..ef213d1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/dlt/FirstBallPredictor.java @@ -0,0 +1,1355 @@ +package com.xy.xyaicpzs.dlt; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 前区首球预测器 + */ +@Slf4j +@Component +public class FirstBallPredictor { + + @Autowired + private D9Mapper d9Mapper; + + @Autowired + private D12Mapper d12Mapper; + + @Autowired + private DltFrontendHistoryTop100Mapper dltFrontendHistoryTop100Mapper; + + @Autowired + private DltFrontendHistoryTopMapper dltFrontendHistoryTopMapper; + + @Autowired + private DltFrontendHistoryAllMapper dltFrontendHistoryAllMapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + + @Override + public String toString() { + return "Ball{" + + "ballNumber=" + ballNumber + + ", coefficient=" + coefficient + + ", masterBallNumber=" + masterBallNumber + + '}'; + } + } + + /** + * 首球预测结果类 + */ + @Data + public static class FirstBallPredictionResult { + private List result; + private String filteringProcess; + + public FirstBallPredictionResult(List result, String filteringProcess) { + this.result = result; + this.filteringProcess = filteringProcess; + } + } + + /** + * 首球预测主方法(带筛选过程) + * @param level 位置级别(high/middle/low) + * @param redBalls 前区号码 + * @param blueBalls 后区号码 + * @return 预测结果和筛选过程 + */ + public FirstBallPredictionResult predictFirstBallWithProcess(String level, List redBalls, List blueBalls) { + // 参数验证 + validateInputParams(level, redBalls, blueBalls); + + // 根据不同级别处理 + Map> ballWithCoefficientMap = new HashMap<>(); + + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据5个前区号码获取候选球 + for (Integer redBall : redBalls) { + List ballsWithCoefficients; + if ("high".equalsIgnoreCase(level)) { + ballsWithCoefficients = getHighLevelBallsWithCoefficients(redBall); + } else if ("middle".equalsIgnoreCase(level)) { + ballsWithCoefficients = getMiddleLevelBallsWithCoefficients(redBall); + } else { // low + ballsWithCoefficients = getLowLevelBallsWithCoefficients(redBall); + } + + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballWithCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从历史排行表获取前3个 + List top3HistoryTop = getTop3FromHistoryTop(); + allCandidateBalls.addAll(top3HistoryTop); + + // Step 3: 从百期排行表获取前3个 + List top3HistoryTop100 = getTop3FromHistoryTop100(); + allCandidateBalls.addAll(top3HistoryTop100); + + // Step 4: 从后区号码获取候选球 + for (Integer blueBall : blueBalls) { + List blueballsWithCoefficients = getTop17FromD12WithCoefficients(blueBall, level); + for (BallWithCoefficient ball : blueballsWithCoefficients) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 最终筛选12个球 + return selectFinal12Balls(allCandidateBalls, ballWithCoefficientMap); + } + + /** + * 首球预测主方法(原版本,保持兼容性) + * @param level 位置级别(high/middle/low) + * @param redBalls 前区号码 + * @param blueBalls 后区号码 + * @return 预测的12个首球号码 + */ + public List predictFirstBall(String level, List redBalls, List blueBalls) { + // 参数验证 + validateInputParams(level, redBalls, blueBalls); + + // 根据不同级别处理 + List result; + Map> ballWithCoefficientMap = new HashMap<>(); + + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据5个前区号码获取候选球 + for (Integer redBall : redBalls) { + List ballsWithCoefficients; + if ("high".equalsIgnoreCase(level)) { + ballsWithCoefficients = getHighLevelBallsWithCoefficients(redBall); + } else if ("middle".equalsIgnoreCase(level)) { + ballsWithCoefficients = getMiddleLevelBallsWithCoefficients(redBall); + } else { // low + ballsWithCoefficients = getLowLevelBallsWithCoefficients(redBall); + } + + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballWithCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从历史排行表获取前3个 + List top3HistoryTop = getTop3FromHistoryTop(); + allCandidateBalls.addAll(top3HistoryTop); + + // Step 3: 从百期排行表获取前3个 + List top3HistoryTop100 = getTop3FromHistoryTop100(); + allCandidateBalls.addAll(top3HistoryTop100); + + // Step 4: 从后区号码获取候选球 + for (Integer blueBall : blueBalls) { + List blueballsWithCoefficients = getTop17FromD12WithCoefficients(blueBall, level); + for (BallWithCoefficient ball : blueballsWithCoefficients) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 最终筛选12个球 + FirstBallPredictionResult predictionResult = selectFinal12Balls(allCandidateBalls, ballWithCoefficientMap); + result = predictionResult.getResult(); + + return result; + } + + /** + * 高位策略:获取D9表中系数最大的前17位球号 + */ + private List getHighLevelBallsWithCoefficients(Integer masterBallNumber) { + List d9List = d9Mapper.selectList( + new LambdaQueryWrapper() + .eq(D9::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d9List)) { + log.warn("No D9 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d9List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 17 && index < d9List.size()) { + double currentCoefficient = d9List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d9List.size() && + d9List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d9List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(17 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 中位策略:获取D9表中平均值附近的17个球号 + */ + private List getMiddleLevelBallsWithCoefficients(Integer masterBallNumber) { + List d9List = d9Mapper.selectList( + new LambdaQueryWrapper() + .eq(D9::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d9List)) { + log.warn("No D9 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d9List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 计算平均系数 + double avgCoefficient = d9List.stream() + .mapToDouble(D9::getCoefficient) + .average() + .orElse(0.0); + + // 找到比平均值大且最接近平均值的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d9List.size() - 1; i >= 0; i--) { + double diff = d9List.get(i).getCoefficient() - avgCoefficient; + if (diff >= 0) { + avgPosition = i; + break; // 找到第一个比平均值大的位置就停止 + } + } + + // 如果没找到大于平均值的系数,取第一个 + if (avgPosition == -1) { + avgPosition = 0; + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d9List.get(avgPosition).getSlaveBallNumber(), + d9List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取8个球号(使用高位策略的毛边处理逻辑) + List upperBalls = getUpperBallsFromAverage(d9List, avgPosition, 8, masterBallNumber); + result.addAll(upperBalls); + + // 向下取8个球号(使用低位策略的毛边处理逻辑) + List lowerBalls = getLowerBallsFromAverage(d9List, avgPosition, 8, masterBallNumber); + result.addAll(lowerBalls); + + return result; + } + + /** + * 从平均值位置向上取指定数量的球号 + */ + private List getUpperBallsFromAverage(List d9List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition - 1; // 从平均值位置的上一个开始 + int addedCount = 0; + + // 从平均值向上遍历(使用高位策略的逻辑) + while (addedCount < needCount && index >= 0) { + double currentCoefficient = d9List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d9List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d9List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向上使用高位策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从平均值位置向下取指定数量的球号 + */ + private List getLowerBallsFromAverage(List d9List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition + 1; // 从平均值位置的下一个开始 + int addedCount = 0; + + // 从平均值向下遍历(使用低位策略的逻辑) + while (addedCount < needCount && index < d9List.size()) { + double currentCoefficient = d9List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向后收集) + int tempIndex = index; + while (tempIndex < d9List.size() && + d9List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d9List.get(tempIndex).getSlaveBallNumber()); + tempIndex++; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向下使用高位策略,因为是前区) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 低位策略:获取D9表中最小值向上17个球号 + */ + private List getLowLevelBallsWithCoefficients(Integer masterBallNumber) { + List d9List = d9Mapper.selectList( + new LambdaQueryWrapper() + .eq(D9::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d9List)) { + log.warn("No D9 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d9List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 找到最小值的位置 + int minPosition = d9List.size() - 1; + + List result = new ArrayList<>(); + int index = minPosition; + int addedCount = 0; + + // 从最小值开始向上遍历 + while (addedCount < 17 && index >= 0) { + double currentCoefficient = d9List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d9List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d9List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needCount = Math.min(17 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D12表获取前17位球号 + */ + private List getTop17FromD12WithCoefficients(Integer masterBallNumber, String level) { + List d12List = d12Mapper.selectList( + new LambdaQueryWrapper() + .eq(D12::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d12List)) { + log.warn("No D12 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d12List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + + if ("high".equalsIgnoreCase(level)) { + // 高位:取系数最大的前17位 + int index = 0; + int addedCount = 0; + + while (addedCount < 17 && index < d12List.size()) { + double currentCoefficient = d12List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d12List.size() && + d12List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d12List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(17 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + } else if ("middle".equalsIgnoreCase(level)) { + // 中位:计算平均值,取其附近的17个球 + double avgCoefficient = d12List.stream() + .mapToDouble(D12::getCoefficient) + .average() + .orElse(0.0); + + // 找到比平均值大且最接近平均值的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d12List.size() - 1; i >= 0; i--) { + double diff = d12List.get(i).getCoefficient() - avgCoefficient; + if (diff >= 0) { + avgPosition = i; + break; // 找到第一个比平均值大的位置就停止 + } + } + + // 如果没找到大于平均值的系数,取第一个 + if (avgPosition == -1) { + avgPosition = 0; + } + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d12List.get(avgPosition).getSlaveBallNumber(), + d12List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取8个球号(使用高位策略的毛边处理逻辑) + List upperBalls = getUpperBallsFromAverageD12(d12List, avgPosition, 8, masterBallNumber); + result.addAll(upperBalls); + + // 向下取8个球号(使用低位策略的毛边处理逻辑) + List lowerBalls = getLowerBallsFromAverageD12(d12List, avgPosition, 8, masterBallNumber); + result.addAll(lowerBalls); + } else { // low + // 低位:取最小值向上的17个球 + int minPosition = d12List.size() - 1; + + int index = minPosition; + int addedCount = 0; + + // 从最小值开始向上遍历 + while (addedCount < 17 && index >= 0) { + double currentCoefficient = d12List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d12List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d12List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needCount = Math.min(17 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + } + + return result; + } + + /** + * 从D12表平均值位置向上取指定数量的球号 + */ + private List getUpperBallsFromAverageD12(List d12List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition - 1; // 从平均值位置的上一个开始 + int addedCount = 0; + + // 从平均值向上遍历(使用高位策略的逻辑) + while (addedCount < needCount && index >= 0) { + double currentCoefficient = d12List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d12List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d12List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向上使用高位策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D12表平均值位置向下取指定数量的球号 + */ + private List getLowerBallsFromAverageD12(List d12List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition + 1; // 从平均值位置的下一个开始 + int addedCount = 0; + + // 从平均值向下遍历(使用低位策略的逻辑) + while (addedCount < needCount && index < d12List.size()) { + double currentCoefficient = d12List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向后收集) + int tempIndex = index; + while (tempIndex < d12List.size() && + d12List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d12List.get(tempIndex).getSlaveBallNumber()); + tempIndex++; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向下使用前区策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 获取历史排行表中系数最大的前3位 + */ + private List getTop3FromHistoryTop() { + List historyTopList = dltFrontendHistoryTopMapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltFrontendHistoryTop::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTopList)) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int size = Math.min(historyTopList.size(), 3); + + for (int i = 0; i < size; i++) { + result.add(historyTopList.get(i).getBallNumber()); + } + + return result; + } + + /** + * 获取百期排行表中系数最大的前3位 + */ + private List getTop3FromHistoryTop100() { + List historyTop100List = dltFrontendHistoryTop100Mapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltFrontendHistoryTop100::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTop100List)) { + return new ArrayList<>(); + } + + // 取前3位 + List top3Balls = new ArrayList<>(); + int count = 0; + int index = 0; + + while (count < 3 && index < historyTop100List.size()) { + double currentCoefficient = historyTop100List.get(index).getActiveCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTop100List.size() && + historyTop100List.get(index).getActiveCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTop100List.get(index).getBallNumber()); + index++; + } + + // 根据需要选择的数量和当前可用数量处理 + int needCount = Math.min(3 - count, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + top3Balls.add(sameCoefficientBalls.get(0)); + count++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + top3Balls.addAll(sameCoefficientBalls); + count += needCount; + } else { + // 需要从多个相同系数的球号中选择部分,使用dlt_frontend_history_all表比较 + List selectedBalls = selectBallsFromHistoryAll(sameCoefficientBalls, needCount); + top3Balls.addAll(selectedBalls); + count += selectedBalls.size(); + } + } + + return top3Balls; + } + + /** + * 从dlt_frontend_history_all表中根据活跃系数选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List selectBallsFromHistoryAll(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + // 获取所有候选球号在dlt_frontend_history_all表中的活跃系数 + Map ballCoefficientMap = new HashMap<>(); + for (Integer ballNumber : candidateBalls) { + DltFrontendHistoryAll record = dltFrontendHistoryAllMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryAll::getBallNumber, ballNumber)); + double coefficient = (record != null) ? record.getActiveCoefficient() : 0.0; + ballCoefficientMap.put(ballNumber, coefficient); + } + + // 按活跃系数降序排序 + List> sortedEntries = ballCoefficientMap.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + // 选择前selectCount个球号 + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedEntries.size()); i++) { + result.add(sortedEntries.get(i).getKey()); + } + + return result; + } + + /** + * 处理多个边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleMultipleBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 1. 先从dlt_frontend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop100 record = dltFrontendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 按系数降序排序 + List> sortedByTop100 = top100Coefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + + // 按系数分组处理 + int currentIndex = 0; + while (result.size() < selectCount && currentIndex < sortedByTop100.size()) { + Double currentCoefficient = sortedByTop100.get(currentIndex).getValue(); + List sameTop100CoefficientBalls = new ArrayList<>(); + + // 收集相同系数的球号 + while (currentIndex < sortedByTop100.size() && + sortedByTop100.get(currentIndex).getValue().equals(currentCoefficient)) { + sameTop100CoefficientBalls.add(sortedByTop100.get(currentIndex).getKey()); + currentIndex++; + } + + int needFromThisGroup = Math.min(selectCount - result.size(), sameTop100CoefficientBalls.size()); + + if (sameTop100CoefficientBalls.size() == 1 || needFromThisGroup == sameTop100CoefficientBalls.size()) { + // 只有一个球或者需要全部选择 + for (int i = 0; i < needFromThisGroup; i++) { + result.add(sameTop100CoefficientBalls.get(i)); + } + } else { + // 需要进一步筛选,使用dlt_frontend_history_top表 + List selectedFromTop = selectFromHistoryTop(sameTop100CoefficientBalls, needFromThisGroup); + result.addAll(selectedFromTop); + } + } + + return result; + } + + /** + * 从dlt_frontend_history_top表中选择指定数量的球号 + */ + private List selectFromHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在dlt_frontend_history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 处理边界冲突(毛边) + */ + private Integer handleBoundaryConflicts(List candidateBalls) { + if (CollectionUtils.isEmpty(candidateBalls)) { + return null; + } + + if (candidateBalls.size() == 1) { + return candidateBalls.get(0); + } + + // 1. 先从dlt_frontend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop100 record = dltFrontendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 找出系数最大的球 + Optional> maxTop100Entry = top100Coefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTop100Entry.isPresent()) { + Double maxCoefficient = maxTop100Entry.get().getValue(); + List maxCoefficientBalls = top100Coefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxCoefficientBalls.size() == 1) { + return maxCoefficientBalls.get(0); + } + + // 2. 如果仍有多个球系数相同,从dlt_frontend_history_top比较 + Map topCoefficients = new HashMap<>(); + for (Integer ball : maxCoefficientBalls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + Optional> maxTopEntry = topCoefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTopEntry.isPresent()) { + Double maxTopCoefficient = maxTopEntry.get().getValue(); + List maxTopCoefficientBalls = topCoefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxTopCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxTopCoefficientBalls.size() == 1) { + return maxTopCoefficientBalls.get(0); + } + } + } + + // 3. 如果仍无法区分,默认选择第一位 + return candidateBalls.get(0); + } + + /** + * 从所有候选球中筛选最终的12个球 + * @param allCandidateBalls 所有候选球 + * @param ballWithCoefficientMap 球号对应的系数信息 + * @return 最终选出的12个球和筛选过程 + */ + private FirstBallPredictionResult selectFinal12Balls(List allCandidateBalls, Map> ballWithCoefficientMap) { + StringBuilder processBuilder = new StringBuilder(); + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>((a, b) -> b.compareTo(a)); // 按频率降序 + for (Map.Entry entry : ballFrequencyMap.entrySet()) { + int ball = entry.getKey(); + int frequency = entry.getValue(); + frequencyGroups.computeIfAbsent(frequency, k -> new ArrayList<>()).add(ball); + } + + // 预先确定最低筛选频率(即实际参与筛选的最低频率) + int minSelectedFrequency = Integer.MAX_VALUE; + int tempSelected = 0; + for (Map.Entry> entry : frequencyGroups.entrySet()) { + int frequency = entry.getKey(); + List balls = entry.getValue(); + + if (tempSelected + balls.size() <= 12) { + // 这个频率组会全部被选中 + minSelectedFrequency = Math.min(minSelectedFrequency, frequency); + tempSelected += balls.size(); + } else { + // 这个频率组需要部分选择,说明这是最后一个参与筛选的频率组 + minSelectedFrequency = Math.min(minSelectedFrequency, frequency); + break; + } + } + + // 生成频率分布说明(只显示实际参与筛选的球号) + processBuilder.append("参与筛选的候选球号按频率分布为"); + List frequencyDescriptions = new ArrayList<>(); + + // 只显示频率大于等于最低筛选频率的球号 + for (Map.Entry> entry : frequencyGroups.entrySet()) { + int frequency = entry.getKey(); + if (frequency >= minSelectedFrequency) { + List balls = entry.getValue(); + Collections.sort(balls); // 排序显示 + + if (balls.size() == 1) { + frequencyDescriptions.add(balls.get(0) + "(出现" + frequency + "次)"); + } else { + String ballsStr = balls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + frequencyDescriptions.add(ballsStr + "(出现" + frequency + "次)"); + } + } + } + processBuilder.append(String.join(",", frequencyDescriptions)); + processBuilder.append(";"); + + List resultBalls = new ArrayList<>(); + List directSelected = new ArrayList<>(); + List needFurtherSelection = new ArrayList<>(); + boolean hasSecondarySelection = false; + String selectionSteps = ""; + String detailedCoefficientInfo = ""; + + // 逐个处理每个频率组 + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + List ballsInGroup = frequencyGroup.getValue(); + int remainingNeeded = 12 - resultBalls.size(); + + if (remainingNeeded <= 0) { + break; + } + + if (ballsInGroup.size() <= remainingNeeded) { + // 组内球数小于等于剩余需要数,全部加入 + resultBalls.addAll(ballsInGroup); + directSelected.addAll(ballsInGroup); + } else { + // 组内球数大于剩余需要数,需要筛选 + hasSecondarySelection = true; + needFurtherSelection.addAll(ballsInGroup); + + // 进行D9系数筛选 + D9SelectionResult d9Result = selectBallsByD9CoefficientWithProcess(ballsInGroup, ballWithCoefficientMap, remainingNeeded); + resultBalls.addAll(d9Result.selectedBalls); + selectionSteps = d9Result.stepDescription; + detailedCoefficientInfo = d9Result.detailedInfo; + break; // 只处理需要筛选的第一组 + } + } + + // 生成筛选过程说明 + if (hasSecondarySelection) { + processBuilder.append("无法直接筛选出前12个,其中"); + Collections.sort(directSelected); + String directSelectedStr = directSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(directSelectedStr).append("直接入选,"); + + Collections.sort(needFurtherSelection); + String needFurtherStr = needFurtherSelection.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(needFurtherStr).append("需要进行二次筛选,"); + + List finalSelected = new ArrayList<>(resultBalls); + finalSelected.removeAll(directSelected); + Collections.sort(finalSelected); + String finalSelectedStr = finalSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("最终筛选出").append(finalSelectedStr).append(","); + + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("组成前12个球号:").append(allResultStr).append("。"); + + processBuilder.append("筛选步骤:").append(selectionSteps).append("。"); + if (!detailedCoefficientInfo.isEmpty()) { + processBuilder.append(" ").append(detailedCoefficientInfo).append("。"); + } + } else { + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("直接筛选出前12个球号:").append(allResultStr).append("。"); + } + + // 无论是否有二次筛选,都要显示筛选步骤 + if (!hasSecondarySelection || selectionSteps.isEmpty()) { + processBuilder.append("筛选步骤:通过频率筛选确定所有球号,无需进行D9系数筛选、百期排位、历史排位。"); + } + + return new FirstBallPredictionResult(resultBalls, processBuilder.toString()); + } + + /** + * D9系数筛选结果类 + */ + @Data + private static class D9SelectionResult { + private List selectedBalls; + private String stepDescription; + private String detailedInfo; + + public D9SelectionResult(List selectedBalls, String stepDescription, String detailedInfo) { + this.selectedBalls = selectedBalls; + this.stepDescription = stepDescription; + this.detailedInfo = detailedInfo; + } + } + + /** + * 根据D9系数筛选球号(带过程记录) + */ + private D9SelectionResult selectBallsByD9CoefficientWithProcess(List candidateBalls, Map> ballWithCoefficientMap, int needCount) { + // 计算每个球的D9系数和 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : candidateBalls) { + List coefficients = ballWithCoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSum.put(ball, sum); + } + + // 生成系数详情信息 + List coefficientDetails = new ArrayList<>(); + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + coefficientDetails.add(entry.getKey() + "(系数和" + String.format("%.2f", entry.getValue()) + ")"); + } + Collections.sort(coefficientDetails); + String detailedInfo = "T3系数和详情:" + String.join(",", coefficientDetails); + + // 按D9系数分组 + Map> coefficientGroups = new TreeMap<>((a, b) -> b.compareTo(a)); // 降序 + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + double coefficient = entry.getValue(); + int ball = entry.getKey(); + coefficientGroups.computeIfAbsent(coefficient, k -> new ArrayList<>()).add(ball); + } + + List result = new ArrayList<>(); + String stepDescription = ""; + boolean usedHistoryRanking = false; + + // 逐个处理每个系数组 + for (Map.Entry> coefficientGroup : coefficientGroups.entrySet()) { + List ballsInGroup = coefficientGroup.getValue(); + int remainingNeeded = needCount - result.size(); + + if (remainingNeeded <= 0) { + break; + } + + if (ballsInGroup.size() <= remainingNeeded) { + // 组内球数小于等于剩余需要数,全部加入 + result.addAll(ballsInGroup); + } else { + // 组内球数大于剩余需要数,按百期排行筛选 + List selectedFromGroup = selectBallsByHistoryRanking(ballsInGroup, remainingNeeded); + result.addAll(selectedFromGroup); + usedHistoryRanking = true; + break; + } + } + + if (usedHistoryRanking) { + stepDescription = "通过频率筛选确定部分球号,通过D9系数和筛选确定剩余球号,需要进行百期排位、历史排位"; + } else { + stepDescription = "通过频率筛选确定部分球号,通过D9系数和筛选确定剩余球号,无需进行百期排位、历史排位"; + } + + return new D9SelectionResult(result, stepDescription, detailedInfo); + } + + /** + * 根据D9系数筛选球号(原版本保持兼容) + */ + private List selectBallsByD9Coefficient(List candidateBalls, Map> ballWithCoefficientMap, int needCount) { + // 计算每个球的D9系数和 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : candidateBalls) { + List coefficients = ballWithCoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSum.put(ball, sum); + } + + // 按D9系数分组 + Map> coefficientGroups = new TreeMap<>((a, b) -> b.compareTo(a)); // 降序 + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + double coefficient = entry.getValue(); + int ball = entry.getKey(); + coefficientGroups.computeIfAbsent(coefficient, k -> new ArrayList<>()).add(ball); + } + + List result = new ArrayList<>(); + + // 逐个处理每个系数组 + for (Map.Entry> coefficientGroup : coefficientGroups.entrySet()) { + List ballsInGroup = coefficientGroup.getValue(); + int remainingNeeded = needCount - result.size(); + + if (remainingNeeded <= 0) { + break; + } + + if (ballsInGroup.size() <= remainingNeeded) { + // 组内球数小于等于剩余需要数,全部加入 + result.addAll(ballsInGroup); + } else { + // 组内球数大于剩余需要数,按百期排行筛选 + List selectedFromGroup = selectBallsByHistoryRanking(ballsInGroup, remainingNeeded); + result.addAll(selectedFromGroup); + } + } + + return result; + } + + /** + * 根据历史排行筛选球号 + */ + private List selectBallsByHistoryRanking(List candidateBalls, int needCount) { + // 先按百期排行筛选 + Map top100Rankings = getHistoryTop100Rankings(candidateBalls); + + // 按百期排行分组 + Map> top100Groups = new TreeMap<>((a, b) -> b.compareTo(a)); // 降序 + for (Map.Entry entry : top100Rankings.entrySet()) { + double ranking = entry.getValue(); + int ball = entry.getKey(); + top100Groups.computeIfAbsent(ranking, k -> new ArrayList<>()).add(ball); + } + + List result = new ArrayList<>(); + + // 逐个处理每个百期排行组 + for (Map.Entry> rankingGroup : top100Groups.entrySet()) { + List ballsInGroup = rankingGroup.getValue(); + int remainingNeeded = needCount - result.size(); + + if (remainingNeeded <= 0) { + break; + } + + if (ballsInGroup.size() <= remainingNeeded) { + // 组内球数小于等于剩余需要数,全部加入 + result.addAll(ballsInGroup); + } else { + // 组内球数大于剩余需要数,按历史排行筛选 + List selectedFromGroup = selectTopBallsByRanking(ballsInGroup, remainingNeeded); + result.addAll(selectedFromGroup); + } + } + + return result; + } + + /** + * 根据历史排行筛选最优球号 + */ + private List selectTopBallsByRanking(List candidateBalls, int needCount) { + Map topRankings = getHistoryTopRankings(candidateBalls); + + return topRankings.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .limit(needCount) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + /** + * 获取球号在百期排行表中的系数 + */ + private Map getHistoryTop100Rankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltFrontendHistoryTop100 record = dltFrontendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop100::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 获取球号在历史排行表中的系数 + */ + private Map getHistoryTopRankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 验证输入参数 + */ + private void validateInputParams(String level, List redBalls, List blueBalls) { + if (!"high".equalsIgnoreCase(level) && !"middle".equalsIgnoreCase(level) && !"low".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是high/middle/low之一"); + } + + if (CollectionUtils.isEmpty(redBalls) || redBalls.size() != 5) { + throw new IllegalArgumentException("前区号码必须为5个"); + } + + if (CollectionUtils.isEmpty(blueBalls) || blueBalls.size() != 2) { + throw new IllegalArgumentException("后区号码必须为2个"); + } + + for (Integer redBall : redBalls) { + if (redBall < 1 || redBall > 35) { + throw new IllegalArgumentException("前区号码范围应为1-35"); + } + } + + for (Integer blueBall : blueBalls) { + if (blueBall < 1 || blueBall > 12) { + throw new IllegalArgumentException("后区号码范围应为1-12"); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/dlt/FollowBackBallPredictor.java b/src/main/java/com/xy/xyaicpzs/dlt/FollowBackBallPredictor.java new file mode 100644 index 0000000..3f3f4b1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/dlt/FollowBackBallPredictor.java @@ -0,0 +1,1289 @@ +package com.xy.xyaicpzs.dlt; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 后区随球预测器 + */ +@Slf4j +@Component +public class FollowBackBallPredictor { + + @Autowired + private D6Mapper d6Mapper; + + @Autowired + private D7Mapper d7Mapper; + + @Autowired + private D10Mapper d10Mapper; + + @Autowired + private D11Mapper d11Mapper; + + @Autowired + private DltBackendHistoryTop100Mapper dltBackendHistoryTop100Mapper; + + @Autowired + private DltBackendHistoryTopMapper dltBackendHistoryTopMapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + + @Override + public String toString() { + return "Ball{" + + "ballNumber=" + ballNumber + + ", coefficient=" + coefficient + + ", masterBallNumber=" + masterBallNumber + + '}'; + } + } + + /** + * 后区随球预测结果类 + */ + @Data + public static class FollowBackBallPredictionResult { + private List result; + private String filteringProcess; + + public FollowBackBallPredictionResult(List result, String filteringProcess) { + this.result = result; + this.filteringProcess = filteringProcess; + } + } + + /** + * 后区随球预测(带过程) + * @param level 位置级别(high/middle/low) + * @param backFirstBall 后区首球1个号码 + * @param nextFrontBalls 下期前区5个号码 + * @param previousFrontBalls 上期前区5个号码 + * @param previousBackBalls 上期后区2个号码 + * @return 预测的3个后区随球号码和筛选过程 + */ + public FollowBackBallPredictionResult predictFollowBackBallWithProcess(String level, Integer backFirstBall, List nextFrontBalls, List previousFrontBalls, + List previousBackBalls) { + // 参数验证 + validateInputParams(level, backFirstBall, nextFrontBalls, previousFrontBalls, previousBackBalls); + + // 存放D7表中对应的系数,用于后续筛选 + Map> d7CoefficientMap = new HashMap<>(); + + // 所有候选球列表 + List allCandidateBalls = new ArrayList<>(); + + // (1) 处理后区首球号码 + List firstBallCandidates; + if ("high".equalsIgnoreCase(level)) { + firstBallCandidates = getHighLevelBallsFromD7(backFirstBall); + } else if ("middle".equalsIgnoreCase(level)) { + firstBallCandidates = getMiddleLevelBallsFromD7(backFirstBall); + } else { // low + firstBallCandidates = getLowLevelBallsFromD7(backFirstBall); + } + for (BallWithCoefficient ball : firstBallCandidates) { + d7CoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // (2) 处理下期前区5个号码 + for (Integer nextFrontBall : nextFrontBalls) { + List ballsFromD6 = getTop10BallsFromD6(nextFrontBall); + for (BallWithCoefficient ball : ballsFromD6) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // (3) 处理上期前区5个号码 + for (Integer previousFrontBall : previousFrontBalls) { + List ballsFromD10 = getTop10BallsFromD10(previousFrontBall); + for (BallWithCoefficient ball : ballsFromD10) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // (4) 处理上期后区2个号码 + for (Integer previousBackBall : previousBackBalls) { + List ballsFromD11 = getTop10BallsFromD11(previousBackBall); + for (BallWithCoefficient ball : ballsFromD11) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // (5) 从历史排行表获取前2名 + List top2FromHistoryTop = getTop2FromBackendHistoryTop(); + allCandidateBalls.addAll(top2FromHistoryTop); + + // (6) 从百期排行表获取前2名 + List top2FromHistoryTop100 = getTop2FromBackendHistoryTop100(); + allCandidateBalls.addAll(top2FromHistoryTop100); + + return selectFinal3BallsWithProcess(allCandidateBalls, d7CoefficientMap); + } + + /** + * 后区随球预测主方法 + * @param level 位置级别(high/middle/low) + * @param backFirstBall 后区首球1个号码 + * @param nextFrontBalls 下期前区5个号码 + * @param previousFrontBalls 上期前区5个号码 + * @param previousBackBalls 上期后区2个号码 + * @return 预测的3个后区随球号码 + */ + public List predictFollowBackBall(String level, Integer backFirstBall, List nextFrontBalls, List previousFrontBalls, + List previousBackBalls) { + // 参数验证 + validateInputParams(level, backFirstBall, nextFrontBalls, previousFrontBalls, previousBackBalls); + + // 存放D7表中对应的系数,用于后续筛选 + Map> d7CoefficientMap = new HashMap<>(); + + // 所有候选球列表 + List allCandidateBalls = new ArrayList<>(); + + // (1) 处理后区首球号码 + List firstBallCandidates; + if ("high".equalsIgnoreCase(level)) { + firstBallCandidates = getHighLevelBallsFromD7(backFirstBall); + } else if ("middle".equalsIgnoreCase(level)) { + firstBallCandidates = getMiddleLevelBallsFromD7(backFirstBall); + } else { // low + firstBallCandidates = getLowLevelBallsFromD7(backFirstBall); + } + for (BallWithCoefficient ball : firstBallCandidates) { + d7CoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // (2) 处理下期前区5个号码 + for (Integer nextFrontBall : nextFrontBalls) { + List nextFrontBallCandidates = getTop10BallsFromD6(nextFrontBall); + for (BallWithCoefficient ball : nextFrontBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // (3) 处理上期前区5个号码 + for (Integer prevFrontBall : previousFrontBalls) { + List prevFrontBallCandidates = getTop10BallsFromD10(prevFrontBall); + for (BallWithCoefficient ball : prevFrontBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // (4) 处理上期后区2个号码 + for (Integer prevBackBall : previousBackBalls) { + List prevBackBallCandidates = getTop10BallsFromD11(prevBackBall); + for (BallWithCoefficient ball : prevBackBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // (5) 从后区历史排行表获取平均值向上的2个球 + List top2HistoryTop = getTop2FromBackendHistoryTop(); + for (Integer ball : top2HistoryTop) { + allCandidateBalls.add(ball); + } + + // (6) 从后区百期排行表获取平均值向上的2个球 + List top2HistoryTop100 = getTop2FromBackendHistoryTop100(); + for (Integer ball : top2HistoryTop100) { + allCandidateBalls.add(ball); + } + + // 最终筛选3个球 + return selectFinal3Balls(allCandidateBalls, d7CoefficientMap); + } + + /** + * 高位策略:从D7表中获取最大值向下的5个球号 + */ + private List getHighLevelBallsFromD7(Integer masterBallNumber) { + List d7List = d7Mapper.selectList( + new LambdaQueryWrapper() + .eq(D7::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d7List)) { + log.warn("No D7 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d7List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 5 && index < d7List.size()) { + double currentCoefficient = d7List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d7List.size() && + d7List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d7List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(5 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 中位策略:从D7表中获取平均值及其附近的5个球号 + */ + private List getMiddleLevelBallsFromD7(Integer masterBallNumber) { + List d7List = d7Mapper.selectList( + new LambdaQueryWrapper() + .eq(D7::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d7List)) { + log.warn("No D7 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d7List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 计算平均系数 + double avgCoefficient = d7List.stream() + .mapToDouble(D7::getCoefficient) + .average() + .orElse(0.0); + + // 找到比平均值大且最接近平均值的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d7List.size() - 1; i >= 0; i--) { + double diff = d7List.get(i).getCoefficient() - avgCoefficient; + if (diff >= 0) { + avgPosition = i; + break; // 找到第一个比平均值大的位置就停止 + } + } + + // 如果没找到大于平均值的系数,取第一个 + if (avgPosition == -1) { + avgPosition = 0; + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d7List.get(avgPosition).getSlaveBallNumber(), + d7List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取2个球号(使用高位策略的毛边处理逻辑) + List upperBalls = getUpperBallsFromAverageD7(d7List, avgPosition, 2, masterBallNumber); + result.addAll(upperBalls); + + // 向下取2个球号(使用低位策略的毛边处理逻辑) + List lowerBalls = getLowerBallsFromAverageD7(d7List, avgPosition, 2, masterBallNumber); + result.addAll(lowerBalls); + + return result; + } + + /** + * 从D7表平均值位置向上取指定数量的球号 + */ + private List getUpperBallsFromAverageD7(List d7List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition - 1; // 从平均值位置的上一个开始 + int addedCount = 0; + + // 从平均值向上遍历(使用高位策略的逻辑) + while (addedCount < needCount && index >= 0) { + double currentCoefficient = d7List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d7List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d7List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向上使用高位策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D7表平均值位置向下取指定数量的球号 + */ + private List getLowerBallsFromAverageD7(List d7List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition + 1; // 从平均值位置的下一个开始 + int addedCount = 0; + + // 从平均值向下遍历(使用低位策略的逻辑) + while (addedCount < needCount && index < d7List.size()) { + double currentCoefficient = d7List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向后收集) + int tempIndex = index; + while (tempIndex < d7List.size() && + d7List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d7List.get(tempIndex).getSlaveBallNumber()); + tempIndex++; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向下使用后区策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 低位策略:从D7表中获取最小值向上的5个球号 + */ + private List getLowLevelBallsFromD7(Integer masterBallNumber) { + List d7List = d7Mapper.selectList( + new LambdaQueryWrapper() + .eq(D7::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d7List)) { + log.warn("No D7 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d7List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 从最小值向上取5个球 + int endPos = d7List.size() - 1; + int startPos = Math.max(0, endPos - 4); + + List result = new ArrayList<>(); + + // 处理上边界(第一位)的毛边情况 + List boundaryCandidates = new ArrayList<>(); + double boundaryCoefficient = d7List.get(startPos).getCoefficient(); + + for (int i = startPos; i >= 0 && d7List.get(i).getCoefficient().equals(boundaryCoefficient); i--) { + boundaryCandidates.add(d7List.get(i).getSlaveBallNumber()); + } + + if (boundaryCandidates.size() > 1) { + Integer bestBall = handleBoundaryConflicts(boundaryCandidates); + result.add(new BallWithCoefficient(bestBall, boundaryCoefficient, masterBallNumber)); + startPos++; + } else { + result.add(new BallWithCoefficient(d7List.get(startPos).getSlaveBallNumber(), + d7List.get(startPos).getCoefficient(), masterBallNumber)); + } + + // 添加其余的球 + for (int i = startPos + 1; i <= endPos; i++) { + result.add(new BallWithCoefficient(d7List.get(i).getSlaveBallNumber(), + d7List.get(i).getCoefficient(), masterBallNumber)); + } + + return result; + } + + /** + * 从D10表中获取最大值向下的10个球号 + */ + private List getTop10BallsFromD10(Integer masterBallNumber) { + List d10List = d10Mapper.selectList( + new LambdaQueryWrapper() + .eq(D10::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d10List)) { + log.warn("No D10 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d10List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 10 && index < d10List.size()) { + double currentCoefficient = d10List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d10List.size() && + d10List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d10List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(10 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D11表中获取最大值向下的10个球号 + */ + private List getTop10BallsFromD11(Integer masterBallNumber) { + List d11List = d11Mapper.selectList( + new LambdaQueryWrapper() + .eq(D11::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d11List)) { + log.warn("No D11 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d11List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 10 && index < d11List.size()) { + double currentCoefficient = d11List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d11List.size() && + d11List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d11List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(10 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D6表中获取最大值向下的10个球号 + */ + private List getTop10BallsFromD6(Integer masterBallNumber) { + List d6List = d6Mapper.selectList( + new LambdaQueryWrapper() + .eq(D6::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d6List)) { + log.warn("No D6 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d6List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 10 && index < d6List.size()) { + double currentCoefficient = d6List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d6List.size() && + d6List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d6List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(10 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 获取后区历史排行表中平均值向上的2位 + */ + private List getTop2FromBackendHistoryTop() { + List historyTopList = dltBackendHistoryTopMapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltBackendHistoryTop::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTopList)) { + return new ArrayList<>(); + } + + // 计算平均系数 + double avgCoefficient = historyTopList.stream() + .mapToDouble(DltBackendHistoryTop::getActiveCoefficient) + .average() + .orElse(0.0); + + // 找出比平均值大且最接近平均值的球号位置 + int startIndex = -1; + double minDiffAboveAvg = Double.MAX_VALUE; + + for (int i = historyTopList.size() - 1; i >= 0; i--) { + double coefficient = historyTopList.get(i).getActiveCoefficient(); + if (coefficient > avgCoefficient) { + double diff = coefficient - avgCoefficient; + if (diff < minDiffAboveAvg) { + minDiffAboveAvg = diff; + startIndex = i; + } + } + } + + List result = new ArrayList<>(); + if (startIndex != -1) { + // 从找到的位置向上取2个球号 + int endIndex = Math.max(0, startIndex - 1); + for (int i = startIndex; i >= endIndex; i--) { + result.add(historyTopList.get(i).getBallNumber()); + } + } + + return result; + } + + /** + * 获取后区百期排行表中平均值向上的2位 + */ + private List getTop2FromBackendHistoryTop100() { + List historyTop100List = dltBackendHistoryTop100Mapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltBackendHistoryTop100::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTop100List)) { + return new ArrayList<>(); + } + + // 计算平均系数 + double avgCoefficient = historyTop100List.stream() + .mapToDouble(DltBackendHistoryTop100::getActiveCoefficient) + .average() + .orElse(0.0); + + // 找出比平均值大且差值最小的所有球号 + double minDiffAboveAvg = Double.MAX_VALUE; + List closestBalls = new ArrayList<>(); + + for (DltBackendHistoryTop100 ball : historyTop100List) { + double coefficient = ball.getActiveCoefficient(); + if (coefficient > avgCoefficient) { + double diff = coefficient - avgCoefficient; + if (diff < minDiffAboveAvg) { + minDiffAboveAvg = diff; + closestBalls.clear(); + closestBalls.add(ball.getBallNumber()); + } else if (diff == minDiffAboveAvg) { + closestBalls.add(ball.getBallNumber()); + } + } + } + + // 如果没有比平均值大的球号,返回空列表 + if (closestBalls.isEmpty()) { + return new ArrayList<>(); + } + + // 如果比平均值大且差值最小的球号数量 <= 2,直接返回 + if (closestBalls.size() <= 2) { + return closestBalls; + } + + // 如果比平均值大且差值最小的球号数量 > 2,通过dlt_backend_history_top表筛选 + return selectTop2FromBackendHistoryTop(closestBalls); + } + + /** + * 从dlt_backend_history_top表中选择前2个球号 + */ + private List selectTop2FromBackendHistoryTop(List candidateBalls) { + // 获取候选球号在dlt_backend_history_top表中的活跃系数 + Map ballCoefficientMap = new HashMap<>(); + for (Integer ballNumber : candidateBalls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop::getBallNumber, ballNumber)); + double coefficient = (record != null) ? record.getActiveCoefficient() : 0.0; + ballCoefficientMap.put(ballNumber, coefficient); + } + + // 按活跃系数降序排序,选择前2个 + List> sortedEntries = ballCoefficientMap.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(2, sortedEntries.size()); i++) { + result.add(sortedEntries.get(i).getKey()); + } + + return result; + } + + /** + * 处理多个边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleMultipleBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 1. 先从dlt_backend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop100 record = dltBackendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 按系数降序排序 + List> sortedByTop100 = top100Coefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + + // 按系数分组处理 + int currentIndex = 0; + while (result.size() < selectCount && currentIndex < sortedByTop100.size()) { + Double currentCoefficient = sortedByTop100.get(currentIndex).getValue(); + List sameTop100CoefficientBalls = new ArrayList<>(); + + // 收集相同系数的球号 + while (currentIndex < sortedByTop100.size() && + sortedByTop100.get(currentIndex).getValue().equals(currentCoefficient)) { + sameTop100CoefficientBalls.add(sortedByTop100.get(currentIndex).getKey()); + currentIndex++; + } + + int needFromThisGroup = Math.min(selectCount - result.size(), sameTop100CoefficientBalls.size()); + + if (sameTop100CoefficientBalls.size() == 1 || needFromThisGroup == sameTop100CoefficientBalls.size()) { + // 只有一个球或者需要全部选择 + for (int i = 0; i < needFromThisGroup; i++) { + result.add(sameTop100CoefficientBalls.get(i)); + } + } else { + // 需要进一步筛选,使用dlt_backend_history_top表 + List selectedFromTop = selectFromBackendHistoryTop(sameTop100CoefficientBalls, needFromThisGroup); + result.addAll(selectedFromTop); + } + } + + return result; + } + + /** + * 从dlt_backend_history_top表中选择指定数量的球号 + */ + private List selectFromBackendHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在dlt_backend_history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 处理边界冲突(毛边) + */ + private Integer handleBoundaryConflicts(List candidateBalls) { + if (CollectionUtils.isEmpty(candidateBalls)) { + return null; + } + + if (candidateBalls.size() == 1) { + return candidateBalls.get(0); + } + + // 1. 先从dlt_backend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop100 record = dltBackendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 找出系数最大的球 + Optional> maxTop100Entry = top100Coefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTop100Entry.isPresent()) { + Double maxCoefficient = maxTop100Entry.get().getValue(); + List maxCoefficientBalls = top100Coefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxCoefficientBalls.size() == 1) { + return maxCoefficientBalls.get(0); + } + + // 2. 如果仍有多个球系数相同,从dlt_backend_history_top比较 + Map topCoefficients = new HashMap<>(); + for (Integer ball : maxCoefficientBalls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + Optional> maxTopEntry = topCoefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTopEntry.isPresent()) { + Double maxTopCoefficient = maxTopEntry.get().getValue(); + List maxTopCoefficientBalls = topCoefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxTopCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxTopCoefficientBalls.size() == 1) { + return maxTopCoefficientBalls.get(0); + } + } + } + + // 3. 如果仍无法区分,默认选择第一位 + return candidateBalls.get(0); + } + + /** + * 从所有候选球中筛选最终的3个球(带过程) + * @param allCandidateBalls 所有候选球 + * @param d7CoefficientMap D7表中的系数信息 + * @return 最终选出的3个球和筛选过程 + */ + private FollowBackBallPredictionResult selectFinal3BallsWithProcess(List allCandidateBalls, Map> d7CoefficientMap) { + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>((f1, f2) -> f2.compareTo(f1)); // 降序 + for (Map.Entry entry : ballFrequencyMap.entrySet()) { + frequencyGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + StringBuilder processBuilder = new StringBuilder(); + List resultBalls = new ArrayList<>(); + List directSelected = new ArrayList<>(); + List needFurtherSelection = new ArrayList<>(); + boolean hasSecondarySelection = false; + String selectionSteps = ""; + String detailedCoefficientInfo = ""; + + // 首先确定实际参与筛选的最低频率 + int currentSelected = 0; + int minSelectedFrequency = 0; + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + int frequency = frequencyGroup.getKey(); + int ballsInGroup = frequencyGroup.getValue().size(); + + if (currentSelected + ballsInGroup <= 3) { + // 这个频率组的球全部入选 + currentSelected += ballsInGroup; + minSelectedFrequency = frequency; + } else { + // 这个频率组只有部分球入选 + minSelectedFrequency = frequency; + break; + } + } + + // 生成频率分布描述 + List frequencyDescriptions = new ArrayList<>(); + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + int frequency = frequencyGroup.getKey(); + if (frequency >= minSelectedFrequency) { + List ballsInGroup = new ArrayList<>(frequencyGroup.getValue()); + Collections.sort(ballsInGroup); + String ballsStr = ballsInGroup.stream().map(String::valueOf).collect(Collectors.joining(", ")); + frequencyDescriptions.add(ballsStr + "(出现" + frequency + "次)"); + } + } + processBuilder.append("参与筛选的候选球号按频率分布为").append(String.join(",", frequencyDescriptions)).append(";"); + + // 按频率从高到低处理每个频率组 + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + if (resultBalls.size() >= 3) { + break; + } + + List ballsInGroup = frequencyGroup.getValue(); + int needCount = Math.min(3 - resultBalls.size(), ballsInGroup.size()); + + if (ballsInGroup.size() <= needCount) { + // 如果这个频率组的球数量 <= 需要的数量,全部加入 + resultBalls.addAll(ballsInGroup); + directSelected.addAll(ballsInGroup); + } else { + // 需要从这个频率组中选择部分球号,使用D7系数筛选 + List selectedFromGroup = selectBallsByD7Coefficient(ballsInGroup, needCount, d7CoefficientMap); + resultBalls.addAll(selectedFromGroup); + needFurtherSelection.addAll(ballsInGroup); + hasSecondarySelection = true; + selectionSteps = "通过频率筛选确定部分球号,通过D7系数和筛选确定剩余球号,无需进行百期排位、历史排位"; + + // 生成系数详情信息 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : ballsInGroup) { + List coefficients = d7CoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream().mapToDouble(BallWithCoefficient::getCoefficient).sum(); + ballCoefficientSum.put(ball, sum); + } + + List coefficientDetails = new ArrayList<>(); + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + coefficientDetails.add(entry.getKey() + "(系数和" + String.format("%.2f", entry.getValue()) + ")"); + } + Collections.sort(coefficientDetails); + detailedCoefficientInfo = "D7系数和详情:" + String.join(",", coefficientDetails); + + break; // 只处理需要筛选的第一组 + } + } + + // 生成筛选过程说明 + if (hasSecondarySelection) { + processBuilder.append("无法直接筛选出前3个,其中"); + Collections.sort(directSelected); + String directSelectedStr = directSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(directSelectedStr).append("直接入选,"); + + Collections.sort(needFurtherSelection); + String needFurtherStr = needFurtherSelection.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(needFurtherStr).append("需要进行二次筛选,"); + + List finalSelected = new ArrayList<>(resultBalls); + finalSelected.removeAll(directSelected); + Collections.sort(finalSelected); + String finalSelectedStr = finalSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("最终筛选出").append(finalSelectedStr).append(","); + + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("组成前3个球号:").append(allResultStr).append("。"); + + processBuilder.append("筛选步骤:").append(selectionSteps).append("。"); + if (!detailedCoefficientInfo.isEmpty()) { + processBuilder.append(" ").append(detailedCoefficientInfo).append("。"); + } + } else { + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("直接筛选出前3个球号:").append(allResultStr).append("。"); + } + + // 无论是否有二次筛选,都要显示筛选步骤 + if (!hasSecondarySelection || selectionSteps.isEmpty()) { + processBuilder.append("筛选步骤:通过频率筛选确定所有球号,无需进行D7系数筛选、百期排位、历史排位。"); + } + + return new FollowBackBallPredictionResult(resultBalls, processBuilder.toString()); + } + + /** + * 从所有候选球中筛选最终的3个球 + * @param allCandidateBalls 所有候选球 + * @param d7CoefficientMap D7表中的系数信息 + * @return 最终选出的3个球 + */ + private List selectFinal3Balls(List allCandidateBalls, Map> d7CoefficientMap) { + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>((f1, f2) -> f2.compareTo(f1)); // 降序 + for (Map.Entry entry : ballFrequencyMap.entrySet()) { + frequencyGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + List resultBalls = new ArrayList<>(); + + // 按频率从高到低处理每个频率组 + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + if (resultBalls.size() >= 3) { + break; + } + + List ballsInGroup = frequencyGroup.getValue(); + int needCount = Math.min(3 - resultBalls.size(), ballsInGroup.size()); + + if (ballsInGroup.size() <= needCount) { + // 如果这个频率组的球数量 <= 需要的数量,全部加入 + resultBalls.addAll(ballsInGroup); + } else { + // 需要从这个频率组中选择部分球号,使用D7系数筛选 + List selectedFromGroup = selectBallsByD7Coefficient(ballsInGroup, needCount, d7CoefficientMap); + resultBalls.addAll(selectedFromGroup); + } + } + + return resultBalls; + } + + /** + * 根据D7系数从候选球中选择指定数量的球号 + */ + private List selectBallsByD7Coefficient(List candidateBalls, int needCount, Map> d7CoefficientMap) { + // 计算每个球号的D7系数和 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : candidateBalls) { + List coefficients = d7CoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSum.put(ball, sum); + } + + // 按D7系数分组 + Map> coefficientGroups = new TreeMap<>((c1, c2) -> c2.compareTo(c1)); // 降序 + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + coefficientGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + List result = new ArrayList<>(); + + // 按系数从高到低处理每个系数组 + for (Map.Entry> coefficientGroup : coefficientGroups.entrySet()) { + if (result.size() >= needCount) { + break; + } + + List ballsInGroup = coefficientGroup.getValue(); + int needFromThisGroup = Math.min(needCount - result.size(), ballsInGroup.size()); + + if (ballsInGroup.size() <= needFromThisGroup) { + // 如果这个系数组的球数量 <= 需要的数量,全部加入 + result.addAll(ballsInGroup); + } else { + // 需要从这个系数组中选择部分球号,使用历史排行筛选 + List selectedFromGroup = selectBallsByBackendHistoryRanking(ballsInGroup, needFromThisGroup); + result.addAll(selectedFromGroup); + } + } + + return result; + } + + /** + * 根据后区历史排行从候选球中选择指定数量的球号 + */ + private List selectBallsByBackendHistoryRanking(List candidateBalls, int needCount) { + if (candidateBalls.size() <= needCount) { + return new ArrayList<>(candidateBalls); + } + + // 先尝试百期排行筛选 + Map top100Rankings = getBackendHistoryTop100Rankings(candidateBalls); + List result = selectTopBallsByBackendRanking(top100Rankings, needCount); + + // 如果百期排行筛选后仍有相同系数的球号,使用历史排行进一步筛选 + if (result.size() < needCount) { + List remaining = new ArrayList<>(candidateBalls); + remaining.removeAll(result); + + Map topRankings = getBackendHistoryTopRankings(remaining); + List additionalBalls = selectTopBallsByBackendRanking(topRankings, needCount - result.size()); + result.addAll(additionalBalls); + } + + return result; + } + + /** + * 根据系数排序选择指定数量的球号 + */ + private List selectTopBallsByBackendRanking(Map rankings, int needCount) { + List> sortedEntries = rankings.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(needCount, sortedEntries.size()); i++) { + result.add(sortedEntries.get(i).getKey()); + } + + return result; + } + + /** + * 获取球号在后区百期排行表中的系数 + */ + private Map getBackendHistoryTop100Rankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltBackendHistoryTop100 record = dltBackendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop100::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 获取球号在后区历史排行表中的系数 + */ + private Map getBackendHistoryTopRankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltBackendHistoryTop::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 验证输入参数 + */ + private void validateInputParams(String level, Integer backFirstBall, List nextFrontBalls, List previousFrontBalls, + List previousBackBalls) { + if (!"high".equalsIgnoreCase(level) && !"middle".equalsIgnoreCase(level) && !"low".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是high/middle/low之一"); + } + + if (backFirstBall == null) { + throw new IllegalArgumentException("后区首球不能为空"); + } + + if (CollectionUtils.isEmpty(nextFrontBalls) || nextFrontBalls.size() != 5) { + throw new IllegalArgumentException("下期前区号码必须为5个"); + } + + if (CollectionUtils.isEmpty(previousFrontBalls) || previousFrontBalls.size() != 5) { + throw new IllegalArgumentException("上期前区号码必须为5个"); + } + + if (CollectionUtils.isEmpty(previousBackBalls) || previousBackBalls.size() != 2) { + throw new IllegalArgumentException("上期后区号码必须为2个"); + } + + if (backFirstBall < 1 || backFirstBall > 12) { + throw new IllegalArgumentException("后区号码范围应为1-12"); + } + + for (Integer frontBall : nextFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("前区号码范围应为1-35"); + } + } + + for (Integer frontBall : previousFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("前区号码范围应为1-35"); + } + } + + for (Integer backBall : previousBackBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("后区号码范围应为1-12"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/dlt/FollowerBallPredictor.java b/src/main/java/com/xy/xyaicpzs/dlt/FollowerBallPredictor.java new file mode 100644 index 0000000..a70af1c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/dlt/FollowerBallPredictor.java @@ -0,0 +1,1258 @@ +package com.xy.xyaicpzs.dlt; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 前区随球预测器 + */ +@Slf4j +@Component +public class FollowerBallPredictor { + + @Autowired + private D5Mapper d5Mapper; + + @Autowired + private D9Mapper d9Mapper; + + @Autowired + private D12Mapper d12Mapper; + + @Autowired + private DltFrontendHistoryTop100Mapper dltFrontendHistoryTop100Mapper; + + @Autowired + private DltFrontendHistoryTopMapper dltFrontendHistoryTopMapper; + + @Autowired + private DltFrontendHistoryAllMapper dltFrontendHistoryAllMapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + + @Override + public String toString() { + return "Ball{" + + "ballNumber=" + ballNumber + + ", coefficient=" + coefficient + + ", masterBallNumber=" + masterBallNumber + + '}'; + } + } + + /** + * 前区随球预测结果类 + */ + @Data + public static class FollowerBallPredictionResult { + private List result; + private String filteringProcess; + + public FollowerBallPredictionResult(List result, String filteringProcess) { + this.result = result; + this.filteringProcess = filteringProcess; + } + } + + /** + * 前区随球预测(带过程) + * @param level 位置级别(high/middle/low) + * @param wellRegardedBalls 看好的4个号码 + * @param previousFrontBalls 上期前区5个号码 + * @param previousBackBalls 上期后区2个号码 + * @return 预测的10个随球号码和筛选过程 + */ + public FollowerBallPredictionResult predictFollowerBallWithProcess(String level, List wellRegardedBalls, List previousFrontBalls, List previousBackBalls) { + // 参数验证 + validateInputParams(level, wellRegardedBalls, previousFrontBalls, previousBackBalls); + + // 存放D5表中对应的系数,用于后续筛选 + Map> d5CoefficientMap = new HashMap<>(); + + // 所有候选球列表 + List allCandidateBalls = new ArrayList<>(); + + // 步骤一:处理第一个看好号码 + if (!wellRegardedBalls.isEmpty()) { + Integer firstBall = wellRegardedBalls.get(0); + List firstBallCandidates; + + if ("high".equalsIgnoreCase(level)) { + firstBallCandidates = getHighLevelBallsFromD5(firstBall); + } else if ("middle".equalsIgnoreCase(level)) { + firstBallCandidates = getMiddleLevelBallsFromD5(firstBall); + } else { // low + firstBallCandidates = getLowLevelBallsFromD5(firstBall); + } + + for (BallWithCoefficient ball : firstBallCandidates) { + d5CoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤二:处理上期前区5个号码 + for (Integer frontBall : previousFrontBalls) { + List frontBallCandidates = getTop30BallsFromD9(frontBall); + for (BallWithCoefficient ball : frontBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤三:从历史排行表获取平均值向上的3个球 + List top3HistoryTop = getTop3FromHistoryTop(); + allCandidateBalls.addAll(top3HistoryTop); + + // 步骤四:从百期排行表获取平均值向上的3个球 + List top3HistoryTop100 = getTop3FromHistoryTop100(); + allCandidateBalls.addAll(top3HistoryTop100); + + // 步骤五:处理上期后区2个号码 + for (Integer backBall : previousBackBalls) { + List backBallCandidates = getTop30BallsFromD12(backBall); + for (BallWithCoefficient ball : backBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + return selectFinal10BallsWithProcess(allCandidateBalls, d5CoefficientMap); + } + + /** + * 随球预测主方法 + * @param level 位置级别(high/middle/low) + * @param wellRegardedBalls 看好的4个号码 + * @param previousFrontBalls 上期前区5个号码 + * @param previousBackBalls 上期后区2个号码 + * @return 预测的10个随球号码 + */ + public List predictFollowerBall(String level, List wellRegardedBalls, List previousFrontBalls, List previousBackBalls) { + // 参数验证 + validateInputParams(level, wellRegardedBalls, previousFrontBalls, previousBackBalls); + + // 存放D5表中对应的系数,用于后续筛选 + Map> d5CoefficientMap = new HashMap<>(); + + // 所有候选球列表 + List allCandidateBalls = new ArrayList<>(); + + // 步骤一:处理第一个看好号码 + if (!wellRegardedBalls.isEmpty()) { + Integer firstBall = wellRegardedBalls.get(0); + List firstBallCandidates; + + if ("high".equalsIgnoreCase(level)) { + firstBallCandidates = getHighLevelBallsFromD5(firstBall); + } else if ("middle".equalsIgnoreCase(level)) { + firstBallCandidates = getMiddleLevelBallsFromD5(firstBall); + } else { // low + firstBallCandidates = getLowLevelBallsFromD5(firstBall); + } + + for (BallWithCoefficient ball : firstBallCandidates) { + d5CoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤二:处理上期前区5个号码 + for (Integer frontBall : previousFrontBalls) { + List frontBallCandidates = getTop30BallsFromD9(frontBall); + for (BallWithCoefficient ball : frontBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤三:从历史排行表获取平均值向上的3个球 + List top3HistoryTop = getTop3FromHistoryTop(); + allCandidateBalls.addAll(top3HistoryTop); + + // 步骤四:从百期排行表获取平均值向上的3个球 + List top3HistoryTop100 = getTop3FromHistoryTop100(); + allCandidateBalls.addAll(top3HistoryTop100); + + // 步骤五:处理上期后区2个号码 + for (Integer backBall : previousBackBalls) { + List backBallCandidates = getTop30BallsFromD12(backBall); + for (BallWithCoefficient ball : backBallCandidates) { + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // 步骤六:加入看好的后3个号码 + for (int i = 1; i < wellRegardedBalls.size(); i++) { + Integer wellBall = wellRegardedBalls.get(i); + allCandidateBalls.add(wellBall); + } + + // 最终筛选10个球 + return selectFinal10Balls(allCandidateBalls, d5CoefficientMap); + } + + /** + * 高位策略:从D5表中获取最大值向下的11个球号 + */ + private List getHighLevelBallsFromD5(Integer masterBallNumber) { + List d5List = d5Mapper.selectList( + new LambdaQueryWrapper() + .eq(D5::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d5List)) { + log.warn("No D5 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d5List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 11 && index < d5List.size()) { + double currentCoefficient = d5List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d5List.size() && + d5List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d5List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(11 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 中位策略:从D5表中获取平均值及其附近的11个球号 + */ + private List getMiddleLevelBallsFromD5(Integer masterBallNumber) { + List d5List = d5Mapper.selectList( + new LambdaQueryWrapper() + .eq(D5::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d5List)) { + log.warn("No D5 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d5List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 计算平均系数 + double avgCoefficient = d5List.stream() + .mapToDouble(D5::getCoefficient) + .average() + .orElse(0.0); + + // 找到比平均值大且最接近平均值的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d5List.size() - 1; i >= 0; i--) { + double diff = d5List.get(i).getCoefficient() - avgCoefficient; + if (diff >= 0) { + avgPosition = i; + break; // 找到第一个比平均值大的位置就停止 + } + } + + // 如果没找到大于平均值的系数,取第一个 + if (avgPosition == -1) { + avgPosition = 0; + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d5List.get(avgPosition).getSlaveBallNumber(), + d5List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取5个球号(使用高位策略的毛边处理逻辑) + List upperBalls = getUpperBallsFromAverageD5(d5List, avgPosition, 5, masterBallNumber); + result.addAll(upperBalls); + + // 向下取5个球号(使用低位策略的毛边处理逻辑) + List lowerBalls = getLowerBallsFromAverageD5(d5List, avgPosition, 5, masterBallNumber); + result.addAll(lowerBalls); + + return result; + } + + /** + * 从D5表平均值位置向上取指定数量的球号 + */ + private List getUpperBallsFromAverageD5(List d5List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition - 1; // 从平均值位置的上一个开始 + int addedCount = 0; + + // 从平均值向上遍历(使用高位策略的逻辑) + while (addedCount < needCount && index >= 0) { + double currentCoefficient = d5List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d5List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d5List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向上使用高位策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D5表平均值位置向下取指定数量的球号 + */ + private List getLowerBallsFromAverageD5(List d5List, int avgPosition, int needCount, Integer masterBallNumber) { + List result = new ArrayList<>(); + int index = avgPosition + 1; // 从平均值位置的下一个开始 + int addedCount = 0; + + // 从平均值向下遍历(使用低位策略的逻辑) + while (addedCount < needCount && index < d5List.size()) { + double currentCoefficient = d5List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向后收集) + int tempIndex = index; + while (tempIndex < d5List.size() && + d5List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d5List.get(tempIndex).getSlaveBallNumber()); + tempIndex++; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算还需要多少个球 + int needFromGroup = Math.min(needCount - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needFromGroup == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边(向下使用前区策略) + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needFromGroup); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 低位策略:从D5表中获取最小值向上第3-13位的11个球号 + */ + private List getLowLevelBallsFromD5(Integer masterBallNumber) { + List d5List = d5Mapper.selectList( + new LambdaQueryWrapper() + .eq(D5::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d5List)) { + log.warn("No D5 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d5List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + // 从最小值开始,跳过前2个位置,取第3-13位共11个球号 + int minPosition = d5List.size() - 1; + List result = new ArrayList<>(); + int index = minPosition; + int positionCount = 1; // 当前位置计数 + int addedCount = 0; // 已加入球号数量 + + // 从最小值开始向上遍历 + while (addedCount < 11 && index >= 0) { + double currentCoefficient = d5List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号(向前收集) + int tempIndex = index; + while (tempIndex >= 0 && + d5List.get(tempIndex).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d5List.get(tempIndex).getSlaveBallNumber()); + tempIndex--; + } + + // 更新index为下一个不同系数的位置 + index = tempIndex; + + // 计算当前系数组占据的位置数 + int currentGroupPositions = sameCoefficientBalls.size(); + + // 判断当前组是否在第3-13位范围内 + int groupStartPosition = positionCount; + int groupEndPosition = positionCount + currentGroupPositions - 1; + + if (groupEndPosition < 3) { + // 整个组都在第3位之前,跳过 + positionCount += currentGroupPositions; + continue; + } + + if (groupStartPosition > 13) { + // 整个组都在第13位之后,结束 + break; + } + + // 计算需要从当前组中选择多少个球号 + int validStartPosition = Math.max(groupStartPosition, 3); + int validEndPosition = Math.min(groupEndPosition, 13); + int needCount = validEndPosition - validStartPosition + 1; + needCount = Math.min(needCount, 11 - addedCount); + + if (needCount > 0) { + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + positionCount += currentGroupPositions; + } + + return result; + } + + /** + * 从D9表中获取最大值向下的30个球号 + */ + private List getTop30BallsFromD9(Integer masterBallNumber) { + List d9List = d9Mapper.selectList( + new LambdaQueryWrapper() + .eq(D9::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d9List)) { + log.warn("No D9 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d9List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 30 && index < d9List.size()) { + double currentCoefficient = d9List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d9List.size() && + d9List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d9List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(30 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 从D12表中获取最大值向下的30个球号 + */ + private List getTop30BallsFromD12(Integer masterBallNumber) { + List d12List = d12Mapper.selectList( + new LambdaQueryWrapper() + .eq(D12::getMasterBallNumber, masterBallNumber)); + if (CollectionUtils.isEmpty(d12List)) { + log.warn("No D12 records found for master ball: {}", masterBallNumber); + return new ArrayList<>(); + } + + // 按系数从大到小排序 + d12List.sort((d1, d2) -> d2.getCoefficient().compareTo(d1.getCoefficient())); + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 30 && index < d12List.size()) { + double currentCoefficient = d12List.get(index).getCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < d12List.size() && + d12List.get(index).getCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(d12List.get(index).getSlaveBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(30 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(new BallWithCoefficient(sameCoefficientBalls.get(0), currentCoefficient, masterBallNumber)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + for (int i = 0; i < sameCoefficientBalls.size(); i++) { + result.add(new BallWithCoefficient(sameCoefficientBalls.get(i), currentCoefficient, masterBallNumber)); + } + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理毛边 + List selectedBalls = handleMultipleBoundaryConflicts(sameCoefficientBalls, needCount); + for (Integer selectedBall : selectedBalls) { + result.add(new BallWithCoefficient(selectedBall, currentCoefficient, masterBallNumber)); + addedCount++; + } + } + } + + return result; + } + + /** + * 获取历史排行表中平均值向上的3位 + */ + private List getTop3FromHistoryTop() { + List historyTopList = dltFrontendHistoryTopMapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltFrontendHistoryTop::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTopList)) { + return new ArrayList<>(); + } + + // 计算平均系数 + double avgCoefficient = historyTopList.stream() + .mapToDouble(DltFrontendHistoryTop::getActiveCoefficient) + .average() + .orElse(0.0); + + // 找出比平均值大且最接近平均值的球号位置 + int startIndex = -1; + double minDiffAboveAvg = Double.MAX_VALUE; + + for (int i = historyTopList.size() - 1; i >= 0; i--) { + double coefficient = historyTopList.get(i).getActiveCoefficient(); + if (coefficient > avgCoefficient) { + double diff = coefficient - avgCoefficient; + if (diff < minDiffAboveAvg) { + minDiffAboveAvg = diff; + startIndex = i; + } + } + } + + List result = new ArrayList<>(); + if (startIndex != -1) { + // 从找到的位置向上取3个球号 + int endIndex = Math.max(0, startIndex - 2); + for (int i = startIndex; i >= endIndex; i--) { + result.add(historyTopList.get(i).getBallNumber()); + } + } + + return result; + } + + /** + * 获取百期排行表中平均值向上的3位 + */ + private List getTop3FromHistoryTop100() { + List historyTop100List = dltFrontendHistoryTop100Mapper.selectList( + new LambdaQueryWrapper() + .orderByDesc(DltFrontendHistoryTop100::getActiveCoefficient)); + if (CollectionUtils.isEmpty(historyTop100List)) { + return new ArrayList<>(); + } + + // 计算平均系数 + double avgCoefficient = historyTop100List.stream() + .mapToDouble(DltFrontendHistoryTop100::getActiveCoefficient) + .average() + .orElse(0.0); + + // 找出比平均值大且差值最小的所有球号 + double minDiffAboveAvg = Double.MAX_VALUE; + List closestBalls = new ArrayList<>(); + + for (DltFrontendHistoryTop100 ball : historyTop100List) { + double coefficient = ball.getActiveCoefficient(); + if (coefficient > avgCoefficient) { + double diff = coefficient - avgCoefficient; + if (diff < minDiffAboveAvg) { + minDiffAboveAvg = diff; + closestBalls.clear(); + closestBalls.add(ball.getBallNumber()); + } else if (diff == minDiffAboveAvg) { + closestBalls.add(ball.getBallNumber()); + } + } + } + + // 如果没有比平均值大的球号,返回空列表 + if (closestBalls.isEmpty()) { + return new ArrayList<>(); + } + + // 如果比平均值大且差值最小的球号数量 <= 3,直接返回 + if (closestBalls.size() <= 3) { + return closestBalls; + } + + // 如果比平均值大且差值最小的球号数量 > 3,通过dlt_frontend_history_all表筛选 + return selectTop3FromHistoryAll(closestBalls); + } + + /** + * 从dlt_frontend_history_all表中选择前3个球号 + */ + private List selectTop3FromHistoryAll(List candidateBalls) { + // 获取候选球号在dlt_frontend_history_all表中的活跃系数 + Map ballCoefficientMap = new HashMap<>(); + for (Integer ballNumber : candidateBalls) { + DltFrontendHistoryAll record = dltFrontendHistoryAllMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryAll::getBallNumber, ballNumber)); + double coefficient = (record != null) ? record.getActiveCoefficient() : 0.0; + ballCoefficientMap.put(ballNumber, coefficient); + } + + // 按活跃系数降序排序,选择前3个 + List> sortedEntries = ballCoefficientMap.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(3, sortedEntries.size()); i++) { + result.add(sortedEntries.get(i).getKey()); + } + + return result; + } + + /** + * 处理多个边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleMultipleBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 1. 先从dlt_frontend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop100 record = dltFrontendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 按系数降序排序 + List> sortedByTop100 = top100Coefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + + // 按系数分组处理 + int currentIndex = 0; + while (result.size() < selectCount && currentIndex < sortedByTop100.size()) { + Double currentCoefficient = sortedByTop100.get(currentIndex).getValue(); + List sameTop100CoefficientBalls = new ArrayList<>(); + + // 收集相同系数的球号 + while (currentIndex < sortedByTop100.size() && + sortedByTop100.get(currentIndex).getValue().equals(currentCoefficient)) { + sameTop100CoefficientBalls.add(sortedByTop100.get(currentIndex).getKey()); + currentIndex++; + } + + int needFromThisGroup = Math.min(selectCount - result.size(), sameTop100CoefficientBalls.size()); + + if (sameTop100CoefficientBalls.size() == 1 || needFromThisGroup == sameTop100CoefficientBalls.size()) { + // 只有一个球或者需要全部选择 + for (int i = 0; i < needFromThisGroup; i++) { + result.add(sameTop100CoefficientBalls.get(i)); + } + } else { + // 需要进一步筛选,使用dlt_frontend_history_top表 + List selectedFromTop = selectFromHistoryTop(sameTop100CoefficientBalls, needFromThisGroup); + result.addAll(selectedFromTop); + } + } + + return result; + } + + /** + * 从dlt_frontend_history_top表中选择指定数量的球号 + */ + private List selectFromHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在dlt_frontend_history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 处理边界冲突(毛边) + */ + private Integer handleBoundaryConflicts(List candidateBalls) { + if (CollectionUtils.isEmpty(candidateBalls)) { + return null; + } + + if (candidateBalls.size() == 1) { + return candidateBalls.get(0); + } + + // 1. 先从dlt_frontend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop100 record = dltFrontendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop100::getBallNumber, ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 找出系数最大的球 + Optional> maxTop100Entry = top100Coefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTop100Entry.isPresent()) { + Double maxCoefficient = maxTop100Entry.get().getValue(); + List maxCoefficientBalls = top100Coefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxCoefficientBalls.size() == 1) { + return maxCoefficientBalls.get(0); + } + + // 2. 如果仍有多个球系数相同,从dlt_frontend_history_top比较 + Map topCoefficients = new HashMap<>(); + for (Integer ball : maxCoefficientBalls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop::getBallNumber, ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + Optional> maxTopEntry = topCoefficients.entrySet().stream() + .max(Map.Entry.comparingByValue()); + + if (maxTopEntry.isPresent()) { + Double maxTopCoefficient = maxTopEntry.get().getValue(); + List maxTopCoefficientBalls = topCoefficients.entrySet().stream() + .filter(e -> e.getValue().equals(maxTopCoefficient)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + if (maxTopCoefficientBalls.size() == 1) { + return maxTopCoefficientBalls.get(0); + } + } + } + + // 3. 如果仍无法区分,默认选择第一位 + return candidateBalls.get(0); + } + + /** + * 从所有候选球中筛选最终的10个球(带过程) + * @param allCandidateBalls 所有候选球 + * @param d5CoefficientMap D5表中的系数信息 + * @return 最终选出的10个球和筛选过程 + */ + private FollowerBallPredictionResult selectFinal10BallsWithProcess(List allCandidateBalls, Map> d5CoefficientMap) { + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>((f1, f2) -> f2.compareTo(f1)); // 降序 + for (Map.Entry entry : ballFrequencyMap.entrySet()) { + frequencyGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + StringBuilder processBuilder = new StringBuilder(); + List resultBalls = new ArrayList<>(); + List directSelected = new ArrayList<>(); + List needFurtherSelection = new ArrayList<>(); + boolean hasSecondarySelection = false; + String selectionSteps = ""; + String detailedCoefficientInfo = ""; + + // 首先确定实际参与筛选的最低频率 + int currentSelected = 0; + int minSelectedFrequency = 0; + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + int frequency = frequencyGroup.getKey(); + int ballsInGroup = frequencyGroup.getValue().size(); + + if (currentSelected + ballsInGroup <= 10) { + // 这个频率组的球全部入选 + currentSelected += ballsInGroup; + minSelectedFrequency = frequency; + } else { + // 这个频率组只有部分球入选 + minSelectedFrequency = frequency; + break; + } + } + + // 生成频率分布描述 + List frequencyDescriptions = new ArrayList<>(); + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + int frequency = frequencyGroup.getKey(); + if (frequency >= minSelectedFrequency) { + List ballsInGroup = new ArrayList<>(frequencyGroup.getValue()); + Collections.sort(ballsInGroup); + String ballsStr = ballsInGroup.stream().map(String::valueOf).collect(Collectors.joining(", ")); + frequencyDescriptions.add(ballsStr + "(出现" + frequency + "次)"); + } + } + processBuilder.append("参与筛选的候选球号按频率分布为").append(String.join(",", frequencyDescriptions)).append(";"); + + // 按频率从高到低处理每个频率组 + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + if (resultBalls.size() >= 10) { + break; + } + + List ballsInGroup = frequencyGroup.getValue(); + int needCount = Math.min(10 - resultBalls.size(), ballsInGroup.size()); + + if (ballsInGroup.size() <= needCount) { + // 如果这个频率组的球数量 <= 需要的数量,全部加入 + resultBalls.addAll(ballsInGroup); + directSelected.addAll(ballsInGroup); + } else { + // 需要从这个频率组中选择部分球号,使用D5系数筛选 + List selectedFromGroup = selectBallsByD5Coefficient(ballsInGroup, needCount, d5CoefficientMap); + resultBalls.addAll(selectedFromGroup); + needFurtherSelection.addAll(ballsInGroup); + hasSecondarySelection = true; + selectionSteps = "通过频率筛选确定部分球号,通过D5系数和筛选确定剩余球号,无需进行百期排位、历史排位"; + + // 生成系数详情信息 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : ballsInGroup) { + List coefficients = d5CoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream().mapToDouble(BallWithCoefficient::getCoefficient).sum(); + ballCoefficientSum.put(ball, sum); + } + + List coefficientDetails = new ArrayList<>(); + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + coefficientDetails.add(entry.getKey() + "(系数和" + String.format("%.2f", entry.getValue()) + ")"); + } + Collections.sort(coefficientDetails); + detailedCoefficientInfo = "D5系数和详情:" + String.join(",", coefficientDetails); + + break; // 只处理需要筛选的第一组 + } + } + + // 生成筛选过程说明 + if (hasSecondarySelection) { + processBuilder.append("无法直接筛选出前10个,其中"); + Collections.sort(directSelected); + String directSelectedStr = directSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(directSelectedStr).append("直接入选,"); + + Collections.sort(needFurtherSelection); + String needFurtherStr = needFurtherSelection.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append(needFurtherStr).append("需要进行二次筛选,"); + + List finalSelected = new ArrayList<>(resultBalls); + finalSelected.removeAll(directSelected); + Collections.sort(finalSelected); + String finalSelectedStr = finalSelected.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("最终筛选出").append(finalSelectedStr).append(","); + + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("组成前10个球号:").append(allResultStr).append("。"); + + processBuilder.append("筛选步骤:").append(selectionSteps).append("。"); + if (!detailedCoefficientInfo.isEmpty()) { + processBuilder.append(" ").append(detailedCoefficientInfo).append("。"); + } + } else { + String allResultStr = resultBalls.stream().map(String::valueOf).collect(Collectors.joining(", ")); + processBuilder.append("直接筛选出前10个球号:").append(allResultStr).append("。"); + } + + // 无论是否有二次筛选,都要显示筛选步骤 + if (!hasSecondarySelection || selectionSteps.isEmpty()) { + processBuilder.append("筛选步骤:通过频率筛选确定所有球号,无需进行D5系数筛选、百期排位、历史排位。"); + } + + return new FollowerBallPredictionResult(resultBalls, processBuilder.toString()); + } + + /** + * 从所有候选球中筛选最终的10个球 + * @param allCandidateBalls 所有候选球 + * @param d5CoefficientMap D5表中的系数信息 + * @return 最终选出的10个球 + */ + private List selectFinal10Balls(List allCandidateBalls, Map> d5CoefficientMap) { + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>((f1, f2) -> f2.compareTo(f1)); // 降序 + for (Map.Entry entry : ballFrequencyMap.entrySet()) { + frequencyGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + List resultBalls = new ArrayList<>(); + + // 按频率从高到低处理每个频率组 + for (Map.Entry> frequencyGroup : frequencyGroups.entrySet()) { + if (resultBalls.size() >= 10) { + break; + } + + List ballsInGroup = frequencyGroup.getValue(); + int needCount = Math.min(10 - resultBalls.size(), ballsInGroup.size()); + + if (ballsInGroup.size() <= needCount) { + // 如果这个频率组的球数量 <= 需要的数量,全部加入 + resultBalls.addAll(ballsInGroup); + } else { + // 需要从这个频率组中选择部分球号,使用D5系数筛选 + List selectedFromGroup = selectBallsByD5Coefficient(ballsInGroup, needCount, d5CoefficientMap); + resultBalls.addAll(selectedFromGroup); + } + } + + return resultBalls; + } + + /** + * 根据D5系数从候选球中选择指定数量的球号 + */ + private List selectBallsByD5Coefficient(List candidateBalls, int needCount, Map> d5CoefficientMap) { + // 计算每个球号的D5系数和 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ball : candidateBalls) { + List coefficients = d5CoefficientMap.getOrDefault(ball, new ArrayList<>()); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSum.put(ball, sum); + } + + // 按D5系数分组 + Map> coefficientGroups = new TreeMap<>((c1, c2) -> c2.compareTo(c1)); // 降序 + for (Map.Entry entry : ballCoefficientSum.entrySet()) { + coefficientGroups.computeIfAbsent(entry.getValue(), k -> new ArrayList<>()).add(entry.getKey()); + } + + List result = new ArrayList<>(); + + // 按系数从高到低处理每个系数组 + for (Map.Entry> coefficientGroup : coefficientGroups.entrySet()) { + if (result.size() >= needCount) { + break; + } + + List ballsInGroup = coefficientGroup.getValue(); + int needFromThisGroup = Math.min(needCount - result.size(), ballsInGroup.size()); + + if (ballsInGroup.size() <= needFromThisGroup) { + // 如果这个系数组的球数量 <= 需要的数量,全部加入 + result.addAll(ballsInGroup); + } else { + // 需要从这个系数组中选择部分球号,使用历史排行筛选 + List selectedFromGroup = selectBallsByHistoryRanking(ballsInGroup, needFromThisGroup); + result.addAll(selectedFromGroup); + } + } + + return result; + } + + /** + * 根据历史排行从候选球中选择指定数量的球号 + */ + private List selectBallsByHistoryRanking(List candidateBalls, int needCount) { + if (candidateBalls.size() <= needCount) { + return new ArrayList<>(candidateBalls); + } + + // 先尝试百期排行筛选 + Map top100Rankings = getHistoryTop100Rankings(candidateBalls); + List result = selectTopBallsByRanking(top100Rankings, needCount); + + // 如果百期排行筛选后仍有相同系数的球号,使用历史排行进一步筛选 + if (result.size() < needCount) { + List remaining = new ArrayList<>(candidateBalls); + remaining.removeAll(result); + + Map topRankings = getHistoryTopRankings(remaining); + List additionalBalls = selectTopBallsByRanking(topRankings, needCount - result.size()); + result.addAll(additionalBalls); + } + + return result; + } + + /** + * 根据系数排序选择指定数量的球号 + */ + private List selectTopBallsByRanking(Map rankings, int needCount) { + List> sortedEntries = rankings.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(needCount, sortedEntries.size()); i++) { + result.add(sortedEntries.get(i).getKey()); + } + + return result; + } + + /** + * 获取球号在百期排行表中的系数 + */ + private Map getHistoryTop100Rankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltFrontendHistoryTop100 record = dltFrontendHistoryTop100Mapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop100::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 获取球号在历史排行表中的系数 + */ + private Map getHistoryTopRankings(List balls) { + Map result = new HashMap<>(); + for (Integer ball : balls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new LambdaQueryWrapper() + .eq(DltFrontendHistoryTop::getBallNumber, ball)); + result.put(ball, record != null ? record.getActiveCoefficient() : 0.0); + } + return result; + } + + /** + * 验证输入参数 + */ + private void validateInputParams(String level, List wellRegardedBalls, List previousFrontBalls, List previousBackBalls) { + if (!"high".equalsIgnoreCase(level) && !"middle".equalsIgnoreCase(level) && !"low".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是high/middle/low之一"); + } + + if (CollectionUtils.isEmpty(wellRegardedBalls) || wellRegardedBalls.size() != 4) { + throw new IllegalArgumentException("看好的号码必须为4个"); + } + + if (CollectionUtils.isEmpty(previousFrontBalls) || previousFrontBalls.size() != 5) { + throw new IllegalArgumentException("上期前区号码必须为5个"); + } + + if (CollectionUtils.isEmpty(previousBackBalls) || previousBackBalls.size() != 2) { + throw new IllegalArgumentException("上期后区号码必须为2个"); + } + + for (Integer frontBall : wellRegardedBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("前区号码范围应为1-35"); + } + } + + for (Integer frontBall : previousFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("前区号码范围应为1-35"); + } + } + + for (Integer backBall : previousBackBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("后区号码范围应为1-12"); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserQueryRequest.java b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserQueryRequest.java index e6cab54..2dfbdf8 100644 --- a/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserQueryRequest.java +++ b/src/main/java/com/xy/xyaicpzs/domain/dto/user/UserQueryRequest.java @@ -1,5 +1,7 @@ package com.xy.xyaicpzs.domain.dto.user; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.xy.xyaicpzs.common.PageRequest; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D10.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D10.java new file mode 100644 index 0000000..659f0c0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D10.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d10表 + * @TableName d10 + */ +@TableName(value ="d10") +@Data +public class D10 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D11.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D11.java new file mode 100644 index 0000000..f67c749 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D11.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d11表 + * @TableName d11 + */ +@TableName(value ="d11") +@Data +public class D11 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D12.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D12.java new file mode 100644 index 0000000..246f0a5 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D12.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d12表 + * @TableName d12 + */ +@TableName(value ="d12") +@Data +public class D12 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D5.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D5.java new file mode 100644 index 0000000..2f5be3a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D5.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d5表 + * @TableName d5 + */ +@TableName(value ="d5") +@Data +public class D5 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D6.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D6.java new file mode 100644 index 0000000..d7ecef1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D6.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d6表 + * @TableName d6 + */ +@TableName(value ="d6") +@Data +public class D6 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D7.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D7.java new file mode 100644 index 0000000..9e886bd --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D7.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d7表 + * @TableName d7 + */ +@TableName(value ="d7") +@Data +public class D7 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D8.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D8.java new file mode 100644 index 0000000..e7f9cc5 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D8.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d8表 + * @TableName d8 + */ +@TableName(value ="d8") +@Data +public class D8 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/D9.java b/src/main/java/com/xy/xyaicpzs/domain/entity/D9.java new file mode 100644 index 0000000..2f4ab19 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/D9.java @@ -0,0 +1,35 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * d9表 + * @TableName d9 + */ +@TableName(value ="d9") +@Data +public class D9 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 主球 + */ + private Integer masterBallNumber; + + /** + * 从球 + */ + private Integer slaveBallNumber; + + /** + * 系数 + */ + private Double coefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistory100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistory100.java new file mode 100644 index 0000000..53d15f6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistory100.java @@ -0,0 +1,50 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透后区最近100期数据表 + * @TableName dlt_backend_history_100 + */ +@TableName(value ="dlt_backend_history_100") +@Data +public class DltBackendHistory100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 平均隐现期(次) + */ + private Double averageHiddenAppear; + + /** + * 当前隐现期 + */ + private Integer currentHiddenInterval; + + /** + * 最多连出期(次) + */ + private Integer maxConsecutive; + + /** + * 活跃系数 + */ + private Double activeCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryAll.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryAll.java new file mode 100644 index 0000000..469f811 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryAll.java @@ -0,0 +1,55 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透后区全部历史数据表 + * @TableName dlt_backend_history_all + */ +@TableName(value ="dlt_backend_history_all") +@Data +public class DltBackendHistoryAll { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 出现频率% + */ + private Double frequencyPercentage; + + /** + * 平均隐现期(次) + */ + private Integer averageHiddenAppear; + + /** + * 最长隐现期(次) + */ + private Integer maxHiddenInterval; + + /** + * 最多连出期(次) + */ + private Integer maxConsecutive; + + /** + * 活跃系数 + */ + private Double activeCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryTop.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryTop.java new file mode 100644 index 0000000..5e8b613 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryTop.java @@ -0,0 +1,36 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透后区历史数据排行表 + * @TableName dlt_backend_history_top + */ +@TableName(value ="dlt_backend_history_top") +@Data +public class DltBackendHistoryTop { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排位 + */ + private Integer ranking; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 活跃系数 + */ + private Double activeCoefficient; + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryTop100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryTop100.java new file mode 100644 index 0000000..173c5b0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltBackendHistoryTop100.java @@ -0,0 +1,36 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透后区百期数据排行表 + * @TableName dlt_backend_history_top_100 + */ +@TableName(value ="dlt_backend_history_top_100") +@Data +public class DltBackendHistoryTop100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排位 + */ + private Integer ranking; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 活跃系数 + */ + private Double activeCoefficient; + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltDrawRecord.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltDrawRecord.java new file mode 100644 index 0000000..a3d6d23 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltDrawRecord.java @@ -0,0 +1,66 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.util.Date; +import lombok.Data; + +/** + * 大乐透开奖信息表 + * @TableName dlt_draw_record + */ +@TableName(value ="dlt_draw_record") +@Data +public class DltDrawRecord { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 开奖期号 + */ + private String drawId; + + /** + * 开奖日期 + */ + private Date drawDate; + + /** + * 前区1 + */ + private Integer frontBall1; + + /** + * 前区2 + */ + private Integer frontBall2; + + /** + * 前区3 + */ + private Integer frontBall3; + + /** + * 前区4 + */ + private Integer frontBall4; + + /** + * 前区5 + */ + private Integer frontBall5; + + /** + * 后区1 + */ + private Integer backBall1; + + /** + * 后区2 + */ + private Integer backBall2; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistory100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistory100.java new file mode 100644 index 0000000..2d004eb --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistory100.java @@ -0,0 +1,50 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透前区最近100期数据表 + * @TableName dlt_frontend_history_100 + */ +@TableName(value ="dlt_frontend_history_100") +@Data +public class DltFrontendHistory100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 平均隐现期(次) + */ + private Double averageHiddenAppear; + + /** + * 当前隐现期 + */ + private Integer currentHiddenInterval; + + /** + * 最多连出期(次) + */ + private Integer maxConsecutive; + + /** + * 活跃系数 + */ + private Double activeCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryAll.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryAll.java new file mode 100644 index 0000000..b048fe5 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryAll.java @@ -0,0 +1,55 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透前区全部历史数据表 + * @TableName dlt_frontend_history_all + */ +@TableName(value ="dlt_frontend_history_all") +@Data +public class DltFrontendHistoryAll { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequencyCount; + + /** + * 出现频率% + */ + private Double frequencyPercentage; + + /** + * 平均隐现期(次) + */ + private Integer averageHiddenAppear; + + /** + * 最长隐现期(次) + */ + private Integer maxHiddenInterval; + + /** + * 最多连出期(次) + */ + private Integer maxConsecutive; + + /** + * 活跃系数 + */ + private Double activeCoefficient; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryTop.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryTop.java new file mode 100644 index 0000000..45ffe83 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryTop.java @@ -0,0 +1,36 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透前区历史数据排行表 + * @TableName dlt_frontend_history_top + */ +@TableName(value ="dlt_frontend_history_top") +@Data +public class DltFrontendHistoryTop { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排位 + */ + private Integer ranking; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 活跃系数 + */ + private Double activeCoefficient; + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryTop100.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryTop100.java new file mode 100644 index 0000000..3c4f4df --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltFrontendHistoryTop100.java @@ -0,0 +1,36 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 大乐透前区百期数据排行表 + * @TableName dlt_frontend_history_top_100 + */ +@TableName(value ="dlt_frontend_history_top_100") +@Data +public class DltFrontendHistoryTop100 { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 排位 + */ + private Integer ranking; + + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 活跃系数 + */ + private Double activeCoefficient; + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/entity/DltPredictRecord.java b/src/main/java/com/xy/xyaicpzs/domain/entity/DltPredictRecord.java new file mode 100644 index 0000000..63f0ba5 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/entity/DltPredictRecord.java @@ -0,0 +1,92 @@ +package com.xy.xyaicpzs.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.util.Date; +import lombok.Data; + +/** + * 大乐透推测记录表 + * @TableName dlt_predict_record + */ +@TableName(value ="dlt_predict_record") +@Data +public class DltPredictRecord { + /** + * 唯一标识符 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 开奖期号 + */ + private Long drawId; + + /** + * 开奖日期 + */ + private Date drawDate; + + /** + * 前区1 + */ + private Integer frontendBall1; + + /** + * 前区2 + */ + private Integer frontendBall2; + + /** + * 前区3 + */ + private Integer frontendBall3; + + /** + * 前区4 + */ + private Integer frontendBall4; + + /** + * 前区5 + */ + private Integer frontendBall5; + + + /** + * 后区1 + */ + private Integer backendBall1; + + /** + * 后区2 + */ + private Integer backendBall2; + + /** + * 预测状态(待开奖/已开奖) + */ + private String predictStatus; + + /** + * 预测结果(未中奖/三等奖/二等奖/一等奖) + */ + private String predictResult; + + /** + * 预测时间 + */ + private Date predictTime; + + /** + * 奖金 + */ + private Long bonus; +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/BackBallPredictionResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/BackBallPredictionResultVO.java new file mode 100644 index 0000000..08d7f34 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/BackBallPredictionResultVO.java @@ -0,0 +1,14 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class BackBallPredictionResultVO { + private List result; + private String filteringProcess; +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/BallAnalysisResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/BallAnalysisResultVO.java new file mode 100644 index 0000000..b387608 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/BallAnalysisResultVO.java @@ -0,0 +1,55 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 球号分析结果VO(首球算法) + */ +@Data +@Builder +public class BallAnalysisResultVO { + + /** + * 分析结果:前11个球号 + */ + private List result; + + /** + * 筛选过程说明 + */ + private String filteringProcess; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/BlueBallAnalysisResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/BlueBallAnalysisResultVO.java new file mode 100644 index 0000000..314c46a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/BlueBallAnalysisResultVO.java @@ -0,0 +1,26 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 蓝球分析结果VO + */ +@Data +@Builder +public class BlueBallAnalysisResultVO { + + /** + * 分析结果:前4个蓝球号码 + */ + private List result; + + /** + * 筛选过程说明 + */ + private String filteringProcess; +} + + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/DLTFirstStepResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTFirstStepResultVO.java new file mode 100644 index 0000000..01897c0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTFirstStepResultVO.java @@ -0,0 +1,66 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 精推大乐透第一步分析结果VO + */ +@Data +@Builder +public class DLTFirstStepResultVO { + + /** + * 分析结果列表 + */ + private List results; + + /** + * 策略级别(H/M/L) + */ + private String strategy; + + /** + * 前区号码(5个) + */ + private List frontBalls; + + /** + * 后区号码(2个) + */ + private List backBalls; + + /** + * 球号分析结果 + */ + @Data + @Builder + public static class BallAnalysisResult { + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequency; + + /** + * 系数和 + */ + private Double coefficientSum; + + /** + * 百期排位(dlt_frontend_history_top_100表排位) + */ + private Integer top100Ranking; + + /** + * 历史排位(dlt_frontend_history_top表排位) + */ + private Integer historyRanking; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/DLTFourthStepResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTFourthStepResultVO.java new file mode 100644 index 0000000..0fcd7ae --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTFourthStepResultVO.java @@ -0,0 +1,76 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 精推大乐透第四步分析结果VO + */ +@Data +@Builder +public class DLTFourthStepResultVO { + + /** + * 分析结果列表 + */ + private List results; + + /** + * 策略级别(H/M/L) + */ + private String strategy; + + /** + * 上期前区号码(5个) + */ + private List previousFrontBalls; + + /** + * 上期后区号码(2个) + */ + private List previousBackBalls; + + /** + * 本期前区号码(5个) + */ + private List currentFrontBalls; + + /** + * 本期后区首球号码 + */ + private Integer currentBackFirstBall; + + /** + * 球号分析结果 + */ + @Data + @Builder + public static class BallAnalysisResult { + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequency; + + /** + * 系数和 + */ + private Double coefficientSum; + + /** + * 百期排位(dlt_backend_history_top_100表排位) + */ + private Integer top100Ranking; + + /** + * 历史排位(dlt_backend_history_top表排位) + */ + private Integer historyRanking; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/DLTSecondStepResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTSecondStepResultVO.java new file mode 100644 index 0000000..4dec643 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTSecondStepResultVO.java @@ -0,0 +1,71 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 精推大乐透第二步分析结果VO + */ +@Data +@Builder +public class DLTSecondStepResultVO { + + /** + * 分析结果列表 + */ + private List results; + + /** + * 策略级别(H/M/L) + */ + private String strategy; + + /** + * 上期前区号码(5个) + */ + private List previousFrontBalls; + + /** + * 上期后区号码(2个) + */ + private List previousBackBalls; + + /** + * 本期首球号码 + */ + private Integer currentFirstBall; + + /** + * 球号分析结果 + */ + @Data + @Builder + public static class BallAnalysisResult { + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequency; + + /** + * 系数和 + */ + private Double coefficientSum; + + /** + * 百期排位(dlt_frontend_history_top_100表排位) + */ + private Integer top100Ranking; + + /** + * 历史排位(dlt_frontend_history_top表排位) + */ + private Integer historyRanking; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/DLTThirdStepResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTThirdStepResultVO.java new file mode 100644 index 0000000..5d5c96e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/DLTThirdStepResultVO.java @@ -0,0 +1,71 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 精推大乐透第三步分析结果VO + */ +@Data +@Builder +public class DLTThirdStepResultVO { + + /** + * 分析结果列表 + */ + private List results; + + /** + * 策略级别(H/M/L) + */ + private String strategy; + + /** + * 上期前区号码(5个) + */ + private List previousFrontBalls; + + /** + * 上期后区号码(2个) + */ + private List previousBackBalls; + + /** + * 本期前区号码(5个) + */ + private List currentFrontBalls; + + /** + * 球号分析结果 + */ + @Data + @Builder + public static class BallAnalysisResult { + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现频次 + */ + private Integer frequency; + + /** + * 系数和 + */ + private Double coefficientSum; + + /** + * 百期排位(dlt_backend_history_top_100表排位) + */ + private Integer top100Ranking; + + /** + * 历史排位(dlt_backend_history_top表排位) + */ + private Integer historyRanking; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/FirstBallPredictionResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/FirstBallPredictionResultVO.java new file mode 100644 index 0000000..2a9bbfc --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/FirstBallPredictionResultVO.java @@ -0,0 +1,25 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 前区首球预测结果VO + */ +@Data +@Builder +public class FirstBallPredictionResultVO { + + /** + * 预测结果:前12个球号 + */ + private List result; + + /** + * 筛选过程说明 + */ + private String filteringProcess; +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/FollowBackBallPredictionResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/FollowBackBallPredictionResultVO.java new file mode 100644 index 0000000..c71e869 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/FollowBackBallPredictionResultVO.java @@ -0,0 +1,14 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class FollowBackBallPredictionResultVO { + private List result; + private String filteringProcess; +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/FollowBallAnalysisResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/FollowBallAnalysisResultVO.java new file mode 100644 index 0000000..d39a326 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/FollowBallAnalysisResultVO.java @@ -0,0 +1,26 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 跟随球号分析结果VO + */ +@Data +@Builder +public class FollowBallAnalysisResultVO { + + /** + * 分析结果:前8位数字 + */ + private List result; + + /** + * 筛选过程说明 + */ + private String filteringProcess; +} + + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/FollowerBallPredictionResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/FollowerBallPredictionResultVO.java new file mode 100644 index 0000000..f2a83ed --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/FollowerBallPredictionResultVO.java @@ -0,0 +1,14 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class FollowerBallPredictionResultVO { + private List result; + private String filteringProcess; +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/RegistrationTrendVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/RegistrationTrendVO.java new file mode 100644 index 0000000..0153606 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/RegistrationTrendVO.java @@ -0,0 +1,49 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +/** + * 用户注册趋势VO + */ +@Data +@Builder +public class RegistrationTrendVO { + /** + * 开始日期 + */ + private String startDate; + + /** + * 结束日期 + */ + private String endDate; + + /** + * 时间粒度 + */ + private String granularity; + + /** + * 用户注册趋势数据 (时间 -> 用户数) + */ + private Map userTrend; + + /** + * 会员注册趋势数据 (时间 -> 会员数) + */ + private Map vipTrend; + + /** + * 总用户数 + */ + private Integer totalUsers; + + /** + * 总会员数 + */ + private Long totalVips; +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/SSQFirstStepResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/SSQFirstStepResultVO.java new file mode 100644 index 0000000..35a9ea5 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/SSQFirstStepResultVO.java @@ -0,0 +1,66 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 双色球第一步分析结果VO + */ +@Data +@Builder +public class SSQFirstStepResultVO { + /** + * 分析结果列表 + */ + private List results; + + /** + * 分析策略 (H/M/L) + */ + private String strategy; + + /** + * 输入的红球号码 + */ + private List redBalls; + + /** + * 输入的蓝球号码 + */ + private Integer blueBall; + + /** + * 球号分析结果内部类 + */ + @Data + @Builder + public static class BallAnalysisResult { + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现次数 + */ + private Integer frequency; + + /** + * 系数和 + */ + private Double coefficientSum; + + /** + * 百期排位 + */ + private Integer top100Ranking; + + /** + * 历史排位 + */ + private Integer historyRanking; + } +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/SSQSecondStepResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/SSQSecondStepResultVO.java new file mode 100644 index 0000000..22d9e6a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/SSQSecondStepResultVO.java @@ -0,0 +1,70 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 双色球第二步分析结果VO + */ +@Data +@Builder +public class SSQSecondStepResultVO { + /** + * 分析结果列表 + */ + private List results; + + /** + * 分析策略 (H/M/L) + */ + private String strategy; + + /** + * 输入的红球号码 + */ + private List redBalls; + + /** + * 输入的蓝球号码 + */ + private Integer blueBall; + + /** + * 下期首球号码 + */ + private Integer nextFirstBall; + + /** + * 球号分析结果内部类 + */ + @Data + @Builder + public static class BallAnalysisResult { + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现次数 + */ + private Integer frequency; + + /** + * 系数和 + */ + private Double coefficientSum; + + /** + * 百期排位 + */ + private Integer top100Ranking; + + /** + * 历史排位 + */ + private Integer historyRanking; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/SSQThirdStepResultVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/SSQThirdStepResultVO.java new file mode 100644 index 0000000..a00329a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/SSQThirdStepResultVO.java @@ -0,0 +1,70 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 双色球第三步分析结果VO + */ +@Data +@Builder +public class SSQThirdStepResultVO { + /** + * 分析结果列表 + */ + private List results; + + /** + * 分析策略 (H/M/L) + */ + private String strategy; + + /** + * 输入的红球号码 + */ + private List redBalls; + + /** + * 输入的蓝球号码 + */ + private Integer blueBall; + + /** + * 下期红球号码 + */ + private List nextRedBalls; + + /** + * 球号分析结果内部类 + */ + @Data + @Builder + public static class BallAnalysisResult { + /** + * 球号 + */ + private Integer ballNumber; + + /** + * 出现次数 + */ + private Integer frequency; + + /** + * 系数和 + */ + private Double coefficientSum; + + /** + * 百期排位 + */ + private Integer top100Ranking; + + /** + * 历史排位 + */ + private Integer historyRanking; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/UserStatisticsVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/UserStatisticsVO.java new file mode 100644 index 0000000..3d24ab5 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/UserStatisticsVO.java @@ -0,0 +1,34 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 新增用户统计VO + */ +@Data +@Builder +public class UserStatisticsVO { + /** + * 新增用户总数 + */ + private Long totalNewUsers; + + /** + * 开始日期 + */ + private String startDate; + + /** + * 结束日期 + */ + private String endDate; + + /** + * 最近的新增用户列表 + */ + private List recentUsers; +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/VipDistributionVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/VipDistributionVO.java new file mode 100644 index 0000000..30897d2 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/VipDistributionVO.java @@ -0,0 +1,59 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.Date; + +/** + * 会员状态分布VO + */ +@Data +@Builder +public class VipDistributionVO { + /** + * 总用户数 + */ + private Long totalUsers; + + /** + * 普通用户数 + */ + private Long normalUsers; + + /** + * 普通用户百分比 + */ + private Double normalPercentage; + + /** + * 有效会员数 + */ + private Long activeVips; + + /** + * 有效会员百分比 + */ + private Double activeVipPercentage; + + /** + * 过期会员数 + */ + private Long expiredVips; + + /** + * 过期会员百分比 + */ + private Double expiredVipPercentage; + + /** + * 即将到期会员数(7天内) + */ + private Long soonExpireVips; + + /** + * 统计时间 + */ + private Date statisticsTime; +} + diff --git a/src/main/java/com/xy/xyaicpzs/domain/vo/VipStatisticsVO.java b/src/main/java/com/xy/xyaicpzs/domain/vo/VipStatisticsVO.java new file mode 100644 index 0000000..bfab34d --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/domain/vo/VipStatisticsVO.java @@ -0,0 +1,44 @@ +package com.xy.xyaicpzs.domain.vo; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +/** + * 新增会员统计VO + */ +@Data +@Builder +public class VipStatisticsVO { + /** + * 新增会员总数 + */ + private Long totalNewVips; + + /** + * 新增用户总数 + */ + private Long totalNewUsers; + + /** + * 会员转化率(百分比) + */ + private Double conversionRate; + + /** + * 开始日期 + */ + private String startDate; + + /** + * 结束日期 + */ + private String endDate; + + /** + * 最近的新增会员列表 + */ + private List recentVips; +} + diff --git a/src/main/java/com/xy/xyaicpzs/jt/jtdlt/BackBallAnalysis.java b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/BackBallAnalysis.java new file mode 100644 index 0000000..ebd7bf4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/BackBallAnalysis.java @@ -0,0 +1,788 @@ +package com.xy.xyaicpzs.jt.jtdlt; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.domain.vo.DLTThirdStepResultVO; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 精推大乐透第三步算法分析 + */ +@Slf4j +@Component +public class BackBallAnalysis { + + @Autowired + private D10Mapper d10Mapper; + + @Autowired + private D11Mapper d11Mapper; + + @Autowired + private D6Mapper d6Mapper; + + @Autowired + private DltBackendHistoryTopMapper dltBackendHistoryTopMapper; + + @Autowired + private DltBackendHistoryTop100Mapper dltBackendHistoryTop100Mapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + } + + /** + * 精推大乐透第三步算法主方法 + * @param level 位置级别(H-高位/M-中位/L-低位) + * @param previousFrontBalls 上期前区5个球号 + * @param previousBackBalls 上期后区2个球号 + * @param currentFrontBalls 本期前区5个球号 + * @return 分析结果 + */ + public DLTThirdStepResultVO analyze(String level, List previousFrontBalls, + List previousBackBalls, List currentFrontBalls) { + // 参数验证 + validateInputParams(level, previousFrontBalls, previousBackBalls, currentFrontBalls); + + log.info("开始精推大乐透第三步分析,策略:{},上期前区:{},上期后区:{},本期前区:{}", + level, previousFrontBalls, previousBackBalls, currentFrontBalls); + + // 用于存储所有候选球号和对应的系数 + Map> ballCoefficientMap = new HashMap<>(); + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据5个上期前区球号从D10表获取候选球(取前10个) + for (Integer previousFrontBall : previousFrontBalls) { + List ballsWithCoefficients = getD10BallsByLevel(previousFrontBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从dlt_backend_history_top获取前2个球号 + List top2HistoryTop = getTop2FromDltBackendHistoryTop(); + allCandidateBalls.addAll(top2HistoryTop); + + // Step 3: 从dlt_backend_history_top_100获取前2个球号 + List top2HistoryTop100 = getTop2FromDltBackendHistoryTop100(); + allCandidateBalls.addAll(top2HistoryTop100); + + // Step 4: 根据2个上期后区球号从D11表获取候选球(取前10个) + for (Integer previousBackBall : previousBackBalls) { + List ballsWithCoefficients = getD11BallsByLevel(previousBackBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 5: 根据5个本期前区球号从D6表获取候选球(根据策略取不同数量) + for (Integer currentFrontBall : currentFrontBalls) { + List ballsFromD6 = getD6BallsByLevel(currentFrontBall, level); + for (BallWithCoefficient ball : ballsFromD6) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 6: 统计分析并生成结果 + List results = analyzeResults(allCandidateBalls, ballCoefficientMap); + + return DLTThirdStepResultVO.builder() + .results(results) + .strategy(level) + .previousFrontBalls(previousFrontBalls) + .previousBackBalls(previousBackBalls) + .currentFrontBalls(currentFrontBalls) + .build(); + } + + /** + * 根据级别从D10表获取候选球号和系数(取前10个,如果第10个系数相同则一并加入) + */ + private List getD10BallsByLevel(Integer masterBallNumber, String level) { + List d10List = d10Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d10List)) { + log.warn("D10表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + // 所有策略都取前10个,如果第10个系数相同则一并加入 + List result = new ArrayList<>(); + + if (d10List.size() <= 10) { + // 如果总数不超过10个,全部加入 + for (D10 d10 : d10List) { + result.add(new BallWithCoefficient(d10.getSlaveBallNumber(), d10.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前10个 + for (int i = 0; i < 10; i++) { + D10 d10 = d10List.get(i); + result.add(new BallWithCoefficient(d10.getSlaveBallNumber(), d10.getCoefficient(), masterBallNumber)); + } + + // 获取第10个球号的系数 + Double boundaryCoefficient = d10List.get(9).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 10; i < d10List.size(); i++) { + D10 d10 = d10List.get(i); + if (d10.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d10.getSlaveBallNumber(), d10.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 根据级别从D11表获取候选球号和系数(取前10个,如果第10个系数相同则一并加入) + */ + private List getD11BallsByLevel(Integer masterBallNumber, String level) { + List d11List = d11Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d11List)) { + log.warn("D11表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + // 所有策略都取前10个,如果第10个系数相同则一并加入 + List result = new ArrayList<>(); + + if (d11List.size() <= 10) { + // 如果总数不超过10个,全部加入 + for (D11 d11 : d11List) { + result.add(new BallWithCoefficient(d11.getSlaveBallNumber(), d11.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前10个 + for (int i = 0; i < 10; i++) { + D11 d11 = d11List.get(i); + result.add(new BallWithCoefficient(d11.getSlaveBallNumber(), d11.getCoefficient(), masterBallNumber)); + } + + // 获取第10个球号的系数 + Double boundaryCoefficient = d11List.get(9).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 10; i < d11List.size(); i++) { + D11 d11 = d11List.get(i); + if (d11.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d11.getSlaveBallNumber(), d11.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 根据级别从D6表获取候选球号和系数 + */ + private List getD6BallsByLevel(Integer masterBallNumber, String level) { + List d6List = d6Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d6List)) { + log.warn("D6表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromD6(d6List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromD6(d6List, masterBallNumber); + } else { // L + return getLowLevelBallsFromD6(d6List, masterBallNumber); + } + } + + /** + * 高位策略:从D6表获取系数最大的前5个球号(如果第5个系数相同则一并加入) + */ + private List getHighLevelBallsFromD6(List d6List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (d6List.size() <= 5) { + // 如果总数不超过5个,全部加入 + for (D6 d6 : d6List) { + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前5个 + for (int i = 0; i < 5; i++) { + D6 d6 = d6List.get(i); + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } + + // 获取第5个球号的系数 + Double boundaryCoefficient = d6List.get(4).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 5; i < d6List.size(); i++) { + D6 d6 = d6List.get(i); + if (d6.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 中位策略:从D6表获取平均值附近的5个球号(向上2个,向下2个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromD6(List d6List, Integer masterBallNumber) { + if (d6List.size() < 5) { + log.warn("D6表数据不足5条,实际{}条", d6List.size()); + List result = new ArrayList<>(); + for (D6 d6 : d6List) { + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = d6List.stream() + .mapToDouble(D6::getCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d6List.size() - 1; i >= 0; i--) { + if (d6List.get(i).getCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d6List.get(avgPosition).getSlaveBallNumber(), + d6List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取2个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 2; i--) { + result.add(new BallWithCoefficient(d6List.get(i).getSlaveBallNumber(), + d6List.get(i).getCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 2) { + upBoundaryCoeff = d6List.get(i).getCoefficient(); + } + } + + // 向上边界处理:继续添加与第2个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 3; i >= 0; i--) { + if (d6List.get(i).getCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(d6List.get(i).getSlaveBallNumber(), + d6List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取2个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < d6List.size() && downCount < 2; i++) { + result.add(new BallWithCoefficient(d6List.get(i).getSlaveBallNumber(), + d6List.get(i).getCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 2) { + downBoundaryCoeff = d6List.get(i).getCoefficient(); + } + } + + // 向下边界处理:继续添加与第2个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 3; i < d6List.size(); i++) { + if (d6List.get(i).getCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(d6List.get(i).getSlaveBallNumber(), + d6List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 低位策略:从D6表获取最小值向上5个球号(含最小值,第1个系数相同则一并加入) + */ + private List getLowLevelBallsFromD6(List d6List, Integer masterBallNumber) { + if (d6List.size() < 5) { + log.warn("D6表数据不足5条,实际{}条", d6List.size()); + List result = new ArrayList<>(); + for (D6 d6 : d6List) { + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 从最小值开始向上取5个(d6List已按系数降序排列,最后5个就是最小的) + List result = new ArrayList<>(); + int startIndex = Math.max(0, d6List.size() - 5); + + // 先加入基本的5个球号 + for (int i = startIndex; i < d6List.size(); i++) { + D6 d6 = d6List.get(i); + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } + + // 处理第1个球号(系数最大的)边界相同情况 + if (startIndex > 0) { + Double firstBallCoeff = d6List.get(startIndex).getCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (d6List.get(i).getCoefficient().equals(firstBallCoeff)) { + result.add(new BallWithCoefficient(d6List.get(i).getSlaveBallNumber(), + d6List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 从dlt_backend_history_top获取前2个球号(按点系数排行,处理边界相同系数) + */ + private List getTop2FromDltBackendHistoryTop() { + List historyTopList = dltBackendHistoryTopMapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(historyTopList)) { + log.warn("dlt_backend_history_top数据为空"); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 2 && index < historyTopList.size()) { + double currentCoefficient = historyTopList.get(index).getActiveCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTopList.size() && + historyTopList.get(index).getActiveCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTopList.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(2 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理边界冲突 + List selectedBalls = handleBackendHistoryBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 从dlt_backend_history_top_100获取前2个球号(按点系数排行,处理边界相同系数) + */ + private List getTop2FromDltBackendHistoryTop100() { + List historyTop100List = dltBackendHistoryTop100Mapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(historyTop100List)) { + log.warn("dlt_backend_history_top_100数据为空"); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 2 && index < historyTop100List.size()) { + double currentCoefficient = historyTop100List.get(index).getActiveCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTop100List.size() && + historyTop100List.get(index).getActiveCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTop100List.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(2 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理边界冲突 + List selectedBalls = handleBackendHistoryBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 处理后区历史排行边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleBackendHistoryBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 1. 先从dlt_backend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop100 record = dltBackendHistoryTop100Mapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 按系数降序排序 + List> sortedByTop100 = top100Coefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + + // 按系数分组处理 + int currentIndex = 0; + while (result.size() < selectCount && currentIndex < sortedByTop100.size()) { + Double currentCoefficient = sortedByTop100.get(currentIndex).getValue(); + List sameTop100CoefficientBalls = new ArrayList<>(); + + // 收集相同系数的球号 + while (currentIndex < sortedByTop100.size() && + sortedByTop100.get(currentIndex).getValue().equals(currentCoefficient)) { + sameTop100CoefficientBalls.add(sortedByTop100.get(currentIndex).getKey()); + currentIndex++; + } + + int needFromThisGroup = Math.min(selectCount - result.size(), sameTop100CoefficientBalls.size()); + + if (sameTop100CoefficientBalls.size() == 1 || needFromThisGroup == sameTop100CoefficientBalls.size()) { + // 只有一个球或者需要全部选择 + for (int i = 0; i < needFromThisGroup; i++) { + result.add(sameTop100CoefficientBalls.get(i)); + } + } else { + // 需要进一步筛选,使用dlt_backend_history_top表 + List selectedFromTop = selectFromBackendHistoryTop(sameTop100CoefficientBalls, needFromThisGroup); + result.addAll(selectedFromTop); + } + } + + return result; + } + + /** + * 从dlt_backend_history_top表中选择指定数量的球号 + */ + private List selectFromBackendHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在dlt_backend_history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 统计分析并生成结果 + */ + private List analyzeResults( + List allCandidateBalls, + Map> ballCoefficientMap) { + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 计算系数和 + Map ballCoefficientSumMap = new HashMap<>(); + for (Map.Entry> entry : ballCoefficientMap.entrySet()) { + Integer ballNumber = entry.getKey(); + List coefficients = entry.getValue(); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSumMap.put(ballNumber, sum); + } + + // 获取所有有排行数据的球号(包括频次为0的球号) + Set allBallsWithRanking = getAllBallsWithRanking(); + + // 获取百期排位和历史排位 + Map top100RankingMap = getTop100Rankings(allBallsWithRanking); + Map historyRankingMap = getHistoryRankings(allBallsWithRanking); + + // 组装结果 + List results = new ArrayList<>(); + for (Integer ballNumber : allBallsWithRanking) { + DLTThirdStepResultVO.BallAnalysisResult result = DLTThirdStepResultVO.BallAnalysisResult.builder() + .ballNumber(ballNumber) + .frequency(ballFrequencyMap.getOrDefault(ballNumber, 0)) + .coefficientSum(ballCoefficientSumMap.getOrDefault(ballNumber, 0.0)) + .top100Ranking(top100RankingMap.get(ballNumber)) + .historyRanking(historyRankingMap.get(ballNumber)) + .build(); + results.add(result); + } + + // 按出现次数降序排列,次数相同的按球号升序排列 + results.sort((a, b) -> { + int frequencyCompare = b.getFrequency().compareTo(a.getFrequency()); + if (frequencyCompare != 0) { + return frequencyCompare; + } + return a.getBallNumber().compareTo(b.getBallNumber()); + }); + + return results; + } + + /** + * 获取所有有排行数据的球号(包括频次为0的球号) + */ + private Set getAllBallsWithRanking() { + Set allBalls = new HashSet<>(); + + // 从DLT后区百期排行表获取所有球号 + List top100List = dltBackendHistoryTop100Mapper.selectList( + new QueryWrapper()); + for (DltBackendHistoryTop100 item : top100List) { + allBalls.add(item.getBallNumber()); + } + + // 从DLT后区历史排行表获取所有球号 + List historyTopList = dltBackendHistoryTopMapper.selectList( + new QueryWrapper()); + for (DltBackendHistoryTop item : historyTopList) { + allBalls.add(item.getBallNumber()); + } + + return allBalls; + } + + /** + * 获取球号在百期排行表中的排位 + */ + private Map getTop100Rankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List top100List = dltBackendHistoryTop100Mapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltBackendHistoryTop100 item : top100List) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在百期排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 获取球号在历史排行表中的排位 + */ + private Map getHistoryRankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List historyTopList = dltBackendHistoryTopMapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltBackendHistoryTop item : historyTopList) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在历史排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 参数验证 + */ + private void validateInputParams(String level, List previousFrontBalls, + List previousBackBalls, List currentFrontBalls) { + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是H/M/L之一"); + } + + if (CollectionUtils.isEmpty(previousFrontBalls) || previousFrontBalls.size() != 5) { + throw new IllegalArgumentException("上期前区球号必须为5个"); + } + + if (CollectionUtils.isEmpty(previousBackBalls) || previousBackBalls.size() != 2) { + throw new IllegalArgumentException("上期后区球号必须为2个"); + } + + if (CollectionUtils.isEmpty(currentFrontBalls) || currentFrontBalls.size() != 5) { + throw new IllegalArgumentException("本期前区球号必须为5个"); + } + + for (Integer frontBall : previousFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("上期前区球号范围应为1-35"); + } + } + + for (Integer backBall : previousBackBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("上期后区球号范围应为1-12"); + } + } + + for (Integer frontBall : currentFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("本期前区球号范围应为1-35"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FirstFrontBallAnalysis.java b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FirstFrontBallAnalysis.java new file mode 100644 index 0000000..5a7964c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FirstFrontBallAnalysis.java @@ -0,0 +1,858 @@ +package com.xy.xyaicpzs.jt.jtdlt; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.domain.vo.DLTFirstStepResultVO; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 精推大乐透第一步算法分析 + */ +@Slf4j +@Component +public class FirstFrontBallAnalysis { + + @Autowired + private D9Mapper d9Mapper; + + @Autowired + private D12Mapper d12Mapper; + + @Autowired + private DltFrontendHistoryTopMapper dltFrontendHistoryTopMapper; + + @Autowired + private DltFrontendHistoryTop100Mapper dltFrontendHistoryTop100Mapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + } + + /** + * 精推大乐透第一步算法主方法 + * @param level 位置级别(H-高位/M-中位/L-低位) + * @param frontBalls 前区5个球号 + * @param backBalls 后区2个球号 + * @return 分析结果 + */ + public DLTFirstStepResultVO analyze(String level, List frontBalls, List backBalls) { + // 参数验证 + validateInputParams(level, frontBalls, backBalls); + + log.info("开始精推大乐透第一步分析,策略:{},前区:{},后区:{}", level, frontBalls, backBalls); + + // 用于存储所有候选球号和对应的系数 + Map> ballCoefficientMap = new HashMap<>(); + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据5个前区球号从D9表获取候选球 + for (Integer frontBall : frontBalls) { + List ballsWithCoefficients = getD9BallsByLevel(frontBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从dlt_frontend_history_top获取前3个球号 + List top3HistoryTop = getTop3FromDltFrontendHistoryTop(); + allCandidateBalls.addAll(top3HistoryTop); + + // Step 3: 从dlt_frontend_history_top_100获取前3个球号 + List top3HistoryTop100 = getTop3FromDltFrontendHistoryTop100(); + allCandidateBalls.addAll(top3HistoryTop100); + + // Step 4: 根据2个后区球号从D12表获取候选球 + for (Integer backBall : backBalls) { + List ballsWithCoefficients = getD12BallsByLevel(backBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 5: 统计分析并生成结果 + List results = analyzeResults(allCandidateBalls, ballCoefficientMap); + + return DLTFirstStepResultVO.builder() + .results(results) + .strategy(level) + .frontBalls(frontBalls) + .backBalls(backBalls) + .build(); + } + + /** + * 根据级别从D9表获取候选球号和系数 + */ + private List getD9BallsByLevel(Integer masterBallNumber, String level) { + List d9List = d9Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d9List)) { + log.warn("D9表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromD9(d9List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromD9(d9List, masterBallNumber); + } else { // L + return getLowLevelBallsFromD9(d9List, masterBallNumber); + } + } + + /** + * 根据级别从D12表获取候选球号和系数 + */ + private List getD12BallsByLevel(Integer masterBallNumber, String level) { + List d12List = d12Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d12List)) { + log.warn("D12表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromD12(d12List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromD12(d12List, masterBallNumber); + } else { // L + return getLowLevelBallsFromD12(d12List, masterBallNumber); + } + } + + /** + * 高位策略:从D9表获取系数最大的前17个球号(如果第17个系数相同则一并加入) + */ + private List getHighLevelBallsFromD9(List d9List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (d9List.size() <= 17) { + // 如果总数不超过17个,全部加入 + for (D9 d9 : d9List) { + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前17个 + for (int i = 0; i < 17; i++) { + D9 d9 = d9List.get(i); + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } + + // 获取第17个球号的系数 + Double boundaryCoefficient = d9List.get(16).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 17; i < d9List.size(); i++) { + D9 d9 = d9List.get(i); + if (d9.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 高位策略:从D12表获取系数最大的前17个球号(如果第17个系数相同则一并加入) + */ + private List getHighLevelBallsFromD12(List d12List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (d12List.size() <= 17) { + // 如果总数不超过17个,全部加入 + for (D12 d12 : d12List) { + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前17个 + for (int i = 0; i < 17; i++) { + D12 d12 = d12List.get(i); + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } + + // 获取第17个球号的系数 + Double boundaryCoefficient = d12List.get(16).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 17; i < d12List.size(); i++) { + D12 d12 = d12List.get(i); + if (d12.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 中位策略:从D9表获取平均值附近的17个球号(向上8个,向下8个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromD9(List d9List, Integer masterBallNumber) { + if (d9List.size() < 17) { + log.warn("D9表数据不足17条,实际{}条", d9List.size()); + List result = new ArrayList<>(); + for (D9 d9 : d9List) { + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = d9List.stream() + .mapToDouble(D9::getCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d9List.size() - 1; i >= 0; i--) { + if (d9List.get(i).getCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d9List.get(avgPosition).getSlaveBallNumber(), + d9List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取8个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 8; i--) { + result.add(new BallWithCoefficient(d9List.get(i).getSlaveBallNumber(), + d9List.get(i).getCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 8) { + upBoundaryCoeff = d9List.get(i).getCoefficient(); + } + } + + // 向上边界处理:继续添加与第8个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 9; i >= 0; i--) { + if (d9List.get(i).getCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(d9List.get(i).getSlaveBallNumber(), + d9List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取8个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < d9List.size() && downCount < 8; i++) { + result.add(new BallWithCoefficient(d9List.get(i).getSlaveBallNumber(), + d9List.get(i).getCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 8) { + downBoundaryCoeff = d9List.get(i).getCoefficient(); + } + } + + // 向下边界处理:继续添加与第8个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 9; i < d9List.size(); i++) { + if (d9List.get(i).getCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(d9List.get(i).getSlaveBallNumber(), + d9List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 中位策略:从D12表获取平均值附近的17个球号(向上8个,向下8个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromD12(List d12List, Integer masterBallNumber) { + if (d12List.size() < 17) { + log.warn("D12表数据不足17条,实际{}条", d12List.size()); + List result = new ArrayList<>(); + for (D12 d12 : d12List) { + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = d12List.stream() + .mapToDouble(D12::getCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d12List.size() - 1; i >= 0; i--) { + if (d12List.get(i).getCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d12List.get(avgPosition).getSlaveBallNumber(), + d12List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取8个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 8; i--) { + result.add(new BallWithCoefficient(d12List.get(i).getSlaveBallNumber(), + d12List.get(i).getCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 8) { + upBoundaryCoeff = d12List.get(i).getCoefficient(); + } + } + + // 向上边界处理:继续添加与第8个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 9; i >= 0; i--) { + if (d12List.get(i).getCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(d12List.get(i).getSlaveBallNumber(), + d12List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取8个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < d12List.size() && downCount < 8; i++) { + result.add(new BallWithCoefficient(d12List.get(i).getSlaveBallNumber(), + d12List.get(i).getCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 8) { + downBoundaryCoeff = d12List.get(i).getCoefficient(); + } + } + + // 向下边界处理:继续添加与第8个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 9; i < d12List.size(); i++) { + if (d12List.get(i).getCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(d12List.get(i).getSlaveBallNumber(), + d12List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 低位策略:从D9表获取最小值向上17个球号(含最小值,第1个系数相同则一并加入) + */ + private List getLowLevelBallsFromD9(List d9List, Integer masterBallNumber) { + if (d9List.size() < 17) { + log.warn("D9表数据不足17条,实际{}条", d9List.size()); + List result = new ArrayList<>(); + for (D9 d9 : d9List) { + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 从最小值开始向上取17个(d9List已按系数降序排列,最后17个就是最小的) + List result = new ArrayList<>(); + int startIndex = Math.max(0, d9List.size() - 17); + + // 先加入基本的17个球号 + for (int i = startIndex; i < d9List.size(); i++) { + D9 d9 = d9List.get(i); + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } + + // 处理第1个球号(系数最大的)边界相同情况 + if (startIndex > 0) { + Double firstBallCoeff = d9List.get(startIndex).getCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (d9List.get(i).getCoefficient().equals(firstBallCoeff)) { + result.add(new BallWithCoefficient(d9List.get(i).getSlaveBallNumber(), + d9List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 低位策略:从D12表获取最小值向上17个球号(含最小值,第1个系数相同则一并加入) + */ + private List getLowLevelBallsFromD12(List d12List, Integer masterBallNumber) { + if (d12List.size() < 17) { + log.warn("D12表数据不足17条,实际{}条", d12List.size()); + List result = new ArrayList<>(); + for (D12 d12 : d12List) { + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 从最小值开始向上取17个(d12List已按系数降序排列,最后17个就是最小的) + List result = new ArrayList<>(); + int startIndex = Math.max(0, d12List.size() - 17); + + // 先加入基本的17个球号 + for (int i = startIndex; i < d12List.size(); i++) { + D12 d12 = d12List.get(i); + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } + + // 处理第1个球号(系数最大的)边界相同情况 + if (startIndex > 0) { + Double firstBallCoeff = d12List.get(startIndex).getCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (d12List.get(i).getCoefficient().equals(firstBallCoeff)) { + result.add(new BallWithCoefficient(d12List.get(i).getSlaveBallNumber(), + d12List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 从dlt_frontend_history_top获取前3个球号(按点系数排行,处理边界相同系数) + */ + private List getTop3FromDltFrontendHistoryTop() { + List historyTopList = dltFrontendHistoryTopMapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(historyTopList)) { + log.warn("dlt_frontend_history_top数据为空"); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 3 && index < historyTopList.size()) { + double currentCoefficient = historyTopList.get(index).getActiveCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTopList.size() && + historyTopList.get(index).getActiveCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTopList.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(3 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理边界冲突 + List selectedBalls = handleFrontendHistoryBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 从dlt_frontend_history_top_100获取前3个球号(按点系数排行,处理边界相同系数) + */ + private List getTop3FromDltFrontendHistoryTop100() { + List historyTop100List = dltFrontendHistoryTop100Mapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(historyTop100List)) { + log.warn("dlt_frontend_history_top_100数据为空"); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 3 && index < historyTop100List.size()) { + double currentCoefficient = historyTop100List.get(index).getActiveCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTop100List.size() && + historyTop100List.get(index).getActiveCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTop100List.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(3 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理边界冲突 + List selectedBalls = handleFrontendHistoryBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 处理前区历史排行边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleFrontendHistoryBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 1. 先从dlt_frontend_history_top_100比较 + Map top100Coefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop100 record = dltFrontendHistoryTop100Mapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ball)); + if (record != null) { + top100Coefficients.put(ball, record.getActiveCoefficient()); + } else { + top100Coefficients.put(ball, 0.0); + } + } + + // 按系数降序排序 + List> sortedByTop100 = top100Coefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + + // 按系数分组处理 + int currentIndex = 0; + while (result.size() < selectCount && currentIndex < sortedByTop100.size()) { + Double currentCoefficient = sortedByTop100.get(currentIndex).getValue(); + List sameTop100CoefficientBalls = new ArrayList<>(); + + // 收集相同系数的球号 + while (currentIndex < sortedByTop100.size() && + sortedByTop100.get(currentIndex).getValue().equals(currentCoefficient)) { + sameTop100CoefficientBalls.add(sortedByTop100.get(currentIndex).getKey()); + currentIndex++; + } + + int needFromThisGroup = Math.min(selectCount - result.size(), sameTop100CoefficientBalls.size()); + + if (sameTop100CoefficientBalls.size() == 1 || needFromThisGroup == sameTop100CoefficientBalls.size()) { + // 只有一个球或者需要全部选择 + for (int i = 0; i < needFromThisGroup; i++) { + result.add(sameTop100CoefficientBalls.get(i)); + } + } else { + // 需要进一步筛选,使用dlt_frontend_history_top表 + List selectedFromTop = selectFromFrontendHistoryTop(sameTop100CoefficientBalls, needFromThisGroup); + result.addAll(selectedFromTop); + } + } + + return result; + } + + /** + * 从dlt_frontend_history_top表中选择指定数量的球号 + */ + private List selectFromFrontendHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在dlt_frontend_history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ball)); + if (record != null) { + topCoefficients.put(ball, record.getActiveCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 统计分析并生成结果 + */ + private List analyzeResults( + List allCandidateBalls, + Map> ballCoefficientMap) { + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 计算系数和 + Map ballCoefficientSumMap = new HashMap<>(); + for (Map.Entry> entry : ballCoefficientMap.entrySet()) { + Integer ballNumber = entry.getKey(); + List coefficients = entry.getValue(); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSumMap.put(ballNumber, sum); + } + + // 获取所有有排行数据的球号(包括频次为0的球号) + Set allBallsWithRanking = getAllBallsWithRanking(); + + // 获取百期排位和历史排位 + Map top100RankingMap = getTop100Rankings(allBallsWithRanking); + Map historyRankingMap = getHistoryRankings(allBallsWithRanking); + + // 组装结果 + List results = new ArrayList<>(); + for (Integer ballNumber : allBallsWithRanking) { + DLTFirstStepResultVO.BallAnalysisResult result = DLTFirstStepResultVO.BallAnalysisResult.builder() + .ballNumber(ballNumber) + .frequency(ballFrequencyMap.getOrDefault(ballNumber, 0)) + .coefficientSum(ballCoefficientSumMap.getOrDefault(ballNumber, 0.0)) + .top100Ranking(top100RankingMap.get(ballNumber)) + .historyRanking(historyRankingMap.get(ballNumber)) + .build(); + results.add(result); + } + + // 按出现次数降序排列,次数相同的按球号升序排列 + results.sort((a, b) -> { + int frequencyCompare = b.getFrequency().compareTo(a.getFrequency()); + if (frequencyCompare != 0) { + return frequencyCompare; + } + return a.getBallNumber().compareTo(b.getBallNumber()); + }); + + return results; + } + + /** + * 获取所有有排行数据的球号(包括频次为0的球号) + */ + private Set getAllBallsWithRanking() { + Set allBalls = new HashSet<>(); + + // 从DLT前区百期排行表获取所有球号 + List top100List = dltFrontendHistoryTop100Mapper.selectList( + new QueryWrapper()); + for (DltFrontendHistoryTop100 item : top100List) { + allBalls.add(item.getBallNumber()); + } + + // 从DLT前区历史排行表获取所有球号 + List historyTopList = dltFrontendHistoryTopMapper.selectList( + new QueryWrapper()); + for (DltFrontendHistoryTop item : historyTopList) { + allBalls.add(item.getBallNumber()); + } + + return allBalls; + } + + /** + * 获取球号在百期排行表中的排位 + */ + private Map getTop100Rankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List top100List = dltFrontendHistoryTop100Mapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltFrontendHistoryTop100 item : top100List) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在百期排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 获取球号在历史排行表中的排位 + */ + private Map getHistoryRankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List historyTopList = dltFrontendHistoryTopMapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltFrontendHistoryTop item : historyTopList) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在历史排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 参数验证 + */ + private void validateInputParams(String level, List frontBalls, List backBalls) { + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是H/M/L之一"); + } + + if (CollectionUtils.isEmpty(frontBalls) || frontBalls.size() != 5) { + throw new IllegalArgumentException("前区球号必须为5个"); + } + + if (CollectionUtils.isEmpty(backBalls) || backBalls.size() != 2) { + throw new IllegalArgumentException("后区球号必须为2个"); + } + + for (Integer frontBall : frontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("前区球号范围应为1-35"); + } + } + + for (Integer backBall : backBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("后区球号范围应为1-12"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FollowBackendBallAnalysis.java b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FollowBackendBallAnalysis.java new file mode 100644 index 0000000..0883389 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FollowBackendBallAnalysis.java @@ -0,0 +1,769 @@ +package com.xy.xyaicpzs.jt.jtdlt; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.domain.vo.DLTFourthStepResultVO; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 精推大乐透第四步算法分析 + */ +@Slf4j +@Component +public class FollowBackendBallAnalysis { + + @Autowired + private D10Mapper d10Mapper; + + @Autowired + private D11Mapper d11Mapper; + + @Autowired + private D6Mapper d6Mapper; + + @Autowired + private D7Mapper d7Mapper; + + @Autowired + private DltBackendHistoryTopMapper dltBackendHistoryTopMapper; + + @Autowired + private DltBackendHistoryTop100Mapper dltBackendHistoryTop100Mapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + } + + /** + * 精推大乐透第四步算法主方法 + * @param level 位置级别(H-高位/M-中位/L-低位) + * @param previousFrontBalls 上期前区5个球号 + * @param previousBackBalls 上期后区2个球号 + * @param currentFrontBalls 本期前区5个球号 + * @param currentBackFirstBall 本期后区首球号码 + * @return 分析结果 + */ + public DLTFourthStepResultVO analyze(String level, List previousFrontBalls, + List previousBackBalls, List currentFrontBalls, + Integer currentBackFirstBall) { + // 参数验证 + validateInputParams(level, previousFrontBalls, previousBackBalls, currentFrontBalls, currentBackFirstBall); + + log.info("开始精推大乐透第四步分析,策略:{},上期前区:{},上期后区:{},本期前区:{},本期后区首球:{}", + level, previousFrontBalls, previousBackBalls, currentFrontBalls, currentBackFirstBall); + + // 用于存储所有候选球号和对应的系数 + Map> ballCoefficientMap = new HashMap<>(); + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据5个上期前区球号从D10表获取候选球(取前10个) + for (Integer previousFrontBall : previousFrontBalls) { + List ballsWithCoefficients = getD10BallsByLevel(previousFrontBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从dlt_backend_history_top取平均值向上连续2个球号 + List top2HistoryTop = getTop2FromDltBackendHistoryTopByAvg(); + allCandidateBalls.addAll(top2HistoryTop); + + // Step 3: 从dlt_backend_history_top_100取平均值向上连续2个球号 + List top2HistoryTop100 = getTop2FromDltBackendHistoryTop100ByAvg(); + allCandidateBalls.addAll(top2HistoryTop100); + + // Step 4: 根据2个上期后区球号从D11表获取候选球(取前10个) + for (Integer previousBackBall : previousBackBalls) { + List ballsWithCoefficients = getD11BallsByLevel(previousBackBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 5: 根据5个本期前区球号从D6表获取候选球(取前10个) + for (Integer currentFrontBall : currentFrontBalls) { + List ballsWithCoefficients = getD6BallsByLevel(currentFrontBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 6: 根据1个本期后区首球从D7表获取候选球(根据策略取不同数量) + List ballsFromD7 = getD7BallsByLevel(currentBackFirstBall, level); + for (BallWithCoefficient ball : ballsFromD7) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // Step 7: 统计分析并生成结果 + List results = analyzeResults(allCandidateBalls, ballCoefficientMap); + + return DLTFourthStepResultVO.builder() + .results(results) + .strategy(level) + .previousFrontBalls(previousFrontBalls) + .previousBackBalls(previousBackBalls) + .currentFrontBalls(currentFrontBalls) + .currentBackFirstBall(currentBackFirstBall) + .build(); + } + + /** + * 根据级别从D10表获取候选球号和系数(取前10个,如果第10个系数相同则一并加入) + */ + private List getD10BallsByLevel(Integer masterBallNumber, String level) { + List d10List = d10Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d10List)) { + log.warn("D10表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + // 所有策略都取前10个,如果第10个系数相同则一并加入 + List result = new ArrayList<>(); + + if (d10List.size() <= 10) { + // 如果总数不超过10个,全部加入 + for (D10 d10 : d10List) { + result.add(new BallWithCoefficient(d10.getSlaveBallNumber(), d10.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前10个 + for (int i = 0; i < 10; i++) { + D10 d10 = d10List.get(i); + result.add(new BallWithCoefficient(d10.getSlaveBallNumber(), d10.getCoefficient(), masterBallNumber)); + } + + // 获取第10个球号的系数 + Double boundaryCoefficient = d10List.get(9).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 10; i < d10List.size(); i++) { + D10 d10 = d10List.get(i); + if (d10.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d10.getSlaveBallNumber(), d10.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 根据级别从D11表获取候选球号和系数(取前10个,如果第10个系数相同则一并加入) + */ + private List getD11BallsByLevel(Integer masterBallNumber, String level) { + List d11List = d11Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d11List)) { + log.warn("D11表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + // 所有策略都取前10个,如果第10个系数相同则一并加入 + List result = new ArrayList<>(); + + if (d11List.size() <= 10) { + // 如果总数不超过10个,全部加入 + for (D11 d11 : d11List) { + result.add(new BallWithCoefficient(d11.getSlaveBallNumber(), d11.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前10个 + for (int i = 0; i < 10; i++) { + D11 d11 = d11List.get(i); + result.add(new BallWithCoefficient(d11.getSlaveBallNumber(), d11.getCoefficient(), masterBallNumber)); + } + + // 获取第10个球号的系数 + Double boundaryCoefficient = d11List.get(9).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 10; i < d11List.size(); i++) { + D11 d11 = d11List.get(i); + if (d11.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d11.getSlaveBallNumber(), d11.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 根据级别从D6表获取候选球号和系数(取前10个,如果第10个系数相同则一并加入) + */ + private List getD6BallsByLevel(Integer masterBallNumber, String level) { + List d6List = d6Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d6List)) { + log.warn("D6表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + // 所有策略都取前10个,如果第10个系数相同则一并加入 + List result = new ArrayList<>(); + + if (d6List.size() <= 10) { + // 如果总数不超过10个,全部加入 + for (D6 d6 : d6List) { + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前10个 + for (int i = 0; i < 10; i++) { + D6 d6 = d6List.get(i); + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } + + // 获取第10个球号的系数 + Double boundaryCoefficient = d6List.get(9).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 10; i < d6List.size(); i++) { + D6 d6 = d6List.get(i); + if (d6.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d6.getSlaveBallNumber(), d6.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 根据级别从D7表获取候选球号和系数 + */ + private List getD7BallsByLevel(Integer masterBallNumber, String level) { + List d7List = d7Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d7List)) { + log.warn("D7表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromD7(d7List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromD7(d7List, masterBallNumber); + } else { // L + return getLowLevelBallsFromD7(d7List, masterBallNumber); + } + } + + /** + * 高位策略:从D7表获取系数最大的前5个球号(如果第5个系数相同则一并加入) + */ + private List getHighLevelBallsFromD7(List d7List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (d7List.size() <= 5) { + // 如果总数不超过5个,全部加入 + for (D7 d7 : d7List) { + result.add(new BallWithCoefficient(d7.getSlaveBallNumber(), d7.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前5个 + for (int i = 0; i < 5; i++) { + D7 d7 = d7List.get(i); + result.add(new BallWithCoefficient(d7.getSlaveBallNumber(), d7.getCoefficient(), masterBallNumber)); + } + + // 获取第5个球号的系数 + Double boundaryCoefficient = d7List.get(4).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 5; i < d7List.size(); i++) { + D7 d7 = d7List.get(i); + if (d7.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d7.getSlaveBallNumber(), d7.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 中位策略:从D7表获取平均值附近的5个球号(向上2个,向下2个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromD7(List d7List, Integer masterBallNumber) { + if (d7List.size() < 5) { + log.warn("D7表数据不足5条,实际{}条", d7List.size()); + List result = new ArrayList<>(); + for (D7 d7 : d7List) { + result.add(new BallWithCoefficient(d7.getSlaveBallNumber(), d7.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = d7List.stream() + .mapToDouble(D7::getCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d7List.size() - 1; i >= 0; i--) { + if (d7List.get(i).getCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d7List.get(avgPosition).getSlaveBallNumber(), + d7List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取2个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 2; i--) { + result.add(new BallWithCoefficient(d7List.get(i).getSlaveBallNumber(), + d7List.get(i).getCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 2) { + upBoundaryCoeff = d7List.get(i).getCoefficient(); + } + } + + // 向上边界处理:继续添加与第2个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 3; i >= 0; i--) { + if (d7List.get(i).getCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(d7List.get(i).getSlaveBallNumber(), + d7List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取2个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < d7List.size() && downCount < 2; i++) { + result.add(new BallWithCoefficient(d7List.get(i).getSlaveBallNumber(), + d7List.get(i).getCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 2) { + downBoundaryCoeff = d7List.get(i).getCoefficient(); + } + } + + // 向下边界处理:继续添加与第2个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 3; i < d7List.size(); i++) { + if (d7List.get(i).getCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(d7List.get(i).getSlaveBallNumber(), + d7List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 低位策略:从D7表获取最小值向上5个球号(含最小值,第1个系数相同则一并加入) + */ + private List getLowLevelBallsFromD7(List d7List, Integer masterBallNumber) { + if (d7List.size() < 5) { + log.warn("D7表数据不足5条,实际{}条", d7List.size()); + List result = new ArrayList<>(); + for (D7 d7 : d7List) { + result.add(new BallWithCoefficient(d7.getSlaveBallNumber(), d7.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 从最小值开始向上取5个(d7List已按系数降序排列,最后5个就是最小的) + List result = new ArrayList<>(); + int startIndex = Math.max(0, d7List.size() - 5); + + // 先加入基本的5个球号 + for (int i = startIndex; i < d7List.size(); i++) { + D7 d7 = d7List.get(i); + result.add(new BallWithCoefficient(d7.getSlaveBallNumber(), d7.getCoefficient(), masterBallNumber)); + } + + // 处理第1个球号(系数最大的)边界相同情况 + if (startIndex > 0) { + Double firstBallCoeff = d7List.get(startIndex).getCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (d7List.get(i).getCoefficient().equals(firstBallCoeff)) { + result.add(new BallWithCoefficient(d7List.get(i).getSlaveBallNumber(), + d7List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 从dlt_backend_history_top取平均值向上连续2个球号(按系数排行,处理边界相同系数) + */ + private List getTop2FromDltBackendHistoryTopByAvg() { + List allHistoryTopList = dltBackendHistoryTopMapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(allHistoryTopList)) { + return new ArrayList<>(); + } + + // 计算平均系数(保留两位小数,截断方式) + final double avgCoefficient = Math.floor(allHistoryTopList.stream() + .mapToDouble(DltBackendHistoryTop::getActiveCoefficient) + .average() + .orElse(0.0) * 100) / 100; + + // 筛选出大于或等于平均值的球,并按差值排序 + List aboveAvgBalls = allHistoryTopList.stream() + .filter(ball -> ball.getActiveCoefficient() >= avgCoefficient) + .sorted(Comparator.comparingDouble(ball -> ball.getActiveCoefficient() - avgCoefficient)) + .collect(Collectors.toList()); + + // 如果没有大于或等于平均值的球号,返回空列表 + if (aboveAvgBalls.isEmpty()) { + return new ArrayList<>(); + } + + // 逐层添加球号,直到达到2个或更多(相同差值的要一起加入) + List result = new ArrayList<>(); + double currentDiff = -1; + + for (DltBackendHistoryTop ball : aboveAvgBalls) { + double diff = ball.getActiveCoefficient() - avgCoefficient; + + // 如果已经有2个球了,且当前差值与之前不同,则停止 + if (result.size() >= 2 && diff != currentDiff) { + break; + } + + result.add(ball.getBallNumber()); + currentDiff = diff; + } + + return result; + } + + /** + * 从dlt_backend_history_top_100取平均值向上连续2个球号(按系数排行,处理边界相同系数) + */ + private List getTop2FromDltBackendHistoryTop100ByAvg() { + List allHistoryTop100List = dltBackendHistoryTop100Mapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(allHistoryTop100List)) { + return new ArrayList<>(); + } + + // 计算平均系数(保留两位小数,截断方式) + final double avgCoefficient = Math.floor(allHistoryTop100List.stream() + .mapToDouble(DltBackendHistoryTop100::getActiveCoefficient) + .average() + .orElse(0.0) * 100) / 100; + + // 筛选出大于或等于平均值的球,并按差值排序 + List aboveAvgBalls = allHistoryTop100List.stream() + .filter(ball -> ball.getActiveCoefficient() >= avgCoefficient) + .sorted(Comparator.comparingDouble(ball -> ball.getActiveCoefficient() - avgCoefficient)) + .collect(Collectors.toList()); + + // 如果没有大于或等于平均值的球号,返回空列表 + if (aboveAvgBalls.isEmpty()) { + return new ArrayList<>(); + } + + // 逐层添加球号,直到达到2个或更多(相同差值的要一起加入) + List result = new ArrayList<>(); + double currentDiff = -1; + + for (DltBackendHistoryTop100 ball : aboveAvgBalls) { + double diff = ball.getActiveCoefficient() - avgCoefficient; + + // 如果已经有2个球了,且当前差值与之前不同,则停止 + if (result.size() >= 2 && diff != currentDiff) { + break; + } + + result.add(ball.getBallNumber()); + currentDiff = diff; + } + + // 如果结果球号数量 > 2,通过dlt_backend_history_top表筛选 + if (result.size() > 2) { + return selectTop2FromDltBackendHistoryTop(result); + } + + return result; + } + + /** + * 从dlt_backend_history_top表中选择前2个球号 + */ + private List selectTop2FromDltBackendHistoryTop(List candidateBalls) { + // 获取候选球号在dlt_backend_history_top表中的活跃系数 + Map ballCoefficientMap = new HashMap<>(); + for (Integer ballNumber : candidateBalls) { + DltBackendHistoryTop record = dltBackendHistoryTopMapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ballNumber)); + double coefficient = (record != null) ? record.getActiveCoefficient() : 0.0; + ballCoefficientMap.put(ballNumber, coefficient); + } + + // 按活跃系数降序排序,选择前2个 + List> sortedEntries = ballCoefficientMap.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(2, sortedEntries.size()); i++) { + result.add(sortedEntries.get(i).getKey()); + } + + return result; + } + + /** + * 统计分析并生成结果 + */ + private List analyzeResults( + List allCandidateBalls, + Map> ballCoefficientMap) { + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 计算系数和 + Map ballCoefficientSumMap = new HashMap<>(); + for (Map.Entry> entry : ballCoefficientMap.entrySet()) { + Integer ballNumber = entry.getKey(); + List coefficients = entry.getValue(); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSumMap.put(ballNumber, sum); + } + + // 获取所有有排行数据的球号(包括频次为0的球号) + Set allBallsWithRanking = getAllBallsWithRanking(); + + // 获取百期排位和历史排位 + Map top100RankingMap = getTop100Rankings(allBallsWithRanking); + Map historyRankingMap = getHistoryRankings(allBallsWithRanking); + + // 组装结果 + List results = new ArrayList<>(); + for (Integer ballNumber : allBallsWithRanking) { + DLTFourthStepResultVO.BallAnalysisResult result = DLTFourthStepResultVO.BallAnalysisResult.builder() + .ballNumber(ballNumber) + .frequency(ballFrequencyMap.getOrDefault(ballNumber, 0)) + .coefficientSum(ballCoefficientSumMap.getOrDefault(ballNumber, 0.0)) + .top100Ranking(top100RankingMap.get(ballNumber)) + .historyRanking(historyRankingMap.get(ballNumber)) + .build(); + results.add(result); + } + + // 按出现次数降序排列,次数相同的按球号升序排列 + results.sort((a, b) -> { + int frequencyCompare = b.getFrequency().compareTo(a.getFrequency()); + if (frequencyCompare != 0) { + return frequencyCompare; + } + return a.getBallNumber().compareTo(b.getBallNumber()); + }); + + return results; + } + + /** + * 获取所有有排行数据的球号(包括频次为0的球号) + */ + private Set getAllBallsWithRanking() { + Set allBalls = new HashSet<>(); + + // 从DLT后区百期排行表获取所有球号 + List top100List = dltBackendHistoryTop100Mapper.selectList( + new QueryWrapper()); + for (DltBackendHistoryTop100 item : top100List) { + allBalls.add(item.getBallNumber()); + } + + // 从DLT后区历史排行表获取所有球号 + List historyTopList = dltBackendHistoryTopMapper.selectList( + new QueryWrapper()); + for (DltBackendHistoryTop item : historyTopList) { + allBalls.add(item.getBallNumber()); + } + + return allBalls; + } + + /** + * 获取球号在百期排行表中的排位 + */ + private Map getTop100Rankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List top100List = dltBackendHistoryTop100Mapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltBackendHistoryTop100 item : top100List) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在百期排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 获取球号在历史排行表中的排位 + */ + private Map getHistoryRankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List historyTopList = dltBackendHistoryTopMapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltBackendHistoryTop item : historyTopList) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在历史排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 参数验证 + */ + private void validateInputParams(String level, List previousFrontBalls, + List previousBackBalls, List currentFrontBalls, + Integer currentBackFirstBall) { + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是H/M/L之一"); + } + + if (CollectionUtils.isEmpty(previousFrontBalls) || previousFrontBalls.size() != 5) { + throw new IllegalArgumentException("上期前区球号必须为5个"); + } + + if (CollectionUtils.isEmpty(previousBackBalls) || previousBackBalls.size() != 2) { + throw new IllegalArgumentException("上期后区球号必须为2个"); + } + + if (CollectionUtils.isEmpty(currentFrontBalls) || currentFrontBalls.size() != 5) { + throw new IllegalArgumentException("本期前区球号必须为5个"); + } + + if (currentBackFirstBall == null || currentBackFirstBall < 1 || currentBackFirstBall > 12) { + throw new IllegalArgumentException("本期后区首球号码范围应为1-12"); + } + + for (Integer frontBall : previousFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("上期前区球号范围应为1-35"); + } + } + + for (Integer backBall : previousBackBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("上期后区球号范围应为1-12"); + } + } + + for (Integer frontBall : currentFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("本期前区球号范围应为1-35"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FollowFrontBallAnalysis.java b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FollowFrontBallAnalysis.java new file mode 100644 index 0000000..407cc30 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/jt/jtdlt/FollowFrontBallAnalysis.java @@ -0,0 +1,714 @@ +package com.xy.xyaicpzs.jt.jtdlt; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.domain.vo.DLTSecondStepResultVO; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 精推大乐透第二步算法分析 + */ +@Slf4j +@Component +public class FollowFrontBallAnalysis { + + @Autowired + private D9Mapper d9Mapper; + + @Autowired + private D12Mapper d12Mapper; + + @Autowired + private D5Mapper d5Mapper; + + @Autowired + private DltFrontendHistoryTopMapper dltFrontendHistoryTopMapper; + + @Autowired + private DltFrontendHistoryTop100Mapper dltFrontendHistoryTop100Mapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + } + + /** + * 精推大乐透第二步算法主方法 + * @param level 位置级别(H-高位/M-中位/L-低位) + * @param previousFrontBalls 上期前区5个球号 + * @param previousBackBalls 上期后区2个球号 + * @param currentFirstBall 本期首球号码 + * @return 分析结果 + */ + public DLTSecondStepResultVO analyze(String level, List previousFrontBalls, + List previousBackBalls, Integer currentFirstBall) { + // 参数验证 + validateInputParams(level, previousFrontBalls, previousBackBalls, currentFirstBall); + + log.info("开始精推大乐透第二步分析,策略:{},上期前区:{},上期后区:{},本期首球:{}", + level, previousFrontBalls, previousBackBalls, currentFirstBall); + + // 用于存储所有候选球号和对应的系数 + Map> ballCoefficientMap = new HashMap<>(); + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据5个上期前区球号从D9表获取候选球(取30个) + for (Integer frontBall : previousFrontBalls) { + List ballsWithCoefficients = getD9BallsByLevel(frontBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从dlt_frontend_history_top取平均值向上连续3个球号 + List top3HistoryTop = getTop3FromDltFrontendHistoryTopByAvg(); + allCandidateBalls.addAll(top3HistoryTop); + + // Step 3: 从dlt_frontend_history_top_100取平均值向上连续3个球号 + List top3HistoryTop100 = getTop3FromDltFrontendHistoryTop100ByAvg(); + allCandidateBalls.addAll(top3HistoryTop100); + + // Step 4: 根据2个上期后区球号从D12表获取候选球(取30个) + for (Integer backBall : previousBackBalls) { + List ballsWithCoefficients = getD12BallsByLevel(backBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 5: 根据本期首球从D5表获取候选球(取11个) + List ballsFromD5 = getD5BallsByLevel(currentFirstBall, level); + for (BallWithCoefficient ball : ballsFromD5) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // Step 6: 统计分析并生成结果 + List results = analyzeResults(allCandidateBalls, ballCoefficientMap); + + return DLTSecondStepResultVO.builder() + .results(results) + .strategy(level) + .previousFrontBalls(previousFrontBalls) + .previousBackBalls(previousBackBalls) + .currentFirstBall(currentFirstBall) + .build(); + } + + /** + * 根据级别从D9表获取候选球号和系数(取30个,如果第30个系数相同则一并加入) + */ + private List getD9BallsByLevel(Integer masterBallNumber, String level) { + List d9List = d9Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d9List)) { + log.warn("D9表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + // 所有策略都取前30个,如果第30个系数相同则一并加入 + List result = new ArrayList<>(); + + if (d9List.size() <= 30) { + // 如果总数不超过30个,全部加入 + for (D9 d9 : d9List) { + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前30个 + for (int i = 0; i < 30; i++) { + D9 d9 = d9List.get(i); + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } + + // 获取第30个球号的系数 + Double boundaryCoefficient = d9List.get(29).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 30; i < d9List.size(); i++) { + D9 d9 = d9List.get(i); + if (d9.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d9.getSlaveBallNumber(), d9.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 根据级别从D12表获取候选球号和系数(取30个,如果第30个系数相同则一并加入) + */ + private List getD12BallsByLevel(Integer masterBallNumber, String level) { + List d12List = d12Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d12List)) { + log.warn("D12表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + // 所有策略都取前30个,如果第30个系数相同则一并加入 + List result = new ArrayList<>(); + + if (d12List.size() <= 30) { + // 如果总数不超过30个,全部加入 + for (D12 d12 : d12List) { + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前30个 + for (int i = 0; i < 30; i++) { + D12 d12 = d12List.get(i); + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } + + // 获取第30个球号的系数 + Double boundaryCoefficient = d12List.get(29).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 30; i < d12List.size(); i++) { + D12 d12 = d12List.get(i); + if (d12.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d12.getSlaveBallNumber(), d12.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 根据级别从D5表获取候选球号和系数(取11个) + */ + private List getD5BallsByLevel(Integer masterBallNumber, String level) { + List d5List = d5Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("coefficient")); + + if (CollectionUtils.isEmpty(d5List)) { + log.warn("D5表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromD5(d5List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromD5(d5List, masterBallNumber); + } else { // L + return getLowLevelBallsFromD5(d5List, masterBallNumber); + } + } + + /** + * 高位策略:从D5表获取系数最大的前11个球号(如果第11个系数相同则一并加入) + */ + private List getHighLevelBallsFromD5(List d5List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (d5List.size() <= 11) { + // 如果总数不超过11个,全部加入 + for (D5 d5 : d5List) { + result.add(new BallWithCoefficient(d5.getSlaveBallNumber(), d5.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前11个 + for (int i = 0; i < 11; i++) { + D5 d5 = d5List.get(i); + result.add(new BallWithCoefficient(d5.getSlaveBallNumber(), d5.getCoefficient(), masterBallNumber)); + } + + // 获取第11个球号的系数 + Double boundaryCoefficient = d5List.get(10).getCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 11; i < d5List.size(); i++) { + D5 d5 = d5List.get(i); + if (d5.getCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(d5.getSlaveBallNumber(), d5.getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 中位策略:从D5表获取平均值附近的11个球号(向上5个,向下5个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromD5(List d5List, Integer masterBallNumber) { + if (d5List.size() < 11) { + log.warn("D5表数据不足11条,实际{}条", d5List.size()); + List result = new ArrayList<>(); + for (D5 d5 : d5List) { + result.add(new BallWithCoefficient(d5.getSlaveBallNumber(), d5.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = d5List.stream() + .mapToDouble(D5::getCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = d5List.size() - 1; i >= 0; i--) { + if (d5List.get(i).getCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(d5List.get(avgPosition).getSlaveBallNumber(), + d5List.get(avgPosition).getCoefficient(), masterBallNumber)); + + // 向上取5个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 5; i--) { + result.add(new BallWithCoefficient(d5List.get(i).getSlaveBallNumber(), + d5List.get(i).getCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 5) { + upBoundaryCoeff = d5List.get(i).getCoefficient(); + } + } + + // 向上边界处理:继续添加与第5个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 6; i >= 0; i--) { + if (d5List.get(i).getCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(d5List.get(i).getSlaveBallNumber(), + d5List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取5个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < d5List.size() && downCount < 5; i++) { + result.add(new BallWithCoefficient(d5List.get(i).getSlaveBallNumber(), + d5List.get(i).getCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 5) { + downBoundaryCoeff = d5List.get(i).getCoefficient(); + } + } + + // 向下边界处理:继续添加与第5个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 6; i < d5List.size(); i++) { + if (d5List.get(i).getCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(d5List.get(i).getSlaveBallNumber(), + d5List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 低位策略:从D5表获取系数最小值(含最小值)向上第3-13的球,共11个球号(边界系数相同则一并加入) + */ + private List getLowLevelBallsFromD5(List d5List, Integer masterBallNumber) { + if (d5List.size() < 13) { + log.warn("D5表数据不足13条,实际{}条", d5List.size()); + List result = new ArrayList<>(); + for (D5 d5 : d5List) { + result.add(new BallWithCoefficient(d5.getSlaveBallNumber(), d5.getCoefficient(), masterBallNumber)); + } + return result; + } + + // 从最小值向上第3-13的球(d5List已按系数降序排列) + // 最小值在最后,向上第3-13就是从倒数第13个到倒数第3个 + List result = new ArrayList<>(); + int size = d5List.size(); + int startIndex = size - 13; // 倒数第13个 + int endIndex = size - 3; // 倒数第3个 + + // 先加入基本的11个球号 + for (int i = startIndex; i <= endIndex; i++) { + D5 d5 = d5List.get(i); + result.add(new BallWithCoefficient(d5.getSlaveBallNumber(), d5.getCoefficient(), masterBallNumber)); + } + + // 处理起始边界:第1个球号(startIndex)系数相同情况 + if (startIndex > 0) { + Double startBoundaryCoeff = d5List.get(startIndex).getCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (d5List.get(i).getCoefficient().equals(startBoundaryCoeff)) { + result.add(new BallWithCoefficient(d5List.get(i).getSlaveBallNumber(), + d5List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + // 处理结束边界:第11个球号(endIndex)系数相同情况 + if (endIndex < d5List.size() - 1) { + Double endBoundaryCoeff = d5List.get(endIndex).getCoefficient(); + + // 继续向后查找系数相同的球号 + for (int i = endIndex + 1; i < d5List.size(); i++) { + if (d5List.get(i).getCoefficient().equals(endBoundaryCoeff)) { + result.add(new BallWithCoefficient(d5List.get(i).getSlaveBallNumber(), + d5List.get(i).getCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 从dlt_frontend_history_top取平均值向上连续3个球号(按系数排行,处理边界相同系数) + */ + private List getTop3FromDltFrontendHistoryTopByAvg() { + List allHistoryTopList = dltFrontendHistoryTopMapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(allHistoryTopList)) { + return new ArrayList<>(); + } + + // 计算平均系数(保留两位小数,截断方式) + final double avgCoefficient = Math.floor(allHistoryTopList.stream() + .mapToDouble(DltFrontendHistoryTop::getActiveCoefficient) + .average() + .orElse(0.0) * 100) / 100; + + // 筛选出大于或等于平均值的球,并按差值排序 + List aboveAvgBalls = allHistoryTopList.stream() + .filter(ball -> ball.getActiveCoefficient() >= avgCoefficient) + .sorted(Comparator.comparingDouble(ball -> ball.getActiveCoefficient() - avgCoefficient)) + .collect(Collectors.toList()); + + // 如果没有大于或等于平均值的球号,返回空列表 + if (aboveAvgBalls.isEmpty()) { + return new ArrayList<>(); + } + + // 逐层添加球号,直到达到3个或更多(相同差值的要一起加入) + List result = new ArrayList<>(); + double currentDiff = -1; + + for (DltFrontendHistoryTop ball : aboveAvgBalls) { + double diff = ball.getActiveCoefficient() - avgCoefficient; + + // 如果已经有3个球了,且当前差值与之前不同,则停止 + if (result.size() >= 3 && diff != currentDiff) { + break; + } + + result.add(ball.getBallNumber()); + currentDiff = diff; + } + + return result; + } + + /** + * 从dlt_frontend_history_top_100取平均值向上连续3个球号(按系数排行,处理边界相同系数) + */ + private List getTop3FromDltFrontendHistoryTop100ByAvg() { + List allHistoryTop100List = dltFrontendHistoryTop100Mapper.selectList( + new QueryWrapper() + .orderByDesc("activeCoefficient")); + + if (CollectionUtils.isEmpty(allHistoryTop100List)) { + return new ArrayList<>(); + } + + // 计算平均系数(保留两位小数,截断方式) + final double avgCoefficient = Math.floor(allHistoryTop100List.stream() + .mapToDouble(DltFrontendHistoryTop100::getActiveCoefficient) + .average() + .orElse(0.0) * 100) / 100; + + // 筛选出大于或等于平均值的球,并按差值排序 + List aboveAvgBalls = allHistoryTop100List.stream() + .filter(ball -> ball.getActiveCoefficient() >= avgCoefficient) + .sorted(Comparator.comparingDouble(ball -> ball.getActiveCoefficient() - avgCoefficient)) + .collect(Collectors.toList()); + + // 如果没有大于或等于平均值的球号,返回空列表 + if (aboveAvgBalls.isEmpty()) { + return new ArrayList<>(); + } + + // 逐层添加球号,直到达到3个或更多(相同差值的要一起加入) + List result = new ArrayList<>(); + double currentDiff = -1; + + for (DltFrontendHistoryTop100 ball : aboveAvgBalls) { + double diff = ball.getActiveCoefficient() - avgCoefficient; + + // 如果已经有3个球了,且当前差值与之前不同,则停止 + if (result.size() >= 3 && diff != currentDiff) { + break; + } + + result.add(ball.getBallNumber()); + currentDiff = diff; + } + + // 如果结果球号数量 > 3,通过dlt_frontend_history_top表筛选 + if (result.size() > 3) { + return selectTop3FromDltFrontendHistoryTop(result); + } + + return result; + } + + /** + * 从dlt_frontend_history_top表中选择前3个球号 + */ + private List selectTop3FromDltFrontendHistoryTop(List candidateBalls) { + // 获取候选球号在dlt_frontend_history_top表中的活跃系数 + Map ballCoefficientMap = new HashMap<>(); + for (Integer ballNumber : candidateBalls) { + DltFrontendHistoryTop record = dltFrontendHistoryTopMapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ballNumber)); + double coefficient = (record != null) ? record.getActiveCoefficient() : 0.0; + ballCoefficientMap.put(ballNumber, coefficient); + } + + // 按活跃系数降序排序,选择前3个 + List> sortedEntries = ballCoefficientMap.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(3, sortedEntries.size()); i++) { + result.add(sortedEntries.get(i).getKey()); + } + + return result; + } + + /** + * 统计分析并生成结果 + */ + private List analyzeResults( + List allCandidateBalls, + Map> ballCoefficientMap) { + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 计算系数和 + Map ballCoefficientSumMap = new HashMap<>(); + for (Map.Entry> entry : ballCoefficientMap.entrySet()) { + Integer ballNumber = entry.getKey(); + List coefficients = entry.getValue(); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSumMap.put(ballNumber, sum); + } + + // 获取所有有排行数据的球号(包括频次为0的球号) + Set allBallsWithRanking = getAllBallsWithRanking(); + + // 获取百期排位和历史排位 + Map top100RankingMap = getTop100Rankings(allBallsWithRanking); + Map historyRankingMap = getHistoryRankings(allBallsWithRanking); + + // 组装结果 + List results = new ArrayList<>(); + for (Integer ballNumber : allBallsWithRanking) { + DLTSecondStepResultVO.BallAnalysisResult result = DLTSecondStepResultVO.BallAnalysisResult.builder() + .ballNumber(ballNumber) + .frequency(ballFrequencyMap.getOrDefault(ballNumber, 0)) + .coefficientSum(ballCoefficientSumMap.getOrDefault(ballNumber, 0.0)) + .top100Ranking(top100RankingMap.get(ballNumber)) + .historyRanking(historyRankingMap.get(ballNumber)) + .build(); + results.add(result); + } + + // 按出现次数降序排列,次数相同的按球号升序排列 + results.sort((a, b) -> { + int frequencyCompare = b.getFrequency().compareTo(a.getFrequency()); + if (frequencyCompare != 0) { + return frequencyCompare; + } + return a.getBallNumber().compareTo(b.getBallNumber()); + }); + + return results; + } + + /** + * 获取所有有排行数据的球号(包括频次为0的球号) + */ + private Set getAllBallsWithRanking() { + Set allBalls = new HashSet<>(); + + // 从DLT前区百期排行表获取所有球号 + List top100List = dltFrontendHistoryTop100Mapper.selectList( + new QueryWrapper()); + for (DltFrontendHistoryTop100 item : top100List) { + allBalls.add(item.getBallNumber()); + } + + // 从DLT前区历史排行表获取所有球号 + List historyTopList = dltFrontendHistoryTopMapper.selectList( + new QueryWrapper()); + for (DltFrontendHistoryTop item : historyTopList) { + allBalls.add(item.getBallNumber()); + } + + return allBalls; + } + + /** + * 获取球号在百期排行表中的排位 + */ + private Map getTop100Rankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List top100List = dltFrontendHistoryTop100Mapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltFrontendHistoryTop100 item : top100List) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在百期排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 获取球号在历史排行表中的排位 + */ + private Map getHistoryRankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的ranking字段作为排名 + List historyTopList = dltFrontendHistoryTopMapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的ranking字段作为排名 + for (DltFrontendHistoryTop item : historyTopList) { + result.put(item.getBallNumber(), item.getRanking()); + } + + // 对于没有在历史排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 参数验证 + */ + private void validateInputParams(String level, List previousFrontBalls, + List previousBackBalls, Integer currentFirstBall) { + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是H/M/L之一"); + } + + if (CollectionUtils.isEmpty(previousFrontBalls) || previousFrontBalls.size() != 5) { + throw new IllegalArgumentException("上期前区球号必须为5个"); + } + + if (CollectionUtils.isEmpty(previousBackBalls) || previousBackBalls.size() != 2) { + throw new IllegalArgumentException("上期后区球号必须为2个"); + } + + if (currentFirstBall == null || currentFirstBall < 1 || currentFirstBall > 35) { + throw new IllegalArgumentException("本期首球号码范围应为1-35"); + } + + for (Integer frontBall : previousFrontBalls) { + if (frontBall < 1 || frontBall > 35) { + throw new IllegalArgumentException("上期前区球号范围应为1-35"); + } + } + + for (Integer backBall : previousBackBalls) { + if (backBall < 1 || backBall > 12) { + throw new IllegalArgumentException("上期后区球号范围应为1-12"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/jt/jtssq/BlueBallAnalysis.java b/src/main/java/com/xy/xyaicpzs/jt/jtssq/BlueBallAnalysis.java new file mode 100644 index 0000000..f492e6e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/jt/jtssq/BlueBallAnalysis.java @@ -0,0 +1,693 @@ +package com.xy.xyaicpzs.jt.jtssq; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.domain.vo.SSQThirdStepResultVO; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 精推双色球第三步算法分析 + */ +@Slf4j +@Component +public class BlueBallAnalysis { + + @Autowired + private T6Mapper t6Mapper; + + @Autowired + private T5Mapper t5Mapper; + + @Autowired + private T8Mapper t8Mapper; + + @Autowired + private BlueHistoryTopMapper blueHistoryTopMapper; + + @Autowired + private BlueHistoryTop100Mapper blueHistoryTop100Mapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + } + + /** + * 精推双色球第三步算法主方法 + * @param level 位置级别(H-高位/M-中位/L-低位) + * @param redBalls 前6个红球号码 + * @param blueBall 蓝球号码 + * @param nextRedBalls 下期6个红球号码 + * @return 分析结果 + */ + public SSQThirdStepResultVO analyze(String level, List redBalls, Integer blueBall, List nextRedBalls) { + // 参数验证 + validateInputParams(level, redBalls, blueBall, nextRedBalls); + + log.info("开始精推双色球第三步分析,策略:{},红球:{},蓝球:{},下期红球:{}", + level, redBalls, blueBall, nextRedBalls); + + // 用于存储所有候选球号和对应的系数 + Map> ballCoefficientMap = new HashMap<>(); + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据6个红球号码获取候选球(T6表前12个) + for (Integer redBall : redBalls) { + List ballsWithCoefficients = getTop12FromT6(redBall); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从blue_history_top_100百期排行获取前2个球号 + List top2HistoryTop100 = getTop2FromHistoryTop100(); + allCandidateBalls.addAll(top2HistoryTop100); + + // Step 3: 根据蓝球号码获取候选球(T5表前12个) + List blueBallsWithCoefficients = getTop12FromT5(blueBall); + for (BallWithCoefficient ball : blueBallsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // Step 4: 根据下期6个红球号码获取候选球(T8表,根据策略不同) + for (Integer nextRedBall : nextRedBalls) { + List nextRedBallsWithCoefficients = getT8BallsByLevel(nextRedBall, level); + for (BallWithCoefficient ball : nextRedBallsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 5: 统计分析并生成结果 + List results = analyzeResults(allCandidateBalls, ballCoefficientMap); + + return SSQThirdStepResultVO.builder() + .results(results) + .strategy(level) + .redBalls(redBalls) + .blueBall(blueBall) + .nextRedBalls(nextRedBalls) + .build(); + } + + /** + * 从T6表获取前12个球号和系数(如果第12个系数相同则一并加入) + */ + private List getTop12FromT6(Integer masterBallNumber) { + List t6List = t6Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient")); + + if (CollectionUtils.isEmpty(t6List)) { + log.warn("T6表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + if (t6List.size() <= 12) { + // 如果总数不超过12个,全部加入 + for (T6 t6 : t6List) { + result.add(new BallWithCoefficient(t6.getSlaveBallNumber(), t6.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前12个 + for (int i = 0; i < 12; i++) { + T6 t6 = t6List.get(i); + result.add(new BallWithCoefficient(t6.getSlaveBallNumber(), t6.getLineCoefficient(), masterBallNumber)); + } + + // 获取第12个球号的系数 + Double boundaryCoefficient = t6List.get(11).getLineCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 12; i < t6List.size(); i++) { + T6 t6 = t6List.get(i); + if (t6.getLineCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t6.getSlaveBallNumber(), t6.getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 从T5表获取前12个球号和系数(如果第12个系数相同则一并加入) + */ + private List getTop12FromT5(Integer masterBallNumber) { + List t5List = t5Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient")); + + if (CollectionUtils.isEmpty(t5List)) { + log.warn("T5表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + if (t5List.size() <= 12) { + // 如果总数不超过12个,全部加入 + for (T5 t5 : t5List) { + result.add(new BallWithCoefficient(t5.getSlaveBallNumber(), t5.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前12个 + for (int i = 0; i < 12; i++) { + T5 t5 = t5List.get(i); + result.add(new BallWithCoefficient(t5.getSlaveBallNumber(), t5.getLineCoefficient(), masterBallNumber)); + } + + // 获取第12个球号的系数 + Double boundaryCoefficient = t5List.get(11).getLineCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 12; i < t5List.size(); i++) { + T5 t5 = t5List.get(i); + if (t5.getLineCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t5.getSlaveBallNumber(), t5.getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 从blue_history_top_100获取前2个球号(按点系数排行,处理边界相同系数) + */ + private List getTop2FromHistoryTop100() { + List blueHistoryTop100List = blueHistoryTop100Mapper.selectList( + new QueryWrapper() + .orderByDesc("pointCoefficient")); + + if (CollectionUtils.isEmpty(blueHistoryTop100List)) { + log.warn("蓝球百期排行表数据为空"); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 2 && index < blueHistoryTop100List.size()) { + double currentCoefficient = blueHistoryTop100List.get(index).getPointCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < blueHistoryTop100List.size() && + blueHistoryTop100List.get(index).getPointCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(blueHistoryTop100List.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(2 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理边界冲突 + List selectedBalls = handleBlueHistoryBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 根据级别从T8表获取候选球号和系数 + */ + private List getT8BallsByLevel(Integer masterBallNumber, String level) { + List t8List = t8Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("faceCoefficient")); + + if (CollectionUtils.isEmpty(t8List)) { + log.warn("T8表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromT8(t8List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromT8(t8List, masterBallNumber); + } else { // L + return getLowLevelBallsFromT8(t8List, masterBallNumber); + } + } + + /** + * 高位策略:从T8表获取系数最大的前5个球号(如果第5个系数相同则一并加入) + */ + private List getHighLevelBallsFromT8(List t8List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (t8List.size() <= 5) { + // 如果总数不超过5个,全部加入 + for (T8 t8 : t8List) { + result.add(new BallWithCoefficient(t8.getSlaveBallNumber(), t8.getFaceCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前5个 + for (int i = 0; i < 5; i++) { + T8 t8 = t8List.get(i); + result.add(new BallWithCoefficient(t8.getSlaveBallNumber(), t8.getFaceCoefficient(), masterBallNumber)); + } + + // 获取第5个球号的系数 + Double boundaryCoefficient = t8List.get(4).getFaceCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 5; i < t8List.size(); i++) { + T8 t8 = t8List.get(i); + if (t8.getFaceCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t8.getSlaveBallNumber(), t8.getFaceCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 中位策略:从T8表获取平均值附近5个球号(向上2个,向下2个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromT8(List t8List, Integer masterBallNumber) { + if (t8List.size() < 5) { + log.warn("T8表数据不足5条,实际{}条", t8List.size()); + List result = new ArrayList<>(); + for (T8 t8 : t8List) { + result.add(new BallWithCoefficient(t8.getSlaveBallNumber(), t8.getFaceCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = t8List.stream() + .mapToDouble(T8::getFaceCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = t8List.size() - 1; i >= 0; i--) { + if (t8List.get(i).getFaceCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(t8List.get(avgPosition).getSlaveBallNumber(), + t8List.get(avgPosition).getFaceCoefficient(), masterBallNumber)); + + // 向上取2个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 2; i--) { + result.add(new BallWithCoefficient(t8List.get(i).getSlaveBallNumber(), + t8List.get(i).getFaceCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 2) { + upBoundaryCoeff = t8List.get(i).getFaceCoefficient(); + } + } + + // 向上边界处理:继续添加与第2个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 3; i >= 0; i--) { + if (t8List.get(i).getFaceCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(t8List.get(i).getSlaveBallNumber(), + t8List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取2个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < t8List.size() && downCount < 2; i++) { + result.add(new BallWithCoefficient(t8List.get(i).getSlaveBallNumber(), + t8List.get(i).getFaceCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 2) { + downBoundaryCoeff = t8List.get(i).getFaceCoefficient(); + } + } + + // 向下边界处理:继续添加与第2个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 3; i < t8List.size(); i++) { + if (t8List.get(i).getFaceCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(t8List.get(i).getSlaveBallNumber(), + t8List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 低位策略:从T8表获取最小值向上第2-6个球(共5个球,边界系数相同则一并加入) + */ + private List getLowLevelBallsFromT8(List t8List, Integer masterBallNumber) { + if (t8List.size() < 6) { + log.warn("T8表数据不足6条,实际{}条", t8List.size()); + List result = new ArrayList<>(); + for (T8 t8 : t8List) { + result.add(new BallWithCoefficient(t8.getSlaveBallNumber(), t8.getFaceCoefficient(), masterBallNumber)); + } + return result; + } + + // 取最小值向上第2-6个球(t8List已按系数降序排列,最后的是最小的) + // 最小值在最后,向上第2个就是值数组的第 size-2 个,向上第6个就是值数组的第 size-6 个 + List result = new ArrayList<>(); + int startIndex = Math.max(0, t8List.size() - 6); // 第2位 + int endIndex = t8List.size() - 2; // 第6位 + + // 先加入基本的5个球号 + for (int i = startIndex; i <= endIndex; i++) { + T8 t8 = t8List.get(i); + result.add(new BallWithCoefficient(t8.getSlaveBallNumber(), t8.getFaceCoefficient(), masterBallNumber)); + } + + // 处理起始边界:第1个球号(startIndex)系数相同情况 + if (startIndex > 0) { + Double startBoundaryCoeff = t8List.get(startIndex).getFaceCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (t8List.get(i).getFaceCoefficient().equals(startBoundaryCoeff)) { + result.add(new BallWithCoefficient(t8List.get(i).getSlaveBallNumber(), + t8List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + // 处理结束边界:第5个球号(endIndex)系数相同情况 + if (endIndex < t8List.size() - 1) { + Double endBoundaryCoeff = t8List.get(endIndex).getFaceCoefficient(); + + // 继续向后查找系数相同的球号 + for (int i = endIndex + 1; i < t8List.size(); i++) { + if (t8List.get(i).getFaceCoefficient().equals(endBoundaryCoeff)) { + result.add(new BallWithCoefficient(t8List.get(i).getSlaveBallNumber(), + t8List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 处理蓝球历史排行边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleBlueHistoryBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 使用blue_history_top表进行筛选 + return selectFromBlueHistoryTop(candidateBalls, selectCount); + } + + /** + * 从blue_history_top表中选择指定数量的球号 + */ + private List selectFromBlueHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在blue_history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + BlueHistoryTop record = blueHistoryTopMapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ball)); + if (record != null) { + topCoefficients.put(ball, record.getPointCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 统计分析并生成结果 + */ + private List analyzeResults( + List allCandidateBalls, + Map> ballCoefficientMap) { + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 计算系数和 + Map ballCoefficientSumMap = new HashMap<>(); + for (Map.Entry> entry : ballCoefficientMap.entrySet()) { + Integer ballNumber = entry.getKey(); + List coefficients = entry.getValue(); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSumMap.put(ballNumber, sum); + } + + // 获取所有有排行数据的球号(包括频次为0的球号) + Set allBallsWithRanking = getAllBallsWithRanking(); + + // 获取百期排位和历史排位 + Map top100RankingMap = getTop100Rankings(allBallsWithRanking); + Map historyRankingMap = getHistoryRankings(allBallsWithRanking); + + // 组装结果 + List results = new ArrayList<>(); + for (Integer ballNumber : allBallsWithRanking) { + SSQThirdStepResultVO.BallAnalysisResult result = SSQThirdStepResultVO.BallAnalysisResult.builder() + .ballNumber(ballNumber) + .frequency(ballFrequencyMap.getOrDefault(ballNumber, 0)) + .coefficientSum(ballCoefficientSumMap.getOrDefault(ballNumber, 0.0)) + .top100Ranking(top100RankingMap.get(ballNumber)) + .historyRanking(historyRankingMap.get(ballNumber)) + .build(); + results.add(result); + } + + // 按出现次数降序排列,次数相同的按球号升序排列 + results.sort((a, b) -> { + int frequencyCompare = b.getFrequency().compareTo(a.getFrequency()); + if (frequencyCompare != 0) { + return frequencyCompare; + } + return a.getBallNumber().compareTo(b.getBallNumber()); + }); + + return results; + } + + /** + * 获取所有有排行数据的球号(包括频次为0的球号) + */ + private Set getAllBallsWithRanking() { + Set allBalls = new HashSet<>(); + + // 从蓝球百期排行表获取所有球号 + List blueHistoryTop100List = blueHistoryTop100Mapper.selectList( + new QueryWrapper()); + for (BlueHistoryTop100 item : blueHistoryTop100List) { + allBalls.add(item.getBallNumber()); + } + + // 从蓝球历史排行表获取所有球号 + List blueHistoryTopList = blueHistoryTopMapper.selectList( + new QueryWrapper()); + for (BlueHistoryTop item : blueHistoryTopList) { + allBalls.add(item.getBallNumber()); + } + + return allBalls; + } + + /** + * 获取球号在蓝球百期排行表中的排位 + */ + private Map getTop100Rankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的no字段作为排名 + List blueHistoryTop100List = blueHistoryTop100Mapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的no字段作为排名 + for (BlueHistoryTop100 item : blueHistoryTop100List) { + result.put(item.getBallNumber(), item.getNo()); + } + + // 对于没有在蓝球百期排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 获取球号在蓝球历史排行表中的排位 + */ + private Map getHistoryRankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的no字段作为排名 + List blueHistoryTopList = blueHistoryTopMapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的no字段作为排名 + for (BlueHistoryTop item : blueHistoryTopList) { + result.put(item.getBallNumber(), item.getNo()); + } + + // 对于没有在蓝球历史排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 参数验证 + */ + private void validateInputParams(String level, List redBalls, Integer blueBall, List nextRedBalls) { + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是H/M/L之一"); + } + + if (CollectionUtils.isEmpty(redBalls) || redBalls.size() != 6) { + throw new IllegalArgumentException("红球号码必须为6个"); + } + + if (blueBall == null || blueBall < 1 || blueBall > 16) { + throw new IllegalArgumentException("蓝球号码范围应为1-16"); + } + + if (CollectionUtils.isEmpty(nextRedBalls) || nextRedBalls.size() != 6) { + throw new IllegalArgumentException("下期红球号码必须为6个"); + } + + for (Integer redBall : redBalls) { + if (redBall < 1 || redBall > 33) { + throw new IllegalArgumentException("红球号码范围应为1-33"); + } + } + + for (Integer nextRedBall : nextRedBalls) { + if (nextRedBall < 1 || nextRedBall > 33) { + throw new IllegalArgumentException("下期红球号码范围应为1-33"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/jt/jtssq/FirstBallAnalysis.java b/src/main/java/com/xy/xyaicpzs/jt/jtssq/FirstBallAnalysis.java new file mode 100644 index 0000000..dbdcc98 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/jt/jtssq/FirstBallAnalysis.java @@ -0,0 +1,653 @@ +package com.xy.xyaicpzs.jt.jtssq; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.domain.vo.SSQFirstStepResultVO; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 精推双色球第一步算法分析 + */ +@Slf4j +@Component +public class FirstBallAnalysis { + + @Autowired + private T3Mapper t3Mapper; + + @Autowired + private T4Mapper t4Mapper; + + @Autowired + private HistoryTopMapper historyTopMapper; + + @Autowired + private HistoryTop100Mapper historyTop100Mapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + } + + /** + * 精推双色球第一步算法主方法 + * @param level 位置级别(H-高位/M-中位/L-低位) + * @param redBalls 前6个红球号码 + * @param blueBall 蓝球号码 + * @return 分析结果 + */ + public SSQFirstStepResultVO analyze(String level, List redBalls, Integer blueBall) { + // 参数验证 + validateInputParams(level, redBalls, blueBall); + + log.info("开始精推双色球第一步分析,策略:{},红球:{},蓝球:{}", level, redBalls, blueBall); + + // 用于存储所有候选球号和对应的系数 + Map> ballCoefficientMap = new HashMap<>(); + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据6个红球号码获取候选球 + for (Integer redBall : redBalls) { + List ballsWithCoefficients = getT3BallsByLevel(redBall, level); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从history_top获取前3个球号 + List top3HistoryTop = getTop3FromHistoryTop(); + allCandidateBalls.addAll(top3HistoryTop); + + // Step 3: 根据蓝球号码获取候选球 + List blueBallsWithCoefficients = getT4BallsByLevel(blueBall, level); + for (BallWithCoefficient ball : blueBallsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // Step 4: 统计分析并生成结果 + List results = analyzeResults(allCandidateBalls, ballCoefficientMap); + + return SSQFirstStepResultVO.builder() + .results(results) + .strategy(level) + .redBalls(redBalls) + .blueBall(blueBall) + .build(); + } + + /** + * 根据级别从T3表获取候选球号和系数 + */ + private List getT3BallsByLevel(Integer masterBallNumber, String level) { + List t3List = t3Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient")); + + if (CollectionUtils.isEmpty(t3List)) { + log.warn("T3表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromT3(t3List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromT3(t3List, masterBallNumber); + } else { // L + return getLowLevelBallsFromT3(t3List, masterBallNumber); + } + } + + /** + * 根据级别从T4表获取候选球号和系数 + */ + private List getT4BallsByLevel(Integer masterBallNumber, String level) { + List t4List = t4Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient")); + + if (CollectionUtils.isEmpty(t4List)) { + log.warn("T4表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromT4(t4List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromT4(t4List, masterBallNumber); + } else { // L + return getLowLevelBallsFromT4(t4List, masterBallNumber); + } + } + + /** + * 高位策略:从T3表获取系数最大的前17个球号(如果第17个系数相同则一并加入) + */ + private List getHighLevelBallsFromT3(List t3List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (t3List.size() <= 17) { + // 如果总数不超过17个,全部加入 + for (T3 t3 : t3List) { + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前17个 + for (int i = 0; i < 17; i++) { + T3 t3 = t3List.get(i); + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } + + // 获取第17个球号的系数 + Double boundaryCoefficient = t3List.get(16).getLineCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 17; i < t3List.size(); i++) { + T3 t3 = t3List.get(i); + if (t3.getLineCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 高位策略:从T4表获取系数最大的前17个球号(如果第17个系数相同则一并加入) + */ + private List getHighLevelBallsFromT4(List t4List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (t4List.size() <= 17) { + // 如果总数不超过17个,全部加入 + for (T4 t4 : t4List) { + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前17个 + for (int i = 0; i < 17; i++) { + T4 t4 = t4List.get(i); + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } + + // 获取第17个球号的系数 + Double boundaryCoefficient = t4List.get(16).getLineCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 17; i < t4List.size(); i++) { + T4 t4 = t4List.get(i); + if (t4.getLineCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 中位策略:从T3表获取平均值附近的17个球号(向上8个,向下8个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromT3(List t3List, Integer masterBallNumber) { + if (t3List.size() < 17) { + log.warn("T3表数据不足17条,实际{}条", t3List.size()); + List result = new ArrayList<>(); + for (T3 t3 : t3List) { + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = t3List.stream() + .mapToDouble(T3::getLineCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = t3List.size() - 1; i >= 0; i--) { + if (t3List.get(i).getLineCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(t3List.get(avgPosition).getSlaveBallNumber(), + t3List.get(avgPosition).getLineCoefficient(), masterBallNumber)); + + // 向上取8个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 8; i--) { + result.add(new BallWithCoefficient(t3List.get(i).getSlaveBallNumber(), + t3List.get(i).getLineCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 8) { + upBoundaryCoeff = t3List.get(i).getLineCoefficient(); + } + } + + // 向上边界处理:继续添加与第8个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 9; i >= 0; i--) { + if (t3List.get(i).getLineCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(t3List.get(i).getSlaveBallNumber(), + t3List.get(i).getLineCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取8个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < t3List.size() && downCount < 8; i++) { + result.add(new BallWithCoefficient(t3List.get(i).getSlaveBallNumber(), + t3List.get(i).getLineCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 8) { + downBoundaryCoeff = t3List.get(i).getLineCoefficient(); + } + } + + // 向下边界处理:继续添加与第8个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 9; i < t3List.size(); i++) { + if (t3List.get(i).getLineCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(t3List.get(i).getSlaveBallNumber(), + t3List.get(i).getLineCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 中位策略:从T4表获取平均值附近的17个球号(向上8个,向下8个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromT4(List t4List, Integer masterBallNumber) { + if (t4List.size() < 17) { + log.warn("T4表数据不足17条,实际{}条", t4List.size()); + List result = new ArrayList<>(); + for (T4 t4 : t4List) { + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = t4List.stream() + .mapToDouble(T4::getLineCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = t4List.size() - 1; i >= 0; i--) { + if (t4List.get(i).getLineCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(t4List.get(avgPosition).getSlaveBallNumber(), + t4List.get(avgPosition).getLineCoefficient(), masterBallNumber)); + + // 向上取8个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 8; i--) { + result.add(new BallWithCoefficient(t4List.get(i).getSlaveBallNumber(), + t4List.get(i).getLineCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 8) { + upBoundaryCoeff = t4List.get(i).getLineCoefficient(); + } + } + + // 向上边界处理:继续添加与第8个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 9; i >= 0; i--) { + if (t4List.get(i).getLineCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(t4List.get(i).getSlaveBallNumber(), + t4List.get(i).getLineCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取8个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < t4List.size() && downCount < 8; i++) { + result.add(new BallWithCoefficient(t4List.get(i).getSlaveBallNumber(), + t4List.get(i).getLineCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 8) { + downBoundaryCoeff = t4List.get(i).getLineCoefficient(); + } + } + + // 向下边界处理:继续添加与第8个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 9; i < t4List.size(); i++) { + if (t4List.get(i).getLineCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(t4List.get(i).getSlaveBallNumber(), + t4List.get(i).getLineCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 低位策略:从T3表获取最小值向上17个球号(含最小值,第1个系数相同则一并加入) + */ + private List getLowLevelBallsFromT3(List t3List, Integer masterBallNumber) { + if (t3List.size() < 17) { + log.warn("T3表数据不足17条,实际{}条", t3List.size()); + List result = new ArrayList<>(); + for (T3 t3 : t3List) { + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 从最小值开始向上取17个(t3List已按系数降序排列,最后17个就是最小的) + List result = new ArrayList<>(); + int startIndex = Math.max(0, t3List.size() - 17); + + // 先加入基本的17个球号 + for (int i = startIndex; i < t3List.size(); i++) { + T3 t3 = t3List.get(i); + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } + + // 处理第1个球号(系数最大的)边界相同情况 + if (startIndex > 0) { + Double firstBallCoeff = t3List.get(startIndex).getLineCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (t3List.get(i).getLineCoefficient().equals(firstBallCoeff)) { + result.add(new BallWithCoefficient(t3List.get(i).getSlaveBallNumber(), + t3List.get(i).getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 低位策略:从T4表获取最小值向上17个球号(含最小值,第1个系数相同则一并加入) + */ + private List getLowLevelBallsFromT4(List t4List, Integer masterBallNumber) { + if (t4List.size() < 17) { + log.warn("T4表数据不足17条,实际{}条", t4List.size()); + List result = new ArrayList<>(); + for (T4 t4 : t4List) { + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 从最小值开始向上取17个(t4List已按系数降序排列,最后17个就是最小的) + List result = new ArrayList<>(); + int startIndex = Math.max(0, t4List.size() - 17); + + // 先加入基本的17个球号 + for (int i = startIndex; i < t4List.size(); i++) { + T4 t4 = t4List.get(i); + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } + + // 处理第1个球号(系数最大的)边界相同情况 + if (startIndex > 0) { + Double firstBallCoeff = t4List.get(startIndex).getLineCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (t4List.get(i).getLineCoefficient().equals(firstBallCoeff)) { + result.add(new BallWithCoefficient(t4List.get(i).getSlaveBallNumber(), + t4List.get(i).getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 从历史排行表获取前3个球号(按点系数排行) + */ + private List getTop3FromHistoryTop() { + List historyTopList = historyTopMapper.selectList( + new QueryWrapper() + .orderByDesc("pointCoefficient") + .last("LIMIT 3")); + + if (CollectionUtils.isEmpty(historyTopList)) { + log.warn("历史排行表数据不足3条,实际{}条", historyTopList.size()); + return new ArrayList<>(); + } + + return historyTopList.stream() + .map(HistoryTop::getBallNumber) + .collect(Collectors.toList()); + } + + /** + * 统计分析并生成结果 + */ + private List analyzeResults( + List allCandidateBalls, + Map> ballCoefficientMap) { + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 计算系数和 + Map ballCoefficientSumMap = new HashMap<>(); + for (Map.Entry> entry : ballCoefficientMap.entrySet()) { + Integer ballNumber = entry.getKey(); + List coefficients = entry.getValue(); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSumMap.put(ballNumber, sum); + } + + // 获取所有有排行数据的球号(包括频次为0的球号) + Set allBallsWithRanking = getAllBallsWithRanking(); + + // 获取百期排位和历史排位 + Map top100RankingMap = getTop100Rankings(allBallsWithRanking); + Map historyRankingMap = getHistoryRankings(allBallsWithRanking); + + // 组装结果 + List results = new ArrayList<>(); + for (Integer ballNumber : allBallsWithRanking) { + SSQFirstStepResultVO.BallAnalysisResult result = SSQFirstStepResultVO.BallAnalysisResult.builder() + .ballNumber(ballNumber) + .frequency(ballFrequencyMap.getOrDefault(ballNumber, 0)) + .coefficientSum(ballCoefficientSumMap.getOrDefault(ballNumber, 0.0)) + .top100Ranking(top100RankingMap.get(ballNumber)) + .historyRanking(historyRankingMap.get(ballNumber)) + .build(); + results.add(result); + } + + // 按出现次数降序排列,次数相同的按球号升序排列 + results.sort((a, b) -> { + int frequencyCompare = b.getFrequency().compareTo(a.getFrequency()); + if (frequencyCompare != 0) { + return frequencyCompare; + } + return a.getBallNumber().compareTo(b.getBallNumber()); + }); + + return results; + } + + /** + * 获取所有有排行数据的球号(包括频次为0的球号) + */ + private Set getAllBallsWithRanking() { + Set allBalls = new HashSet<>(); + + // 从百期排行表获取所有球号 + List top100List = historyTop100Mapper.selectList( + new QueryWrapper()); + for (HistoryTop100 item : top100List) { + allBalls.add(item.getBallNumber()); + } + + // 从历史排行表获取所有球号 + List historyTopList = historyTopMapper.selectList( + new QueryWrapper()); + for (HistoryTop item : historyTopList) { + allBalls.add(item.getBallNumber()); + } + + return allBalls; + } + + /** + * 获取球号在百期排行表中的排位 + */ + private Map getTop100Rankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的no字段作为排名 + List top100List = historyTop100Mapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的no字段作为排名 + for (HistoryTop100 item : top100List) { + result.put(item.getBallNumber(), item.getNo()); + } + + // 对于没有在百期排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 获取球号在历史排行表中的排位 + */ + private Map getHistoryRankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的no字段作为排名 + List historyTopList = historyTopMapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的no字段作为排名 + for (HistoryTop item : historyTopList) { + result.put(item.getBallNumber(), item.getNo()); + } + + // 对于没有在历史排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 参数验证 + */ + private void validateInputParams(String level, List redBalls, Integer blueBall) { + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是H/M/L之一"); + } + + if (CollectionUtils.isEmpty(redBalls) || redBalls.size() != 6) { + throw new IllegalArgumentException("红球号码必须为6个"); + } + + if (blueBall == null || blueBall < 1 || blueBall > 16) { + throw new IllegalArgumentException("蓝球号码范围应为1-16"); + } + + for (Integer redBall : redBalls) { + if (redBall < 1 || redBall > 33) { + throw new IllegalArgumentException("红球号码范围应为1-33"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/jt/jtssq/FollowBallAnalysis.java b/src/main/java/com/xy/xyaicpzs/jt/jtssq/FollowBallAnalysis.java new file mode 100644 index 0000000..d0fd3ba --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/jt/jtssq/FollowBallAnalysis.java @@ -0,0 +1,685 @@ +package com.xy.xyaicpzs.jt.jtssq; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.*; +import com.xy.xyaicpzs.domain.vo.SSQSecondStepResultVO; +import com.xy.xyaicpzs.mapper.*; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * 精推双色球第二步算法分析 + */ +@Slf4j +@Component +public class FollowBallAnalysis { + + @Autowired + private T3Mapper t3Mapper; + + @Autowired + private T4Mapper t4Mapper; + + @Autowired + private T7Mapper t7Mapper; + + @Autowired + private HistoryTopMapper historyTopMapper; + + @Autowired + private HistoryTop100Mapper historyTop100Mapper; + + /** + * 球号和系数的关联类 + */ + @Data + public static class BallWithCoefficient { + private Integer ballNumber; + private Double coefficient; + private Integer masterBallNumber; // 主球号,用于标识来源 + + public BallWithCoefficient(Integer ballNumber, Double coefficient, Integer masterBallNumber) { + this.ballNumber = ballNumber; + this.coefficient = coefficient; + this.masterBallNumber = masterBallNumber; + } + } + + /** + * 精推双色球第二步算法主方法 + * @param level 位置级别(H-高位/M-中位/L-低位) + * @param redBalls 前6个红球号码 + * @param blueBall 蓝球号码 + * @param nextFirstBall 下期首球号码 + * @return 分析结果 + */ + public SSQSecondStepResultVO analyze(String level, List redBalls, Integer blueBall, Integer nextFirstBall) { + // 参数验证 + validateInputParams(level, redBalls, blueBall, nextFirstBall); + + log.info("开始精推双色球第二步分析,策略:{},红球:{},蓝球:{},下期首球:{}", + level, redBalls, blueBall, nextFirstBall); + + // 用于存储所有候选球号和对应的系数 + Map> ballCoefficientMap = new HashMap<>(); + List allCandidateBalls = new ArrayList<>(); + + // Step 1: 根据6个红球号码获取候选球(T3表前26个) + for (Integer redBall : redBalls) { + List ballsWithCoefficients = getTop26FromT3(redBall); + for (BallWithCoefficient ball : ballsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + } + + // Step 2: 从history_top_100百期排行获取前3个球号 + List top3HistoryTop100 = getTop3FromHistoryTop100(); + allCandidateBalls.addAll(top3HistoryTop100); + + // Step 3: 根据蓝球号码获取候选球(T4表前26个) + List blueBallsWithCoefficients = getTop26FromT4(blueBall); + for (BallWithCoefficient ball : blueBallsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // Step 4: 根据下期首球号码获取候选球(T7表,根据策略不同) + List nextFirstBallsWithCoefficients = getT7BallsByLevel(nextFirstBall, level); + for (BallWithCoefficient ball : nextFirstBallsWithCoefficients) { + ballCoefficientMap.computeIfAbsent(ball.getBallNumber(), k -> new ArrayList<>()).add(ball); + allCandidateBalls.add(ball.getBallNumber()); + } + + // Step 5: 统计分析并生成结果 + List results = analyzeResults(allCandidateBalls, ballCoefficientMap); + + return SSQSecondStepResultVO.builder() + .results(results) + .strategy(level) + .redBalls(redBalls) + .blueBall(blueBall) + .nextFirstBall(nextFirstBall) + .build(); + } + + /** + * 从T3表获取前26个球号和系数(如果第26个系数相同则一并加入) + */ + private List getTop26FromT3(Integer masterBallNumber) { + List t3List = t3Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient")); + + if (CollectionUtils.isEmpty(t3List)) { + log.warn("T3表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + if (t3List.size() <= 26) { + // 如果总数不超过26个,全部加入 + for (T3 t3 : t3List) { + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前26个 + for (int i = 0; i < 26; i++) { + T3 t3 = t3List.get(i); + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } + + // 获取第26个球号的系数 + Double boundaryCoefficient = t3List.get(25).getLineCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 26; i < t3List.size(); i++) { + T3 t3 = t3List.get(i); + if (t3.getLineCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t3.getSlaveBallNumber(), t3.getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 从T4表获取前26个球号和系数(如果第26个系数相同则一并加入) + */ + private List getTop26FromT4(Integer masterBallNumber) { + List t4List = t4Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("lineCoefficient")); + + if (CollectionUtils.isEmpty(t4List)) { + log.warn("T4表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + + if (t4List.size() <= 26) { + // 如果总数不超过26个,全部加入 + for (T4 t4 : t4List) { + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前26个 + for (int i = 0; i < 26; i++) { + T4 t4 = t4List.get(i); + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } + + // 获取第26个球号的系数 + Double boundaryCoefficient = t4List.get(25).getLineCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 26; i < t4List.size(); i++) { + T4 t4 = t4List.get(i); + if (t4.getLineCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t4.getSlaveBallNumber(), t4.getLineCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 从history_top_100获取前3个球号(按点系数排行,处理边界相同系数) + */ + private List getTop3FromHistoryTop100() { + List historyTop100List = historyTop100Mapper.selectList( + new QueryWrapper() + .orderByDesc("pointCoefficient")); + + if (CollectionUtils.isEmpty(historyTop100List)) { + log.warn("百期排行表数据为空"); + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + int index = 0; + int addedCount = 0; + + while (addedCount < 3 && index < historyTop100List.size()) { + double currentCoefficient = historyTop100List.get(index).getPointCoefficient(); + List sameCoefficientBalls = new ArrayList<>(); + + // 收集所有相同系数的球号 + while (index < historyTop100List.size() && + historyTop100List.get(index).getPointCoefficient().equals(currentCoefficient)) { + sameCoefficientBalls.add(historyTop100List.get(index).getBallNumber()); + index++; + } + + // 计算还需要多少个球 + int needCount = Math.min(3 - addedCount, sameCoefficientBalls.size()); + + if (sameCoefficientBalls.size() == 1) { + // 只有一个球号,直接加入 + result.add(sameCoefficientBalls.get(0)); + addedCount++; + } else if (needCount == sameCoefficientBalls.size()) { + // 需要选择的数量等于可用数量,全部加入 + result.addAll(sameCoefficientBalls); + addedCount += sameCoefficientBalls.size(); + } else { + // 需要从多个相同系数的球号中选择部分,处理边界冲突 + List selectedBalls = handleSSQHistoryBoundaryConflicts(sameCoefficientBalls, needCount); + result.addAll(selectedBalls); + addedCount += selectedBalls.size(); + } + } + + return result; + } + + /** + * 根据级别从T7表获取候选球号和系数 + */ + private List getT7BallsByLevel(Integer masterBallNumber, String level) { + List t7List = t7Mapper.selectList( + new QueryWrapper() + .eq("masterBallNumber", masterBallNumber) + .orderByDesc("faceCoefficient")); + + if (CollectionUtils.isEmpty(t7List)) { + log.warn("T7表中主球{}没有数据", masterBallNumber); + return new ArrayList<>(); + } + + if ("H".equalsIgnoreCase(level)) { + return getHighLevelBallsFromT7(t7List, masterBallNumber); + } else if ("M".equalsIgnoreCase(level)) { + return getMiddleLevelBallsFromT7(t7List, masterBallNumber); + } else { // L + return getLowLevelBallsFromT7(t7List, masterBallNumber); + } + } + + /** + * 高位策略:从T7表获取系数最大的前10个球号(如果第10个系数相同则一并加入) + */ + private List getHighLevelBallsFromT7(List t7List, Integer masterBallNumber) { + List result = new ArrayList<>(); + + if (t7List.size() <= 10) { + // 如果总数不超过10个,全部加入 + for (T7 t7 : t7List) { + result.add(new BallWithCoefficient(t7.getSlaveBallNumber(), t7.getFaceCoefficient(), masterBallNumber)); + } + return result; + } + + // 先加入前10个 + for (int i = 0; i < 10; i++) { + T7 t7 = t7List.get(i); + result.add(new BallWithCoefficient(t7.getSlaveBallNumber(), t7.getFaceCoefficient(), masterBallNumber)); + } + + // 获取第10个球号的系数 + Double boundaryCoefficient = t7List.get(9).getFaceCoefficient(); + + // 继续查找后面系数相同的球号 + for (int i = 10; i < t7List.size(); i++) { + T7 t7 = t7List.get(i); + if (t7.getFaceCoefficient().equals(boundaryCoefficient)) { + result.add(new BallWithCoefficient(t7.getSlaveBallNumber(), t7.getFaceCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + + return result; + } + + /** + * 中位策略:从T7表获取平均值附近10个球号(向上5个,向下4个,边界系数相同则一并加入) + */ + private List getMiddleLevelBallsFromT7(List t7List, Integer masterBallNumber) { + if (t7List.size() < 10) { + log.warn("T7表数据不足10条,实际{}条", t7List.size()); + List result = new ArrayList<>(); + for (T7 t7 : t7List) { + result.add(new BallWithCoefficient(t7.getSlaveBallNumber(), t7.getFaceCoefficient(), masterBallNumber)); + } + return result; + } + + // 计算平均系数(保留两位小数,截断方式) + double avgCoefficient = t7List.stream() + .mapToDouble(T7::getFaceCoefficient) + .average() + .orElse(0.0); + // 截断到两位小数 + avgCoefficient = Math.floor(avgCoefficient * 100) / 100; + + // 找到第一个比平均值大的位置(从下到上遍历) + int avgPosition = -1; + for (int i = t7List.size() - 1; i >= 0; i--) { + if (t7List.get(i).getFaceCoefficient() >= avgCoefficient) { + avgPosition = i; + break; + } + } + + if (avgPosition == -1) { + avgPosition = 0; // 如果没找到,取第一个 + } + + List result = new ArrayList<>(); + + // 先加入平均值位置的球号 + result.add(new BallWithCoefficient(t7List.get(avgPosition).getSlaveBallNumber(), + t7List.get(avgPosition).getFaceCoefficient(), masterBallNumber)); + + // 向上取5个球号,处理边界系数相同情况 + int upCount = 0; + Double upBoundaryCoeff = null; + for (int i = avgPosition - 1; i >= 0 && upCount < 5; i--) { + result.add(new BallWithCoefficient(t7List.get(i).getSlaveBallNumber(), + t7List.get(i).getFaceCoefficient(), masterBallNumber)); + upCount++; + if (upCount == 5) { + upBoundaryCoeff = t7List.get(i).getFaceCoefficient(); + } + } + + // 向上边界处理:继续添加与第5个球号系数相同的球号 + if (upBoundaryCoeff != null) { + for (int i = avgPosition - 6; i >= 0; i--) { + if (t7List.get(i).getFaceCoefficient().equals(upBoundaryCoeff)) { + result.add(new BallWithCoefficient(t7List.get(i).getSlaveBallNumber(), + t7List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + // 向下取4个球号,处理边界系数相同情况 + int downCount = 0; + Double downBoundaryCoeff = null; + for (int i = avgPosition + 1; i < t7List.size() && downCount < 4; i++) { + result.add(new BallWithCoefficient(t7List.get(i).getSlaveBallNumber(), + t7List.get(i).getFaceCoefficient(), masterBallNumber)); + downCount++; + if (downCount == 4) { + downBoundaryCoeff = t7List.get(i).getFaceCoefficient(); + } + } + + // 向下边界处理:继续添加与第4个球号系数相同的球号 + if (downBoundaryCoeff != null) { + for (int i = avgPosition + 5; i < t7List.size(); i++) { + if (t7List.get(i).getFaceCoefficient().equals(downBoundaryCoeff)) { + result.add(new BallWithCoefficient(t7List.get(i).getSlaveBallNumber(), + t7List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; + } + } + } + + return result; + } + + /** + * 低位策略:从T7表获取最小值向上第3-12个球(共10个球,边界系数相同则一并加入) + */ + private List getLowLevelBallsFromT7(List t7List, Integer masterBallNumber) { + if (t7List.size() < 12) { + log.warn("T7表数据不足12条,实际{}条", t7List.size()); + List result = new ArrayList<>(); + for (T7 t7 : t7List) { + result.add(new BallWithCoefficient(t7.getSlaveBallNumber(), t7.getFaceCoefficient(), masterBallNumber)); + } + return result; + } + + // 取最小值向上第3-12个球(t7List已按系数降序排列,最后的是最小的) + // 最小值在最后,向上第3个就是值数组的第 size-3 个,向上第12个就是值数组的第 size-12 个 + List result = new ArrayList<>(); + int startIndex = Math.max(0, t7List.size() - 12); // 第3位 + int endIndex = t7List.size() - 3; // 第12位 + + // 先加入基本的10个球号 + for (int i = startIndex; i <= endIndex; i++) { + T7 t7 = t7List.get(i); + result.add(new BallWithCoefficient(t7.getSlaveBallNumber(), t7.getFaceCoefficient(), masterBallNumber)); + } + + // 处理起始边界:第1个球号(startIndex)系数相同情况 + if (startIndex > 0) { + Double startBoundaryCoeff = t7List.get(startIndex).getFaceCoefficient(); + + // 继续向前查找系数相同的球号 + for (int i = startIndex - 1; i >= 0; i--) { + if (t7List.get(i).getFaceCoefficient().equals(startBoundaryCoeff)) { + result.add(new BallWithCoefficient(t7List.get(i).getSlaveBallNumber(), + t7List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + // 处理结束边界:第10个球号(endIndex)系数相同情况 + if (endIndex < t7List.size() - 1) { + Double endBoundaryCoeff = t7List.get(endIndex).getFaceCoefficient(); + + // 继续向后查找系数相同的球号 + for (int i = endIndex + 1; i < t7List.size(); i++) { + if (t7List.get(i).getFaceCoefficient().equals(endBoundaryCoeff)) { + result.add(new BallWithCoefficient(t7List.get(i).getSlaveBallNumber(), + t7List.get(i).getFaceCoefficient(), masterBallNumber)); + } else { + break; // 系数不同,停止查找 + } + } + } + + return result; + } + + /** + * 处理SSQ历史排行边界冲突,选择指定数量的球号 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 选中的球号列表 + */ + private List handleSSQHistoryBoundaryConflicts(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 使用history_top表进行筛选 + return selectFromHistoryTop(candidateBalls, selectCount); + } + + /** + * 从history_top表中选择指定数量的球号 + */ + private List selectFromHistoryTop(List candidateBalls, int selectCount) { + if (CollectionUtils.isEmpty(candidateBalls) || selectCount <= 0) { + return new ArrayList<>(); + } + + if (selectCount >= candidateBalls.size()) { + return new ArrayList<>(candidateBalls); + } + + // 获取在history_top表中的系数 + Map topCoefficients = new HashMap<>(); + for (Integer ball : candidateBalls) { + HistoryTop record = historyTopMapper.selectOne( + new QueryWrapper() + .eq("ballNumber", ball)); + if (record != null) { + topCoefficients.put(ball, record.getPointCoefficient()); + } else { + topCoefficients.put(ball, 0.0); + } + } + + // 按系数降序排序,选择前selectCount个 + List> sortedByTop = topCoefficients.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .collect(Collectors.toList()); + + List result = new ArrayList<>(); + for (int i = 0; i < Math.min(selectCount, sortedByTop.size()); i++) { + result.add(sortedByTop.get(i).getKey()); + } + + // 如果仍然不够,按原始顺序补充 + while (result.size() < selectCount && result.size() < candidateBalls.size()) { + for (Integer ball : candidateBalls) { + if (!result.contains(ball) && result.size() < selectCount) { + result.add(ball); + } + } + } + + return result; + } + + /** + * 统计分析并生成结果 + */ + private List analyzeResults( + List allCandidateBalls, + Map> ballCoefficientMap) { + + // 统计球号出现次数 + Map ballFrequencyMap = new HashMap<>(); + for (Integer ball : allCandidateBalls) { + ballFrequencyMap.put(ball, ballFrequencyMap.getOrDefault(ball, 0) + 1); + } + + // 计算系数和 + Map ballCoefficientSumMap = new HashMap<>(); + for (Map.Entry> entry : ballCoefficientMap.entrySet()) { + Integer ballNumber = entry.getKey(); + List coefficients = entry.getValue(); + double sum = coefficients.stream() + .mapToDouble(BallWithCoefficient::getCoefficient) + .sum(); + ballCoefficientSumMap.put(ballNumber, sum); + } + + // 获取所有有排行数据的球号(包括频次为0的球号) + Set allBallsWithRanking = getAllBallsWithRanking(); + + // 获取百期排位和历史排位 + Map top100RankingMap = getTop100Rankings(allBallsWithRanking); + Map historyRankingMap = getHistoryRankings(allBallsWithRanking); + + // 组装结果 + List results = new ArrayList<>(); + for (Integer ballNumber : allBallsWithRanking) { + SSQSecondStepResultVO.BallAnalysisResult result = SSQSecondStepResultVO.BallAnalysisResult.builder() + .ballNumber(ballNumber) + .frequency(ballFrequencyMap.getOrDefault(ballNumber, 0)) + .coefficientSum(ballCoefficientSumMap.getOrDefault(ballNumber, 0.0)) + .top100Ranking(top100RankingMap.get(ballNumber)) + .historyRanking(historyRankingMap.get(ballNumber)) + .build(); + results.add(result); + } + + // 按出现次数降序排列,次数相同的按球号升序排列 + results.sort((a, b) -> { + int frequencyCompare = b.getFrequency().compareTo(a.getFrequency()); + if (frequencyCompare != 0) { + return frequencyCompare; + } + return a.getBallNumber().compareTo(b.getBallNumber()); + }); + + return results; + } + + /** + * 获取所有有排行数据的球号(包括频次为0的球号) + */ + private Set getAllBallsWithRanking() { + Set allBalls = new HashSet<>(); + + // 从百期排行表获取所有球号 + List top100List = historyTop100Mapper.selectList( + new QueryWrapper()); + for (HistoryTop100 item : top100List) { + allBalls.add(item.getBallNumber()); + } + + // 从历史排行表获取所有球号 + List historyTopList = historyTopMapper.selectList( + new QueryWrapper()); + for (HistoryTop item : historyTopList) { + allBalls.add(item.getBallNumber()); + } + + return allBalls; + } + + /** + * 获取球号在百期排行表中的排位 + */ + private Map getTop100Rankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的no字段作为排名 + List top100List = historyTop100Mapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的no字段作为排名 + for (HistoryTop100 item : top100List) { + result.put(item.getBallNumber(), item.getNo()); + } + + // 对于没有在百期排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 获取球号在历史排行表中的排位 + */ + private Map getHistoryRankings(Set ballNumbers) { + Map result = new HashMap<>(); + + // 直接查询指定球号的排行数据,使用表中的no字段作为排名 + List historyTopList = historyTopMapper.selectList( + new QueryWrapper() + .in("ballNumber", ballNumbers)); + + // 使用表中的no字段作为排名 + for (HistoryTop item : historyTopList) { + result.put(item.getBallNumber(), item.getNo()); + } + + // 对于没有在历史排行表中的球号,设置为null + for (Integer ballNumber : ballNumbers) { + if (!result.containsKey(ballNumber)) { + result.put(ballNumber, null); + } + } + + return result; + } + + /** + * 参数验证 + */ + private void validateInputParams(String level, List redBalls, Integer blueBall, Integer nextFirstBall) { + if (!"H".equalsIgnoreCase(level) && !"M".equalsIgnoreCase(level) && !"L".equalsIgnoreCase(level)) { + throw new IllegalArgumentException("位置级别必须是H/M/L之一"); + } + + if (CollectionUtils.isEmpty(redBalls) || redBalls.size() != 6) { + throw new IllegalArgumentException("红球号码必须为6个"); + } + + if (blueBall == null || blueBall < 1 || blueBall > 16) { + throw new IllegalArgumentException("蓝球号码范围应为1-16"); + } + + if (nextFirstBall == null || nextFirstBall < 1 || nextFirstBall > 33) { + throw new IllegalArgumentException("下期首球号码范围应为1-33"); + } + + for (Integer redBall : redBalls) { + if (redBall < 1 || redBall > 33) { + throw new IllegalArgumentException("红球号码范围应为1-33"); + } + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D10Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D10Mapper.java new file mode 100644 index 0000000..005340c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D10Mapper.java @@ -0,0 +1,15 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D10; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【d10(d10表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D10 +*/ +public interface D10Mapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D11Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D11Mapper.java new file mode 100644 index 0000000..a6d5199 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D11Mapper.java @@ -0,0 +1,15 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D11; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【d11(d11表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D11 +*/ +public interface D11Mapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D12Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D12Mapper.java new file mode 100644 index 0000000..4c6576a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D12Mapper.java @@ -0,0 +1,16 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D12; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【d12(d12表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D12 +*/ +public interface D12Mapper extends BaseMapper { +} + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D5Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D5Mapper.java new file mode 100644 index 0000000..94e2b5f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D5Mapper.java @@ -0,0 +1,15 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D5; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【d5(d5表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D5 +*/ +public interface D5Mapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D6Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D6Mapper.java new file mode 100644 index 0000000..8dacfd6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D6Mapper.java @@ -0,0 +1,15 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D6; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【d6(d6表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D6 +*/ +public interface D6Mapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D7Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D7Mapper.java new file mode 100644 index 0000000..650d328 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D7Mapper.java @@ -0,0 +1,19 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D7; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【d7(d7表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D7 +*/ +public interface D7Mapper extends BaseMapper { +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D8Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D8Mapper.java new file mode 100644 index 0000000..f27784c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D8Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D8; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【d8(d8表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D8 +*/ +public interface D8Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/D9Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/D9Mapper.java new file mode 100644 index 0000000..5cca024 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/D9Mapper.java @@ -0,0 +1,17 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.D9; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【d9(d9表)】的数据库操作Mapper +* @createDate 2025-08-21 15:06:10 +* @Entity com.xy.xyaicpzs.domain.entity.D9 +*/ +public interface D9Mapper extends BaseMapper { +} + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistory100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistory100Mapper.java new file mode 100644 index 0000000..2255105 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistory100Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistory100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_100(大乐透后区最近100期数据表)】的数据库操作Mapper +* @createDate 2025-08-21 11:35:47 +* @Entity com.xy.xyaicpzs.domain.entity.DltBackendHistory100 +*/ +public interface DltBackendHistory100Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryAllMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryAllMapper.java new file mode 100644 index 0000000..eefe529 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryAllMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryAll; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_all(大乐透后区全部历史数据表)】的数据库操作Mapper +* @createDate 2025-08-21 11:35:47 +* @Entity com.xy.xyaicpzs.domain.entity.DltBackendHistoryAll +*/ +public interface DltBackendHistoryAllMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryTop100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryTop100Mapper.java new file mode 100644 index 0000000..00b08f2 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryTop100Mapper.java @@ -0,0 +1,17 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_top_100(大乐透后区百期数据排行表)】的数据库操作Mapper +* @createDate 2025-08-20 16:24:40 +* @Entity com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop100 +*/ +public interface DltBackendHistoryTop100Mapper extends BaseMapper { + + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryTopMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryTopMapper.java new file mode 100644 index 0000000..bd986b4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltBackendHistoryTopMapper.java @@ -0,0 +1,17 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_top(大乐透后区历史数据排行表)】的数据库操作Mapper +* @createDate 2025-08-20 16:24:40 +* @Entity com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop +*/ +public interface DltBackendHistoryTopMapper extends BaseMapper { + + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltDrawRecordMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltDrawRecordMapper.java new file mode 100644 index 0000000..be2de51 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltDrawRecordMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【dlt_draw_record(大乐透开奖信息表)】的数据库操作Mapper +* @createDate 2025-08-20 15:55:06 +* @Entity com.xy.xyaicpzs.domain.entity.DltDrawRecord +*/ +public interface DltDrawRecordMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistory100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistory100Mapper.java new file mode 100644 index 0000000..b259f12 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistory100Mapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistory100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_100(大乐透前区最近100期数据表)】的数据库操作Mapper +* @createDate 2025-08-20 16:24:40 +* @Entity com.xy.xyaicpzs.domain.entity.DltFrontendHistory100 +*/ +public interface DltFrontendHistory100Mapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryAllMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryAllMapper.java new file mode 100644 index 0000000..c05d417 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryAllMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryAll; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_all(大乐透前区全部历史数据表)】的数据库操作Mapper +* @createDate 2025-08-20 16:24:40 +* @Entity com.xy.xyaicpzs.domain.entity.DltFrontendHistoryAll +*/ +public interface DltFrontendHistoryAllMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryTop100Mapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryTop100Mapper.java new file mode 100644 index 0000000..fcc8f91 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryTop100Mapper.java @@ -0,0 +1,17 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop100; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_top_100(大乐透前区百期数据排行表)】的数据库操作Mapper +* @createDate 2025-08-20 16:24:40 +* @Entity com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop100 +*/ +public interface DltFrontendHistoryTop100Mapper extends BaseMapper { + + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryTopMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryTopMapper.java new file mode 100644 index 0000000..c26c766 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltFrontendHistoryTopMapper.java @@ -0,0 +1,17 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import java.util.List; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_top(大乐透前区历史数据排行表)】的数据库操作Mapper +* @createDate 2025-08-20 16:24:40 +* @Entity com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop +*/ +public interface DltFrontendHistoryTopMapper extends BaseMapper { + + +} diff --git a/src/main/java/com/xy/xyaicpzs/mapper/DltPredictRecordMapper.java b/src/main/java/com/xy/xyaicpzs/mapper/DltPredictRecordMapper.java new file mode 100644 index 0000000..ba96f32 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/mapper/DltPredictRecordMapper.java @@ -0,0 +1,18 @@ +package com.xy.xyaicpzs.mapper; + +import com.xy.xyaicpzs.domain.entity.DltPredictRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** +* @author XY003 +* @description 针对表【dlt_predict_record(大乐透推测记录表)】的数据库操作Mapper +* @createDate 2025-09-08 14:01:12 +* @Entity com.xy.xyaicpzs.domain.entity.DltPredictRecord +*/ +public interface DltPredictRecordMapper extends BaseMapper { + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/BallAnalysisService.java b/src/main/java/com/xy/xyaicpzs/service/BallAnalysisService.java index afa3ddc..2902076 100644 --- a/src/main/java/com/xy/xyaicpzs/service/BallAnalysisService.java +++ b/src/main/java/com/xy/xyaicpzs/service/BallAnalysisService.java @@ -15,6 +15,9 @@ import java.util.stream.Collectors; import com.xy.xyaicpzs.domain.vo.BallHitRateVO; import com.xy.xyaicpzs.domain.vo.PrizeEstimateVO; import com.xy.xyaicpzs.domain.vo.RedBallHitRateVO; +import com.xy.xyaicpzs.domain.vo.BallAnalysisResultVO; +import com.xy.xyaicpzs.domain.vo.FollowBallAnalysisResultVO; +import com.xy.xyaicpzs.domain.vo.BlueBallAnalysisResultVO; /** * 球号分析服务类 @@ -202,6 +205,77 @@ public class BallAnalysisService { return result; } + /** + * 球号分析算法(包含筛选过程说明) + * @param level 高位/中位/低位标识 (H/M/L) + * @param redBalls 6个红球号码 + * @param blueBall 蓝球号码 + * @return 分析结果:出现频率最高的前11位数字及筛选过程说明 + */ + public BallAnalysisResultVO analyzeBallsWithProcess(String level, List redBalls, Integer blueBall) { + log.info("开始球号分析算法(含过程说明),级别:{},红球:{},蓝球:{}", level, redBalls, blueBall); + + // 验证输入参数 + if (redBalls == null || redBalls.size() != 6) { + throw new IllegalArgumentException("红球数量必须为6个"); + } + if (blueBall == null) { + throw new IllegalArgumentException("蓝球不能为空"); + } + if (!Arrays.asList("H", "M", "L").contains(level)) { + throw new IllegalArgumentException("级别必须为H、M或L"); + } + + List allNumbers = new ArrayList<>(); + // 第一步:记录球号和系数的对应关系 + List ballsWithCoefficients = new ArrayList<>(); + + // 第一步:处理6个红球,每个红球获取17个数字 + log.info("第一步:处理6个红球,使用{}级别算法", level); + for (int i = 0; i < redBalls.size(); i++) { + Integer redBall = redBalls.get(i); + log.info("处理第{}个红球:{}", i + 1, redBall); + + List ballsWithCoeffs = getTop17FromT3WithCoefficients(redBall, level); + ballsWithCoefficients.addAll(ballsWithCoeffs); + + List ballNumbers = ballsWithCoeffs.stream() + .map(BallWithCoefficient::getBallNumber) + .collect(Collectors.toList()); + allNumbers.addAll(ballNumbers); + + log.info("红球{}获取到{}个数字:{}", redBall, ballNumbers.size(), ballNumbers); + } + + // 记录第一步的球号和系数统计 + log.info("=== 第一步球号和系数统计(共{}个) ===", ballsWithCoefficients.size()); + for (int i = 0; i < ballsWithCoefficients.size(); i++) { + BallWithCoefficient ballWithCoeff = ballsWithCoefficients.get(i); + log.info("第{}个:{}", i + 1, ballWithCoeff.toString()); + } + log.info("=== 第一步统计结束 ==="); + + // 第二步:从history_top获取前3个球号 + log.info("第二步:从history_top获取前3个球号"); + List top3Numbers = getTop3FromHistoryTop(); + allNumbers.addAll(top3Numbers); + log.info("从history_top获取到{}个数字:{}", top3Numbers.size(), top3Numbers); + + // 第三步:用蓝球从t4表获取17个数字 + log.info("第三步:用蓝球{}从t4表获取17个数字,使用{}级别算法", blueBall, level); + List blueNumbers = getTop17FromT4(blueBall, level); + allNumbers.addAll(blueNumbers); + log.info("蓝球{}获取到{}个数字:{}", blueBall, blueNumbers.size(), blueNumbers); + + log.info("总共收集到{}个数字", allNumbers.size()); + + // 第四步:统计频率并获取前11个(包含过程说明) + BallAnalysisResultVO result = getTop11ByFrequencyWithProcess(allNumbers, ballsWithCoefficients); + + log.info("球号分析算法完成,结果:{}", result.getResult()); + return result; + } + /** * 从T3表获取指定主球的17个从球号及其系数 * 根据不同级别使用不同的选择策略 @@ -1306,6 +1380,140 @@ public class BallAnalysisService { return result; } + /** + * 统计数字出现频率,返回频率最高的前11个数字(包含筛选过程说明) + * 如果频次相同的球号超过11个,使用多层筛选: + * 1. ballNumbersWithCoefficients系数和筛选 + * 2. history_top_100表排名筛选 + * 3. history_top表点系数筛选 + * 4. 随机选择 + */ + private BallAnalysisResultVO getTop11ByFrequencyWithProcess(List allNumbers, List ballsWithCoefficients) { + log.debug("统计{}个数字的出现频率", allNumbers.size()); + StringBuilder processDescription = new StringBuilder(); + + // 统计频率 + Map frequencyMap = new HashMap<>(); + for (Integer number : allNumbers) { + frequencyMap.put(number, frequencyMap.getOrDefault(number, 0) + 1); + } + + log.debug("数字频率统计:{}", frequencyMap); + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry entry : frequencyMap.entrySet()) { + Integer frequency = entry.getValue(); + Integer ballNumber = entry.getKey(); + + frequencyGroups.computeIfAbsent(frequency, k -> new ArrayList<>()).add(ballNumber); + } + + List result = new ArrayList<>(); + boolean needMultiLevelFiltering = false; + List multiLevelCandidates = new ArrayList<>(); + List directlySelectedBalls = new ArrayList<>(); + List secondaryFilteredBalls = new ArrayList<>(); + + // 统计所有参与筛选的高频球号(用于显示) + Map> allCandidateFrequencyGroups = new LinkedHashMap<>(); + int highestFrequency = frequencyGroups.keySet().iterator().next(); + + // 收集所有可能参与筛选的球号(不只是最高频率,而是前几个高频率组) + int frequencyThreshold = Math.max(1, highestFrequency - 2); // 显示最高频率、次高频率、第三高频率等 + for (Map.Entry> group : frequencyGroups.entrySet()) { + Integer frequency = group.getKey(); + List balls = group.getValue(); + Collections.sort(balls); + + // 只收集较高频率的球号用于显示 + if (frequency >= frequencyThreshold) { + allCandidateFrequencyGroups.put(frequency, new ArrayList<>(balls)); + } + } + + // 按频率从高到低处理,进行实际筛选 + for (Map.Entry> group : frequencyGroups.entrySet()) { + List balls = group.getValue(); + Collections.sort(balls); + + // 检查加入这组球号后是否会超过11个 + if (result.size() + balls.size() <= 11) { + // 不会超过11个,直接添加所有球号 + result.addAll(balls); + directlySelectedBalls.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过11个,需要从这组球号中选择部分 + int remainingSlots = 11 - result.size(); + log.info("需要从{}个频率相同的球号中选择{}个,开始多层筛选", balls.size(), remainingSlots); + + needMultiLevelFiltering = true; + multiLevelCandidates = new ArrayList<>(balls); + + // 使用新的筛选方法,返回详细的筛选步骤信息 + MultiLevelFilteringResult filteringResult = performMultiLevelFilteringWithDetails(balls, remainingSlots, ballsWithCoefficients); + result.addAll(filteringResult.getSelectedBalls()); + secondaryFilteredBalls = new ArrayList<>(filteringResult.getSelectedBalls()); + + // 记录筛选步骤信息 + processDescription.append("筛选步骤:").append(filteringResult.getStepsDescription()); + if (!filteringResult.getDetailedStepsInfo().isEmpty()) { + processDescription.append(" ").append(filteringResult.getDetailedStepsInfo()); + } + + log.info("多层筛选完成,最终选择:{}", filteringResult.getSelectedBalls()); + break; // 已经达到11个,结束 + } + + // 如果已经有11个,结束 + if (result.size() >= 11) { + break; + } + } + + // 构建详细的筛选过程说明 + StringBuilder detailedDescription = new StringBuilder(); + + if (allCandidateFrequencyGroups.isEmpty()) { + detailedDescription.append("所有球号出现频率相同,"); + } else { + detailedDescription.append("参与筛选的候选球号按频率分布为") + .append(formatBallNumbersWithFrequency(allCandidateFrequencyGroups)) + .append(";"); + } + + if (!needMultiLevelFiltering) { + detailedDescription.append("直接按频率筛选出前11个球号:") + .append(formatBallNumbersComplete(result)) + .append("。筛选步骤:通过频率筛选确定所有球号,无需进行T3系数和筛选、百期排位、历史排位。"); + } else { + detailedDescription.append("无法直接筛选出前11个"); + + if (!directlySelectedBalls.isEmpty()) { + detailedDescription.append(",其中") + .append(formatBallNumbers(directlySelectedBalls)) + .append("直接入选"); + } + + detailedDescription.append(",") + .append(formatBallNumbers(multiLevelCandidates)) + .append("需要进行二次筛选,最终筛选出") + .append(formatBallNumbers(secondaryFilteredBalls)) + .append(",组成前11个球号:") + .append(formatBallNumbersComplete(result)) + .append("。") + .append(processDescription.toString()); + } + + log.info("频率统计最终结果(共{}个):{}", result.size(), result); + + return BallAnalysisResultVO.builder() + .result(result) + .filteringProcess(detailedDescription.toString()) + .build(); + } + /** * 多层筛选:ballNumbersWithCoefficients系数和筛选 -> history_top_100表排名 -> history_top表点系数 -> 随机选择 */ @@ -1364,6 +1572,816 @@ public class BallAnalysisService { return randomSelected; } + /** + * 多层筛选(包含过程说明):ballNumbersWithCoefficients系数和筛选 -> history_top_100表排名 -> history_top表点系数 -> 随机选择 + */ + private List selectBallsByMultiLevelFilteringWithProcess(List candidateBalls, int selectCount, + List ballsWithCoefficients, StringBuilder processDescription) { + log.debug("开始多层筛选(含过程说明),候选球号:{},需要选择:{}个", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + return new ArrayList<>(candidateBalls); + } + + List currentCandidates = new ArrayList<>(candidateBalls); + List filteringSteps = new ArrayList<>(); + + // 第一层:ballNumbersWithCoefficients系数和筛选 + log.debug("=== 第一层:ballNumbersWithCoefficients系数和筛选 ==="); + List ballNumbersFiltered = selectBallsByBallNumbersWithCoefficients(currentCandidates, selectCount, ballsWithCoefficients); + log.debug("ballNumbersWithCoefficients系数和筛选结果:{}", ballNumbersFiltered); + + if (ballNumbersFiltered.size() < currentCandidates.size()) { + filteringSteps.add("T3系数和筛选"); + } + + // 如果ballNumbersWithCoefficients筛选后的结果数量等于selectCount,说明没有相同系数和的情况,直接返回 + if (ballNumbersFiltered.size() == selectCount) { + log.debug("ballNumbersWithCoefficients系数和筛选完成,选择:{}", ballNumbersFiltered); + return ballNumbersFiltered; + } + + // 如果ballNumbersWithCoefficients筛选后的结果数量超过selectCount,说明有系数和相同的情况,继续下一层筛选 + currentCandidates = ballNumbersFiltered; + log.debug("ballNumbersWithCoefficients筛选后剩余候选:{}", currentCandidates); + + // 第二层:history_top_100表排名筛选 + log.debug("=== 第二层:history_top_100表排名筛选 ==="); + List historyTop100Filtered = selectBallsByHistoryTop100Ranking(currentCandidates, selectCount); + log.debug("history_top_100表排名筛选结果:{}", historyTop100Filtered); + + if (historyTop100Filtered.size() < currentCandidates.size()) { + filteringSteps.add("history_top_100表排名筛选"); + } + + if (historyTop100Filtered.size() == selectCount) { + log.debug("history_top_100表排名筛选完成,选择:{}", historyTop100Filtered); + return historyTop100Filtered; + } + currentCandidates = historyTop100Filtered; + log.debug("history_top_100表筛选后剩余候选:{}", currentCandidates); + + // 第三层:history_top表点系数筛选 + log.debug("=== 第三层:history_top表点系数筛选 ==="); + List historyTopFiltered = selectBallsByHistoryTopPointCoefficient(currentCandidates, selectCount); + log.debug("history_top表点系数筛选结果:{}", historyTopFiltered); + + if (historyTopFiltered.size() < currentCandidates.size()) { + filteringSteps.add("history_top表点系数筛选"); + } + + if (historyTopFiltered.size() == selectCount) { + log.debug("history_top表点系数筛选完成,选择:{}", historyTopFiltered); + return historyTopFiltered; + } + currentCandidates = historyTopFiltered; + log.debug("history_top表筛选后剩余候选:{}", currentCandidates); + + // 第四层:随机选择 + log.debug("=== 第四层:随机选择 ==="); + List randomSelected = selectBallsRandomly(currentCandidates, selectCount); + log.debug("随机筛选完成,选择:{}", randomSelected); + + if (currentCandidates.size() > selectCount) { + filteringSteps.add("随机选择"); + } + + return randomSelected; + } + + /** + * 筛选详情结果内部类 + */ + private static class FilteringDetailResult { + private List filteredBalls; + private String detailInfo; + + public FilteringDetailResult(List filteredBalls, String detailInfo) { + this.filteredBalls = filteredBalls; + this.detailInfo = detailInfo; + } + + public List getFilteredBalls() { + return filteredBalls; + } + + public String getDetailInfo() { + return detailInfo; + } + } + + /** + * 多层筛选结果内部类 + */ + private static class MultiLevelFilteringResult { + private List selectedBalls; + private String stepsDescription; + private String detailedStepsInfo; + + public MultiLevelFilteringResult(List selectedBalls, String stepsDescription) { + this.selectedBalls = selectedBalls; + this.stepsDescription = stepsDescription; + this.detailedStepsInfo = ""; + } + + public MultiLevelFilteringResult(List selectedBalls, String stepsDescription, String detailedStepsInfo) { + this.selectedBalls = selectedBalls; + this.stepsDescription = stepsDescription; + this.detailedStepsInfo = detailedStepsInfo; + } + + public List getSelectedBalls() { + return selectedBalls; + } + + public String getStepsDescription() { + return stepsDescription; + } + + public String getDetailedStepsInfo() { + return detailedStepsInfo; + } + } + + /** + * 跟随球号多级筛选结果内部类 + */ + private static class FollowBallMultiLevelFilteringResult { + private List selectedBalls; + private String stepsDescription; + private String detailedStepsInfo; + + public FollowBallMultiLevelFilteringResult(List selectedBalls, String stepsDescription) { + this.selectedBalls = selectedBalls; + this.stepsDescription = stepsDescription; + this.detailedStepsInfo = ""; + } + + public FollowBallMultiLevelFilteringResult(List selectedBalls, String stepsDescription, String detailedStepsInfo) { + this.selectedBalls = selectedBalls; + this.stepsDescription = stepsDescription; + this.detailedStepsInfo = detailedStepsInfo; + } + + public List getSelectedBalls() { + return selectedBalls; + } + + public String getStepsDescription() { + return stepsDescription; + } + + public String getDetailedStepsInfo() { + return detailedStepsInfo; + } + } + + /** + * 跟随球号多级筛选(带详细过程说明) + * @param candidateBalls 候选球号 + * @param selectCount 需要选择的数量 + * @param step1BallsWithCoefficients 第一步T7面系数数据 + * @return 筛选结果和详细说明 + */ + private FollowBallMultiLevelFilteringResult performFollowBallMultiLevelFilteringWithDetails(List candidateBalls, int selectCount, List step1BallsWithCoefficients) { + List completedSteps = new ArrayList<>(); + List skippedSteps = new ArrayList<>(); + + // 构建 step1 球号->面系数 映射(找不到则为0) + Map num2Coeff = new HashMap<>(); + if (step1BallsWithCoefficients != null) { + for (BallWithCoefficient bwc : step1BallsWithCoefficients) { + num2Coeff.put(bwc.getBallNumber(), bwc.getCoefficient()); + } + } + + // 先按面系数降序,再按球号升序 + List sortedByT7 = new ArrayList<>(candidateBalls); + sortedByT7.sort((a, b) -> { + double ca = num2Coeff.getOrDefault(a, 0.0); + double cb = num2Coeff.getOrDefault(b, 0.0); + if (Double.compare(cb, ca) != 0) { + return Double.compare(cb, ca); + } + return Integer.compare(a, b); + }); + + // 记录所有候选球号的T7面系数详情 + StringBuilder t7Details = new StringBuilder(); + Map> t7Groups = new TreeMap<>(Collections.reverseOrder()); + for (Integer ball : candidateBalls) { + double coeff = num2Coeff.getOrDefault(ball, 0.0); + t7Groups.computeIfAbsent(coeff, k -> new ArrayList<>()).add(ball); + } + for (Map.Entry> entry : t7Groups.entrySet()) { + List balls = entry.getValue(); + Collections.sort(balls); + if (t7Details.length() > 0) t7Details.append(","); + t7Details.append(formatBallNumbers(balls)).append("(T7面系数").append(String.format("%.2f", entry.getKey())).append(")"); + } + + // 如果正好能取齐,直接取 + if (sortedByT7.size() <= selectCount) { + completedSteps.add("通过T7面系数筛选确定"); + skippedSteps.add("百期排位"); + skippedSteps.add("历史排位"); + skippedSteps.add("随机选择"); + + String stepsDesc = buildFollowBallStepsDescription(completedSteps, skippedSteps); + String detailInfo = "T7面系数筛选:" + formatBallNumbers(candidateBalls) + "参与筛选," + + formatBallNumbers(sortedByT7) + "通过T7面系数筛选出来。" + + "T7面系数详情:" + t7Details.toString() + "。"; + + return new FollowBallMultiLevelFilteringResult(sortedByT7, stepsDesc, detailInfo); + } + + // 检查是否存在边界相同面系数 + double thresholdCoeff = num2Coeff.getOrDefault(sortedByT7.get(selectCount - 1), 0.0); + + // 统计与阈值系数相同的球号数量 + long countAtThreshold = sortedByT7.stream() + .mapToDouble(bn -> num2Coeff.getOrDefault(bn, 0.0)) + .filter(c -> Double.compare(c, thresholdCoeff) == 0) + .count(); + + // 如果阈值系数只有1个球号,说明没有边界重复,直接取前selectCount个 + if (countAtThreshold == 1) { + List directSelected = sortedByT7.subList(0, selectCount); + completedSteps.add("通过T7面系数筛选确定"); + skippedSteps.add("百期排位"); + skippedSteps.add("历史排位"); + skippedSteps.add("随机选择"); + + String stepsDesc = buildFollowBallStepsDescription(completedSteps, skippedSteps); + String detailInfo = "T7面系数筛选:" + formatBallNumbers(candidateBalls) + "参与筛选," + + formatBallNumbers(directSelected) + "通过T7面系数筛选出来。" + + "T7面系数详情:" + t7Details.toString() + "。"; + + return new FollowBallMultiLevelFilteringResult(directSelected, stepsDesc, detailInfo); + } + + // 存在边界相同面系数:拆分高于阈值与阈值相等的集合 + List higherThanThreshold = new ArrayList<>(); + List equalThresholdGroup = new ArrayList<>(); + for (Integer bn : sortedByT7) { + double c = num2Coeff.getOrDefault(bn, 0.0); + if (c > thresholdCoeff) { + higherThanThreshold.add(bn); + } else if (Double.compare(c, thresholdCoeff) == 0) { + equalThresholdGroup.add(bn); + } + } + + List result = new ArrayList<>(higherThanThreshold); + int stillNeed = selectCount - higherThanThreshold.size(); + + if (stillNeed > 0) { + // 需要进一步筛选 + FollowBallTop100HistoryFilteringResult furtherResult = selectByTop100ThenHistoryTopWithDetails(equalThresholdGroup, stillNeed); + result.addAll(furtherResult.getSelectedBalls()); + + completedSteps.add("通过T7面系数和百期排位、历史排位筛选确定"); + + StringBuilder allDetails = new StringBuilder(); + allDetails.append("T7面系数筛选:").append(formatBallNumbers(candidateBalls)).append("参与筛选,"); + allDetails.append(formatBallNumbers(higherThanThreshold)).append("通过T7面系数直接筛选出来,"); + allDetails.append(formatBallNumbers(equalThresholdGroup)).append("因T7面系数相同需要进入百期排位、历史排位筛选。"); + allDetails.append("T7面系数详情:").append(t7Details.toString()).append("。"); + if (!furtherResult.getDetailedInfo().isEmpty()) { + allDetails.append(" ").append(furtherResult.getDetailedInfo()); + } + + String stepsDesc = buildFollowBallStepsDescription(completedSteps, skippedSteps); + return new FollowBallMultiLevelFilteringResult(result, stepsDesc, allDetails.toString()); + } else { + completedSteps.add("通过T7面系数筛选确定"); + skippedSteps.add("百期排位"); + skippedSteps.add("历史排位"); + skippedSteps.add("随机选择"); + + String stepsDesc = buildFollowBallStepsDescription(completedSteps, skippedSteps); + String detailInfo = "T7面系数筛选:" + formatBallNumbers(candidateBalls) + "参与筛选," + + formatBallNumbers(result) + "通过T7面系数筛选出来。" + + "T7面系数详情:" + t7Details.toString() + "。"; + + return new FollowBallMultiLevelFilteringResult(result, stepsDesc, detailInfo); + } + } + + /** + * 跟随球号百期排位和历史排位筛选结果 + */ + private static class FollowBallTop100HistoryFilteringResult { + private List selectedBalls; + private String detailedInfo; + + public FollowBallTop100HistoryFilteringResult(List selectedBalls, String detailedInfo) { + this.selectedBalls = selectedBalls; + this.detailedInfo = detailedInfo; + } + + public List getSelectedBalls() { + return selectedBalls; + } + + public String getDetailedInfo() { + return detailedInfo; + } + } + + /** + * 蓝球多级筛选结果内部类 + */ + private static class BlueBallMultiLevelFilteringResult { + private List selectedBalls; + private String stepsDescription; + private String detailedStepsInfo; + + public BlueBallMultiLevelFilteringResult(List selectedBalls, String stepsDescription) { + this.selectedBalls = selectedBalls; + this.stepsDescription = stepsDescription; + this.detailedStepsInfo = ""; + } + + public BlueBallMultiLevelFilteringResult(List selectedBalls, String stepsDescription, String detailedStepsInfo) { + this.selectedBalls = selectedBalls; + this.stepsDescription = stepsDescription; + this.detailedStepsInfo = detailedStepsInfo; + } + + public List getSelectedBalls() { + return selectedBalls; + } + + public String getStepsDescription() { + return stepsDescription; + } + + public String getDetailedStepsInfo() { + return detailedStepsInfo; + } + } + + /** + * 通过百期排位和历史排位进行筛选(带详细信息) + */ + private FollowBallTop100HistoryFilteringResult selectByTop100ThenHistoryTopWithDetails(List candidateBalls, int needCount) { + if (candidateBalls == null || candidateBalls.isEmpty() || needCount <= 0) { + return new FollowBallTop100HistoryFilteringResult(new ArrayList<>(), ""); + } + + StringBuilder detailedInfo = new StringBuilder(); + + // 在同T7系数的候选中,先用100期逐组加入 + QueryWrapper query = new QueryWrapper<>(); + query.in("ballNumber", candidateBalls) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); + List top100List = historyTop100Mapper.selectList(query); + + List selectedByTop100 = new ArrayList<>(); + List undecidedGroup = new ArrayList<>(); + StringBuilder top100Details = new StringBuilder(); + + if (top100List != null && !top100List.isEmpty()) { + // 记录所有球号的百期排位详情 + Map> top100Groups = new TreeMap<>(Collections.reverseOrder()); + for (HistoryTop100 item : top100List) { + top100Groups.computeIfAbsent(item.getPointCoefficient(), k -> new ArrayList<>()).add(item.getBallNumber()); + } + for (Map.Entry> entry : top100Groups.entrySet()) { + List balls = entry.getValue(); + Collections.sort(balls); + if (top100Details.length() > 0) top100Details.append(","); + top100Details.append(formatBallNumbers(balls)).append("(百期排位").append(String.format("%.2f", entry.getKey())).append(")"); + } + + // 记录不在百期表中的球号 + List notInTop100 = new ArrayList<>(candidateBalls); + List inTop100 = top100List.stream().map(HistoryTop100::getBallNumber).collect(Collectors.toList()); + notInTop100.removeAll(inTop100); + if (!notInTop100.isEmpty()) { + Collections.sort(notInTop100); + if (top100Details.length() > 0) top100Details.append(","); + top100Details.append(formatBallNumbers(notInTop100)).append("(无百期排位)"); + } + + // 分组筛选 + Map> groups = new LinkedHashMap<>(); + for (HistoryTop100 item : top100List) { + groups.computeIfAbsent(item.getPointCoefficient(), k -> new ArrayList<>()) + .add(item.getBallNumber()); + } + for (Map.Entry> entry : groups.entrySet()) { + List groupBalls = entry.getValue(); + if (selectedByTop100.size() + groupBalls.size() <= needCount) { + selectedByTop100.addAll(groupBalls); + } else { + undecidedGroup.addAll(groupBalls); + break; + } + if (selectedByTop100.size() == needCount) { + break; + } + } + } + + if (selectedByTop100.size() >= needCount) { + detailedInfo.append("百期排位筛选:").append(formatBallNumbers(candidateBalls)).append("参与筛选,"); + detailedInfo.append(formatBallNumbers(selectedByTop100.subList(0, needCount))).append("通过百期排位筛选出来。"); + detailedInfo.append("百期排位详情:").append(top100Details.toString()).append("。"); + return new FollowBallTop100HistoryFilteringResult(selectedByTop100.subList(0, needCount), detailedInfo.toString()); + } + + // 还需要历史排位筛选 + int stillNeed = needCount - selectedByTop100.size(); + List forHistoryTop = !undecidedGroup.isEmpty() ? new ArrayList<>(undecidedGroup) : + (top100List == null || top100List.isEmpty() ? new ArrayList<>(candidateBalls) : new ArrayList<>()); + + List selectedByHistoryTop = new ArrayList<>(); + StringBuilder historyDetails = new StringBuilder(); + + if (!forHistoryTop.isEmpty()) { + QueryWrapper historyQuery = new QueryWrapper<>(); + historyQuery.in("ballNumber", forHistoryTop) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); + List historyTopList = historyTopMapper.selectList(historyQuery); + + if (historyTopList != null && !historyTopList.isEmpty()) { + // 记录所有球号的历史排位详情 + Map> historyGroups = new TreeMap<>(Collections.reverseOrder()); + for (HistoryTop item : historyTopList) { + historyGroups.computeIfAbsent(item.getPointCoefficient(), k -> new ArrayList<>()).add(item.getBallNumber()); + } + for (Map.Entry> entry : historyGroups.entrySet()) { + List balls = entry.getValue(); + Collections.sort(balls); + if (historyDetails.length() > 0) historyDetails.append(","); + historyDetails.append(formatBallNumbers(balls)).append("(历史点系数").append(String.format("%.2f", entry.getKey())).append(")"); + } + + // 记录不在历史表中的球号 + List notInHistory = new ArrayList<>(forHistoryTop); + List inHistory = historyTopList.stream().map(HistoryTop::getBallNumber).collect(Collectors.toList()); + notInHistory.removeAll(inHistory); + if (!notInHistory.isEmpty()) { + Collections.sort(notInHistory); + if (historyDetails.length() > 0) historyDetails.append(","); + historyDetails.append(formatBallNumbers(notInHistory)).append("(无历史数据)"); + } + + // 按点系数筛选 + selectedByHistoryTop.addAll(historyTopList.stream() + .limit(stillNeed) + .map(HistoryTop::getBallNumber) + .collect(Collectors.toList())); + } + } + + List finalResult = new ArrayList<>(selectedByTop100); + finalResult.addAll(selectedByHistoryTop); + + // 构建详细信息 + detailedInfo.append("百期排位筛选:").append(formatBallNumbers(candidateBalls)).append("参与筛选,"); + if (!selectedByTop100.isEmpty()) { + detailedInfo.append(formatBallNumbers(selectedByTop100)).append("通过百期排位筛选出来,"); + } + if (!forHistoryTop.isEmpty()) { + detailedInfo.append(formatBallNumbers(forHistoryTop)).append("因百期排位不足或相同需要进入历史排位筛选。"); + } + if (top100Details.length() > 0) { + detailedInfo.append("百期排位详情:").append(top100Details.toString()).append("。"); + } + + if (!forHistoryTop.isEmpty() && !selectedByHistoryTop.isEmpty()) { + detailedInfo.append(" 历史排位筛选:").append(formatBallNumbers(forHistoryTop)).append("参与筛选,"); + detailedInfo.append(formatBallNumbers(selectedByHistoryTop)).append("通过历史排位筛选出来。"); + } + if (historyDetails.length() > 0) { + detailedInfo.append(" 历史排位详情:").append(historyDetails.toString()).append("。"); + } + + return new FollowBallTop100HistoryFilteringResult(finalResult, detailedInfo.toString()); + } + + /** + * 构建跟随球号筛选步骤描述 + */ + private String buildFollowBallStepsDescription(List completedSteps, List skippedSteps) { + StringBuilder sb = new StringBuilder(); + if (!completedSteps.isEmpty()) { + if (completedSteps.size() == 1) { + sb.append("通过频率筛选确定部分球号,通过").append(completedSteps.get(0)).append("确定剩余球号"); + } else { + sb.append("通过频率筛选确定部分球号,经过"); + for (int i = 0; i < completedSteps.size(); i++) { + if (i > 0) sb.append("、"); + sb.append(completedSteps.get(i)); + } + sb.append("最终确定剩余球号"); + } + } + if (!skippedSteps.isEmpty()) { + if (sb.length() > 0) sb.append(","); + sb.append("无需进行").append(String.join("、", skippedSteps)); + } + return sb.append("。").toString(); + } + + /** + * 蓝球多级筛选(带详细过程说明) + * @param candidateBalls 候选蓝球号码 + * @param selectCount 需要选择的数量 + * @param step2BlueT8Coefficients 第二步T8面系数数据 + * @return 筛选结果和详细说明 + */ + private BlueBallMultiLevelFilteringResult performBlueBallMultiLevelFilteringWithDetails(List candidateBalls, int selectCount, List step2BlueT8Coefficients) { + List completedSteps = new ArrayList<>(); + List skippedSteps = new ArrayList<>(); + + // 统计同频候选在step2BlueT8Coefficients中的面系数和(找不到视为0) + Map sumMap = new HashMap<>(); + StringBuilder t8Details = new StringBuilder(); + Map> t8Groups = new TreeMap<>(Collections.reverseOrder()); + + for (Integer ball : candidateBalls) { + double sum = 0.0; + if (step2BlueT8Coefficients != null) { + for (BallWithCoefficient bwc : step2BlueT8Coefficients) { + if (ball.equals(bwc.getBallNumber())) { + sum += bwc.getCoefficient(); + } + } + } + sumMap.put(ball, sum); + t8Groups.computeIfAbsent(sum, k -> new ArrayList<>()).add(ball); + } + + // 记录所有候选球号的T8面系数和详情 + for (Map.Entry> entry : t8Groups.entrySet()) { + List balls = entry.getValue(); + Collections.sort(balls); + if (t8Details.length() > 0) t8Details.append(","); + t8Details.append(formatBallNumbers(balls)).append("(T8面系数和").append(String.format("%.2f", entry.getKey())).append(")"); + } + + // 按"系数和降序、球号升序"排序,取前selectCount个 + List sortedCandidates = new ArrayList<>(candidateBalls); + sortedCandidates.sort((a, b) -> { + int cmp = Double.compare(sumMap.getOrDefault(b, 0.0), sumMap.getOrDefault(a, 0.0)); + if (cmp != 0) return cmp; + return Integer.compare(a, b); + }); + + List result = sortedCandidates.subList(0, Math.min(selectCount, sortedCandidates.size())); + + completedSteps.add("通过T8面系数和筛选确定"); + skippedSteps.add("百期排位"); + skippedSteps.add("历史排位"); + + String stepsDesc = buildBlueBallStepsDescription(completedSteps, skippedSteps); + String detailInfo = "T8面系数和筛选:" + formatBallNumbers(candidateBalls) + "参与筛选," + + formatBallNumbers(result) + "通过T8面系数和筛选出来。" + + "T8面系数和详情:" + t8Details.toString() + "。"; + + return new BlueBallMultiLevelFilteringResult(result, stepsDesc, detailInfo); + } + + /** + * 构建蓝球筛选步骤描述 + */ + private String buildBlueBallStepsDescription(List completedSteps, List skippedSteps) { + StringBuilder sb = new StringBuilder(); + if (!completedSteps.isEmpty()) { + if (completedSteps.size() == 1) { + sb.append("通过频率筛选确定部分球号,通过").append(completedSteps.get(0)).append("确定剩余球号"); + } else { + sb.append("通过频率筛选确定部分球号,经过"); + for (int i = 0; i < completedSteps.size(); i++) { + if (i > 0) sb.append("、"); + sb.append(completedSteps.get(i)); + } + sb.append("最终确定剩余球号"); + } + } + if (!skippedSteps.isEmpty()) { + if (sb.length() > 0) sb.append(","); + sb.append("无需进行").append(String.join("、", skippedSteps)); + } + return sb.append("。").toString(); + } + + /** + * 格式化球号列表为字符串 + */ + private String formatBallNumbers(List ballNumbers) { + if (ballNumbers == null || ballNumbers.isEmpty()) { + return ""; + } + + List sortedBalls = new ArrayList<>(ballNumbers); + Collections.sort(sortedBalls); + + if (sortedBalls.size() <= 10) { + return sortedBalls.toString().replace("[", "").replace("]", ""); + } else { + // 如果球号太多,只显示前几个和后几个 + List displayBalls = new ArrayList<>(); + displayBalls.addAll(sortedBalls.subList(0, 5)); + displayBalls.addAll(sortedBalls.subList(sortedBalls.size() - 3, sortedBalls.size())); + return displayBalls.subList(0, 5).toString().replace("[", "").replace("]", "") + + "等" + sortedBalls.size() + "个"; + } + } + + /** + * 格式化球号列表为字符串(完整显示,不使用"等"字简化) + */ + private String formatBallNumbersComplete(List ballNumbers) { + if (ballNumbers == null || ballNumbers.isEmpty()) { + return ""; + } + + List sortedBalls = new ArrayList<>(ballNumbers); + Collections.sort(sortedBalls); + + return sortedBalls.toString().replace("[", "").replace("]", ""); + } + + /** + * 格式化带频率的球号列表为字符串 + * @param frequencyGroups 按频率分组的球号,key为频率,value为球号列表 + */ + private String formatBallNumbersWithFrequency(Map> frequencyGroups) { + if (frequencyGroups == null || frequencyGroups.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + List parts = new ArrayList<>(); + + // 按频率从高到低排序 + List>> sortedEntries = frequencyGroups.entrySet().stream() + .sorted(Map.Entry.>comparingByKey().reversed()) + .collect(Collectors.toList()); + + for (Map.Entry> entry : sortedEntries) { + Integer frequency = entry.getKey(); + List balls = entry.getValue(); + + // 对球号排序 + Collections.sort(balls); + + // 始终显示所有球号,不使用省略 + String ballsStr = balls.toString().replace("[", "").replace("]", ""); + parts.add(ballsStr + "(出现" + frequency + "次)"); + } + + // 组合所有部分,显示完整的频率分布信息 + for (int i = 0; i < parts.size(); i++) { + if (i > 0) { + result.append(","); + } + result.append(parts.get(i)); + } + + return result.toString(); + } + + /** + * 执行多层筛选并返回详细的步骤信息 + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @param ballsWithCoefficients 系数数据 + * @return 筛选结果和步骤描述 + */ + private MultiLevelFilteringResult performMultiLevelFilteringWithDetails(List candidateBalls, + int selectCount, List ballsWithCoefficients) { + log.debug("开始详细多层筛选,候选球号:{},需要选择:{}个", candidateBalls, selectCount); + + if (candidateBalls.size() <= selectCount) { + return new MultiLevelFilteringResult(new ArrayList<>(candidateBalls), "直接选择,无需进一步筛选。"); + } + + List currentCandidates = new ArrayList<>(candidateBalls); + List completedSteps = new ArrayList<>(); + List skippedSteps = new ArrayList<>(); + StringBuilder detailedInfo = new StringBuilder(); + + // 第一层:ballNumbersWithCoefficients系数和筛选 + log.debug("=== 第一层:T3系数和筛选 ==="); + FilteringDetailResult t3Result = selectBallsByBallNumbersWithCoefficientsWithDetails(currentCandidates, selectCount, ballsWithCoefficients); + List ballNumbersFiltered = t3Result.getFilteredBalls(); + log.debug("T3系数和筛选结果:{}", ballNumbersFiltered); + + if (ballNumbersFiltered.size() < currentCandidates.size()) { + completedSteps.add("T3系数和筛选"); + if (!t3Result.getDetailInfo().isEmpty()) { + detailedInfo.append("T3系数和详情:").append(t3Result.getDetailInfo()).append("。"); + } + } + + if (ballNumbersFiltered.size() == selectCount) { + log.debug("T3系数和筛选完成,选择:{}", ballNumbersFiltered); + skippedSteps.add("百期排位"); + skippedSteps.add("历史排位"); + return new MultiLevelFilteringResult(ballNumbersFiltered, buildStepsDescription(completedSteps, skippedSteps), detailedInfo.toString()); + } + + currentCandidates = ballNumbersFiltered; + + // 第二层:history_top_100表排名筛选(百期排位) + log.debug("=== 第二层:百期排位筛选 ==="); + FilteringDetailResult top100Result = selectBallsByHistoryTop100RankingWithDetails(currentCandidates, selectCount); + List historyTop100Filtered = top100Result.getFilteredBalls(); + log.debug("百期排位筛选结果:{}", historyTop100Filtered); + + if (historyTop100Filtered.size() < currentCandidates.size()) { + completedSteps.add("百期排位"); + if (!top100Result.getDetailInfo().isEmpty()) { + if (detailedInfo.length() > 0) detailedInfo.append(" "); + detailedInfo.append("百期排位详情:").append(top100Result.getDetailInfo()).append("。"); + } + } + + if (historyTop100Filtered.size() == selectCount) { + log.debug("百期排位筛选完成,选择:{}", historyTop100Filtered); + skippedSteps.add("历史排位"); + return new MultiLevelFilteringResult(historyTop100Filtered, buildStepsDescription(completedSteps, skippedSteps), detailedInfo.toString()); + } + currentCandidates = historyTop100Filtered; + + // 第三层:history_top表点系数筛选(历史排位) + log.debug("=== 第三层:历史排位筛选 ==="); + FilteringDetailResult historyTopResult = selectBallsByHistoryTopPointCoefficientWithDetails(currentCandidates, selectCount); + List historyTopFiltered = historyTopResult.getFilteredBalls(); + log.debug("历史排位筛选结果:{}", historyTopFiltered); + + if (historyTopFiltered.size() < currentCandidates.size()) { + completedSteps.add("历史排位"); + if (!historyTopResult.getDetailInfo().isEmpty()) { + if (detailedInfo.length() > 0) detailedInfo.append(" "); + detailedInfo.append("历史排位详情:").append(historyTopResult.getDetailInfo()).append("。"); + } + } + + if (historyTopFiltered.size() == selectCount) { + log.debug("历史排位筛选完成,选择:{}", historyTopFiltered); + return new MultiLevelFilteringResult(historyTopFiltered, buildStepsDescription(completedSteps, skippedSteps), detailedInfo.toString()); + } + currentCandidates = historyTopFiltered; + + // 第四层:随机选择 + log.debug("=== 第四层:随机选择 ==="); + List randomSelected = selectBallsRandomly(currentCandidates, selectCount); + log.debug("随机筛选完成,选择:{}", randomSelected); + + if (currentCandidates.size() > selectCount) { + completedSteps.add("随机选择"); + } + + return new MultiLevelFilteringResult(randomSelected, buildStepsDescription(completedSteps, skippedSteps), detailedInfo.toString()); + } + + /** + * 构建筛选步骤描述 + * @param completedSteps 已完成的步骤 + * @param skippedSteps 跳过的步骤 + * @return 步骤描述字符串 + */ + private String buildStepsDescription(List completedSteps, List skippedSteps) { + StringBuilder description = new StringBuilder(); + + if (!completedSteps.isEmpty()) { + if (completedSteps.size() == 1) { + description.append("通过频率筛选确定部分球号,通过").append(completedSteps.get(0)).append("确定剩余球号"); + } else { + description.append("通过频率筛选确定部分球号,经过"); + for (int i = 0; i < completedSteps.size(); i++) { + if (i > 0) description.append("、"); + description.append(completedSteps.get(i)); + } + description.append("最终确定剩余球号"); + } + } + + if (!skippedSteps.isEmpty()) { + if (description.length() > 0) { + description.append(",无需进行"); + } else { + description.append("无需进行"); + } + for (int i = 0; i < skippedSteps.size(); i++) { + if (i > 0) description.append("、"); + description.append(skippedSteps.get(i)); + } + } + + description.append("。"); + return description.toString(); + } + /** * 根据ballNumbersWithCoefficients系数和进行筛选 * 当系数和相同时,返回所有相同系数和的球号,让下一层筛选来处理 @@ -1444,6 +2462,82 @@ public class BallAnalysisService { } } + /** + * 根据ballNumbersWithCoefficients系数和进行筛选(包含详细信息) + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @param ballsWithCoefficients 系数数据 + * @return 筛选结果和详细信息 + */ + private FilteringDetailResult selectBallsByBallNumbersWithCoefficientsWithDetails(List candidateBalls, + int selectCount, List ballsWithCoefficients) { + log.debug("使用ballNumbersWithCoefficients系数和筛选{}个候选球号,需要选择{}个", candidateBalls.size(), selectCount); + + if (candidateBalls.size() <= selectCount) { + return new FilteringDetailResult(new ArrayList<>(candidateBalls), ""); + } + + // 计算每个球号的系数和 + Map ballCoefficientSum = new HashMap<>(); + for (Integer ballNumber : candidateBalls) { + double coefficientSum = calculateBallNumbersWithCoefficientsSum(ballNumber, ballsWithCoefficients); + ballCoefficientSum.put(ballNumber, coefficientSum); + log.debug("球号{}的ballNumbersWithCoefficients系数和:{}", ballNumber, coefficientSum); + } + + // 按系数和分组(从高到低排序) + Map> coefficientGroups = new TreeMap<>(Collections.reverseOrder()); + for (Integer ballNumber : candidateBalls) { + double coefficientSum = ballCoefficientSum.get(ballNumber); + coefficientGroups.computeIfAbsent(coefficientSum, k -> new ArrayList<>()).add(ballNumber); + } + + log.debug("ballNumbersWithCoefficients系数分组:{}", coefficientGroups); + + List result = new ArrayList<>(); + StringBuilder detailInfo = new StringBuilder(); + + // 先记录所有球号的系数和详细信息 + for (Map.Entry> group : coefficientGroups.entrySet()) { + Double coefficientSum = group.getKey(); + List balls = group.getValue(); + Collections.sort(balls); // 按球号升序排序 + + // 记录详细信息(显示所有参与筛选的球号) + if (detailInfo.length() > 0) detailInfo.append(","); + detailInfo.append(formatBallNumbers(balls)).append("(系数和").append(String.format("%.2f", coefficientSum)).append(")"); + } + + // 再按系数和从高到低筛选球号 + for (Map.Entry> group : coefficientGroups.entrySet()) { + Double coefficientSum = group.getKey(); + List balls = group.getValue(); + + log.debug("系数和{}的球号:{}", coefficientSum, balls); + + // 检查加入这组球号后是否会超过selectCount个 + if (result.size() + balls.size() <= selectCount) { + // 不会超过,直接添加所有球号(按球号升序排序) + Collections.sort(balls); + result.addAll(balls); + log.debug("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过,返回所有相同系数和的球号,让下一层筛选来处理 + result.addAll(balls); + log.debug("系数和相同,返回所有相同系数的球号:{},让下一层筛选处理", balls); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + log.debug("ballNumbersWithCoefficients筛选结果:{}", result); + return new FilteringDetailResult(result, detailInfo.toString()); + } + /** * 根据T3表线系数之和进行筛选 * 当线系数之和相同时,返回所有相同线系数之和的球号,让下一层筛选来处理 @@ -1548,7 +2642,7 @@ public class BallAnalysisService { * @param firstThreeRedBalls 前3个红球号码(独立的) * @param lastSixRedBalls 后6个红球号码(独立的) * @param blueBall 1个蓝球号码 - * @return 分析结果:出现频率最高的前8位数字 + * @return 分析结果:出现频率最高的前10位数字 */ public List fallowBallAnalysis(String level, List firstThreeRedBalls, List lastSixRedBalls, Integer blueBall) { @@ -1621,13 +2715,99 @@ public class BallAnalysisService { log.info("总共收集到{}个数字", allNumbers.size()); - // 第六步:统计频率并获取前8个(若同频超过8个,用第一步T7面系数作为二次筛选依据) - List result = getTop8ByFrequency(allNumbers, step1BallsWithCoefficients); + // 第六步:统计频率并获取前10个(若同频超过10个,用第一步T7面系数作为二次筛选依据) + List result = getTop10ByFrequency(allNumbers, step1BallsWithCoefficients); log.info("跟随球号分析算法完成,结果:{}", result); return result; } + /** + * 跟随球号分析算法(带筛选过程说明) + * @param level 级别:H-高,M-中,L-低 + * @param firstThreeRedBalls 前3个红球 + * @param lastSixRedBalls 后6个红球 + * @param blueBall 蓝球 + * @return 分析结果VO,包含前10个数字和筛选过程说明 + */ + public FollowBallAnalysisResultVO fallowBallAnalysisWithProcess(String level, List firstThreeRedBalls, + List lastSixRedBalls, Integer blueBall) { + log.info("开始跟随球号分析算法(带过程说明),级别:{},前3个红球:{},后6个红球:{},蓝球:{}", + level, firstThreeRedBalls, lastSixRedBalls, blueBall); + + // 验证输入参数 + if (firstThreeRedBalls == null || firstThreeRedBalls.size() != 3) { + throw new IllegalArgumentException("前3个红球数量必须为3个"); + } + if (lastSixRedBalls == null || lastSixRedBalls.size() != 6) { + throw new IllegalArgumentException("后6个红球数量必须为6个"); + } + if (blueBall == null) { + throw new IllegalArgumentException("蓝球不能为空"); + } + if (!Arrays.asList("H", "M", "L").contains(level)) { + throw new IllegalArgumentException("级别必须为H、M或L"); + } + + // 验证球号范围和重复性 + validateBallNumbers(firstThreeRedBalls, lastSixRedBalls, blueBall); + + List allNumbers = new ArrayList<>(); + + // 第一步:处理第1个红球,从T7表获取10个数字(同时记录系数) + log.info("第一步:处理第1个红球{},从T7表获取10个数字,使用{}级别算法", firstThreeRedBalls.get(0), level); + List firstRedBallNumbers = getTop10FromT7(firstThreeRedBalls.get(0), level); + allNumbers.addAll(firstRedBallNumbers); + log.info("第1个红球{}获取到{}个数字:{}", firstThreeRedBalls.get(0), firstRedBallNumbers.size(), firstRedBallNumbers); + + // 同步记录上述10个数字在T7表中的面系数,记录格式与analyzeBalls一致 + List step1BallsWithCoefficients = getT7CoefficientsFor(firstThreeRedBalls.get(0), firstRedBallNumbers); + log.info("=== 第一步(T7)球号和系数统计(共{}个) ===", step1BallsWithCoefficients.size()); + for (int i = 0; i < step1BallsWithCoefficients.size(); i++) { + BallWithCoefficient ballWithCoeff = step1BallsWithCoefficients.get(i); + log.info("第{}个:{}", i + 1, ballWithCoeff.toString()); + } + log.info("=== 第一步统计结束 ==="); + + // 第二步:处理后6个红球,每个红球从T3表获取26个数字 + log.info("第二步:处理后6个红球,每个红球从T3表获取26个数字"); + for (int i = 0; i < lastSixRedBalls.size(); i++) { + Integer redBall = lastSixRedBalls.get(i); + log.info("处理第{}个红球:{}", i + 1, redBall); + + List ballNumbers = getTop26FromT3(redBall); + allNumbers.addAll(ballNumbers); + + log.info("红球{}获取到{}个数字:{}", redBall, ballNumbers.size(), ballNumbers); + } + + // 第三步:从history_top_100获取前3个球号 + log.info("第三步:从history_top_100获取前3个球号"); + List top3Numbers = getTop3FromHistoryTop100(); + allNumbers.addAll(top3Numbers); + log.info("从history_top_100获取到{}个数字:{}", top3Numbers.size(), top3Numbers); + + // 第四步:取出前3个红球号码的后两个 + log.info("第四步:取出前3个红球号码的后两个"); + List lastTwoOfFirstThree = Arrays.asList(firstThreeRedBalls.get(1), firstThreeRedBalls.get(2)); + allNumbers.addAll(lastTwoOfFirstThree); + log.info("前3个红球的后两个:{}", lastTwoOfFirstThree); + + // 第五步:用上期蓝球从T4表获取26个蓝球号码 + log.info("第五步:用上期蓝球{}从T4表获取26个蓝球号码", blueBall); + List blueNumbers = getTop26FromT4(blueBall); + allNumbers.addAll(blueNumbers); + log.info("蓝球{}获取到{}个数字:{}", blueBall, blueNumbers.size(), blueNumbers); + + log.info("总共收集到{}个数字", allNumbers.size()); + + // 第六步:统计频率并获取前10个(带筛选过程说明) + FollowBallAnalysisResultVO result = getTop10ByFrequencyWithProcess(allNumbers, step1BallsWithCoefficients); + + log.info("跟随球号分析算法(带过程说明)完成,结果:{},过程说明:{}", result.getResult(), result.getFilteringProcess()); + return result; + } + /** * 验证球号的有效性和唯一性 */ @@ -2172,10 +3352,10 @@ public class BallAnalysisService { } /** - * 统计数字出现频率,返回频率最高的前8个数字 - * 如果频次相同的球号超过8个,使用T7表面系数进行二次筛选 + * 统计数字出现频率,返回频率最高的前10个数字 + * 如果频次相同的球号超过10个,使用T7表面系数进行二次筛选 */ - private List getTop8ByFrequency(List allNumbers, List step1BallsWithCoefficients) { + private List getTop10ByFrequency(List allNumbers, List step1BallsWithCoefficients) { log.debug("统计{}个数字的出现频率", allNumbers.size()); // 统计频率 @@ -2209,14 +3389,14 @@ public class BallAnalysisService { log.info("频率{}的球号:{}", frequency, balls); - // 检查加入这组球号后是否会超过8个 - if (result.size() + balls.size() <= 8) { - // 不会超过8个,直接添加所有球号 + // 检查加入这组球号后是否会超过10个 + if (result.size() + balls.size() <= 10) { + // 不会超过10个,直接添加所有球号 result.addAll(balls); log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); } else { - // 会超过8个:先按第一步T7面系数排序;如仍有边界相同面系数,继续按 100期 → 历史 → 随机 的回退规则,只在边界同系数的集合内决策 - int remainingSlots = 8 - result.size(); + // 会超过10个:先按第一步T7面系数排序;如仍有边界相同面系数,继续按 100期 → 历史 → 随机 的回退规则,只在边界同系数的集合内决策 + int remainingSlots = 10 - result.size(); log.info("需要从{}个频率相同的球号中选择{}个,先按第一步T7面系数进行筛选(找不到则按0处理)", balls.size(), remainingSlots); // 构建 step1 球号->面系数 映射(找不到则为0) @@ -2292,6 +3472,136 @@ public class BallAnalysisService { return result; } + /** + * 统计频率并获取前10个(带筛选过程说明) + * @param allNumbers 所有数字 + * @param step1BallsWithCoefficients 第一步T7面系数数据 + * @return 包含结果和筛选过程说明的VO对象 + */ + private FollowBallAnalysisResultVO getTop10ByFrequencyWithProcess(List allNumbers, List step1BallsWithCoefficients) { + log.debug("统计{}个数字的出现频率", allNumbers.size()); + + // 统计频率 + Map frequencyMap = new HashMap<>(); + for (Integer number : allNumbers) { + frequencyMap.put(number, frequencyMap.getOrDefault(number, 0) + 1); + } + + log.debug("数字频率统计:{}", frequencyMap); + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry entry : frequencyMap.entrySet()) { + Integer frequency = entry.getValue(); + Integer ballNumber = entry.getKey(); + + frequencyGroups.computeIfAbsent(frequency, k -> new ArrayList<>()).add(ballNumber); + } + + log.debug("按频率分组:{}", frequencyGroups); + + List result = new ArrayList<>(); + List directlySelectedBalls = new ArrayList<>(); + List multiLevelCandidates = new ArrayList<>(); + List secondaryFilteredBalls = new ArrayList<>(); + boolean needMultiLevelFiltering = false; + StringBuilder processDescription = new StringBuilder(); + String detailedStepsInfo = ""; + + // 按频率从高到低处理 + for (Map.Entry> group : frequencyGroups.entrySet()) { + Integer frequency = group.getKey(); + List balls = group.getValue(); + + // 对同频率的球号按数字升序排序 + Collections.sort(balls); + + log.info("频率{}的球号:{}", frequency, balls); + + // 检查加入这组球号后是否会超过10个 + if (result.size() + balls.size() <= 10) { + // 不会超过10个,直接添加所有球号 + result.addAll(balls); + directlySelectedBalls.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过10个,需要多级筛选 + needMultiLevelFiltering = true; + multiLevelCandidates.addAll(balls); + + int remainingSlots = 10 - result.size(); + log.info("需要从{}个频率相同的球号中选择{}个,先按第一步T7面系数进行筛选", balls.size(), remainingSlots); + + // 进行多级筛选 + FollowBallMultiLevelFilteringResult filteringResult = performFollowBallMultiLevelFilteringWithDetails(balls, remainingSlots, step1BallsWithCoefficients); + result.addAll(filteringResult.getSelectedBalls()); + secondaryFilteredBalls = new ArrayList<>(filteringResult.getSelectedBalls()); + processDescription.append("筛选步骤:").append(filteringResult.getStepsDescription()); + detailedStepsInfo = filteringResult.getDetailedStepsInfo(); + + // 已达到10个,结束 + break; + } + + // 如果已经有10个,结束 + if (result.size() >= 10) { + break; + } + } + + // 构建详细的过程描述 + StringBuilder detailedDescription = new StringBuilder(); + + // 1. 频率分布描述 - 只包含实际参与筛选的球号 + List allParticipatedBalls = new ArrayList<>(); + allParticipatedBalls.addAll(directlySelectedBalls); + allParticipatedBalls.addAll(multiLevelCandidates); + + Map> participatedFrequencyGroups = new LinkedHashMap<>(); + for (Integer ball : allParticipatedBalls) { + Integer frequency = frequencyMap.get(ball); + participatedFrequencyGroups.computeIfAbsent(frequency, k -> new ArrayList<>()).add(ball); + } + + // 按频率从高到低排序 + Map> sortedParticipatedFrequencyGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry> entry : participatedFrequencyGroups.entrySet()) { + Integer frequency = entry.getKey(); + List balls = new ArrayList<>(entry.getValue()); + Collections.sort(balls); // 球号按升序排序 + sortedParticipatedFrequencyGroups.put(frequency, balls); + } + + detailedDescription.append("参与筛选的候选球号按频率分布为"); + detailedDescription.append(formatBallNumbersWithFrequency(sortedParticipatedFrequencyGroups)); + + // 2. 筛选结果描述 + if (needMultiLevelFiltering) { + detailedDescription.append(";无法直接筛选出前10个,其中"); + detailedDescription.append(formatBallNumbers(directlySelectedBalls)); + detailedDescription.append("直接入选,"); + detailedDescription.append(formatBallNumbers(multiLevelCandidates)); + detailedDescription.append("需要进行二次筛选,最终筛选出"); + detailedDescription.append(formatBallNumbers(secondaryFilteredBalls)); + detailedDescription.append(",组成前10个球号:"); + detailedDescription.append(formatBallNumbers(result)); + detailedDescription.append("。"); + detailedDescription.append(processDescription.toString()); + if (!detailedStepsInfo.isEmpty()) { + detailedDescription.append(" ").append(detailedStepsInfo); + } + } else { + detailedDescription.append(";直接按频率筛选出前10个球号:"); + detailedDescription.append(formatBallNumbers(result)); + detailedDescription.append("。筛选步骤:通过频率筛选确定所有球号,无需进行T7面系数筛选、百期排位、历史排位。"); + } + + return FollowBallAnalysisResultVO.builder() + .result(result) + .filteringProcess(detailedDescription.toString()) + .build(); + } + /** * 根据T7表的面系数总和选择球号,支持多级筛选 * @param candidateBalls 候选球号列表 @@ -2493,6 +3803,107 @@ public class BallAnalysisService { return result; } + /** + * 根据history_top_100表的排名筛选球号(包含详细信息) + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 筛选结果和详细信息 + */ + private FilteringDetailResult selectBallsByHistoryTop100RankingWithDetails(List candidateBalls, int selectCount) { + log.info("使用history_top_100表排名筛选球号,候选:{},需要选择:{}", candidateBalls, selectCount); + + // 查询这些球号在history_top_100表中的点系数 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("ballNumber", candidateBalls) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List top100List = historyTop100Mapper.selectList(queryWrapper); + + if (top100List.isEmpty()) { + log.warn("候选球号{}在history_top_100表中未找到数据,按球号升序返回", candidateBalls); + Collections.sort(candidateBalls); + return new FilteringDetailResult(candidateBalls.subList(0, Math.min(selectCount, candidateBalls.size())), ""); + } + + // 按点系数分组 + Map> pointCoefficientGroups = new LinkedHashMap<>(); + for (HistoryTop100 item : top100List) { + Double pointCoefficient = item.getPointCoefficient(); + Integer ballNumber = item.getBallNumber(); + pointCoefficientGroups.computeIfAbsent(pointCoefficient, k -> new ArrayList<>()).add(ballNumber); + } + + log.info("history_top_100表点系数分组:{}", pointCoefficientGroups); + + List result = new ArrayList<>(); + StringBuilder detailInfo = new StringBuilder(); + + // 先记录所有球号的排位详细信息 + for (Map.Entry> group : pointCoefficientGroups.entrySet()) { + Double pointCoefficient = group.getKey(); + List balls = group.getValue(); + Collections.sort(balls); // 按球号升序排序 + + // 记录详细信息(显示所有参与筛选的球号) + if (detailInfo.length() > 0) detailInfo.append(","); + detailInfo.append(formatBallNumbers(balls)).append("(排位").append(String.format("%.2f", pointCoefficient)).append(")"); + } + + // 再按点系数从高到低筛选球号 + for (Map.Entry> group : pointCoefficientGroups.entrySet()) { + Double pointCoefficient = group.getKey(); + List balls = group.getValue(); + + log.info("点系数{}的球号:{}", pointCoefficient, balls); + + // 检查加入这组球号后是否会超过selectCount个 + if (result.size() + balls.size() <= selectCount) { + // 不会超过,直接添加所有球号(按球号升序排序) + Collections.sort(balls); + result.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过,返回所有相同点系数的球号,让下一层筛选来处理 + result.addAll(balls); + log.info("点系数相同,返回所有相同点系数的球号:{},让下一层筛选处理", balls); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 处理不在history_top_100表中的球号 + List notInTop100 = new ArrayList<>(candidateBalls); + List inTop100 = new ArrayList<>(); + for (HistoryTop100 item : top100List) { + inTop100.add(item.getBallNumber()); + } + notInTop100.removeAll(inTop100); + + // 先记录所有不在表中的球号的详细信息 + if (!notInTop100.isEmpty()) { + Collections.sort(notInTop100); + if (detailInfo.length() > 0) detailInfo.append(","); + detailInfo.append(formatBallNumbers(notInTop100)).append("(无排位)"); + } + + // 再选择需要的球号加入结果 + if (!notInTop100.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + int addCount = Math.min(remainingSlots, notInTop100.size()); + result.addAll(notInTop100.subList(0, addCount)); + + log.info("添加不在history_top_100表中的{}个球号:{}", addCount, notInTop100.subList(0, addCount)); + } + + log.info("history_top_100表筛选结果:{}", result); + return new FilteringDetailResult(result, detailInfo.toString()); + } + /** * 根据history_top表的点系数筛选球号 * 当点系数相同时,返回所有相同点系数的球号,让下一层筛选来处理 @@ -2567,6 +3978,107 @@ public class BallAnalysisService { return result; } + /** + * 根据history_top表的点系数筛选球号(包含详细信息) + * @param candidateBalls 候选球号列表 + * @param selectCount 需要选择的数量 + * @return 筛选结果和详细信息 + */ + private FilteringDetailResult selectBallsByHistoryTopPointCoefficientWithDetails(List candidateBalls, int selectCount) { + log.info("使用history_top表点系数筛选球号,候选:{},需要选择:{}", candidateBalls, selectCount); + + // 查询这些球号在history_top表中的点系数 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.in("ballNumber", candidateBalls) + .orderByDesc("pointCoefficient") + .orderByAsc("ballNumber"); // 点系数相同时按球号升序 + + List historyTopList = historyTopMapper.selectList(queryWrapper); + + if (historyTopList.isEmpty()) { + log.warn("候选球号{}在history_top表中未找到数据,按球号升序返回", candidateBalls); + Collections.sort(candidateBalls); + return new FilteringDetailResult(candidateBalls.subList(0, Math.min(selectCount, candidateBalls.size())), ""); + } + + // 按点系数分组 + Map> pointCoefficientGroups = new LinkedHashMap<>(); + for (HistoryTop item : historyTopList) { + Double pointCoefficient = item.getPointCoefficient(); + Integer ballNumber = item.getBallNumber(); + pointCoefficientGroups.computeIfAbsent(pointCoefficient, k -> new ArrayList<>()).add(ballNumber); + } + + log.info("history_top表点系数分组:{}", pointCoefficientGroups); + + List result = new ArrayList<>(); + StringBuilder detailInfo = new StringBuilder(); + + // 先记录所有球号的点系数详细信息 + for (Map.Entry> group : pointCoefficientGroups.entrySet()) { + Double pointCoefficient = group.getKey(); + List balls = group.getValue(); + Collections.sort(balls); // 按球号升序排序 + + // 记录详细信息(显示所有参与筛选的球号) + if (detailInfo.length() > 0) detailInfo.append(","); + detailInfo.append(formatBallNumbers(balls)).append("(点系数").append(String.format("%.2f", pointCoefficient)).append(")"); + } + + // 处理不在history_top表中的球号的详细信息 + List notInHistoryTop = new ArrayList<>(candidateBalls); + List inHistoryTop = new ArrayList<>(); + for (HistoryTop item : historyTopList) { + inHistoryTop.add(item.getBallNumber()); + } + notInHistoryTop.removeAll(inHistoryTop); + + // 先记录所有不在表中的球号的详细信息 + if (!notInHistoryTop.isEmpty()) { + Collections.sort(notInHistoryTop); + if (detailInfo.length() > 0) detailInfo.append(","); + detailInfo.append(formatBallNumbers(notInHistoryTop)).append("(无历史数据)"); + } + + // 再按点系数从高到低筛选球号 + for (Map.Entry> group : pointCoefficientGroups.entrySet()) { + Double pointCoefficient = group.getKey(); + List balls = group.getValue(); + + log.info("点系数{}的球号:{}", pointCoefficient, balls); + + // 检查加入这组球号后是否会超过selectCount个 + if (result.size() + balls.size() <= selectCount) { + // 不会超过,直接添加所有球号(按球号升序排序) + Collections.sort(balls); + result.addAll(balls); + log.info("直接添加{}个球号,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过,返回所有相同点系数的球号,让下一层筛选来处理 + result.addAll(balls); + log.info("点系数相同,返回所有相同点系数的球号:{},让下一层筛选处理", balls); + break; + } + + // 如果已经有selectCount个,结束 + if (result.size() >= selectCount) { + break; + } + } + + // 再选择需要的不在表中的球号加入结果 + if (!notInHistoryTop.isEmpty() && result.size() < selectCount) { + int remainingSlots = selectCount - result.size(); + int addCount = Math.min(remainingSlots, notInHistoryTop.size()); + result.addAll(notInHistoryTop.subList(0, addCount)); + + log.info("添加不在history_top表中的{}个球号:{}", addCount, notInHistoryTop.subList(0, addCount)); + } + + log.info("history_top表筛选结果:{}", result); + return new FilteringDetailResult(result, detailInfo.toString()); + } + /** * 随机选择球号 * @param candidateBalls 候选球号列表 @@ -2764,6 +4276,91 @@ public class BallAnalysisService { return result; } + /** + * 蓝球分析算法主方法(带筛选过程说明) + * @param level 高位/中位/低位标识 (H/M/L) + * @param predictedRedBalls 6个预测的红球号码 + * @param predictedBlueBalls 2个预测的蓝球号码 + * @param lastRedBalls 6个上期红球号码 + * @param lastBlueBall 1个上期蓝球号码 + * @return 分析结果:出现频率最高的前4个蓝球号码及筛选过程说明 + */ + public BlueBallAnalysisResultVO blueBallAnalysisWithProcess(String level, List predictedRedBalls, + List predictedBlueBalls, List lastRedBalls, + Integer lastBlueBall) { + log.info("开始蓝球分析算法(带过程说明),级别:{},预测红球:{},预测蓝球:{},上期红球:{},上期蓝球:{}", + level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + + // 验证输入参数 + validateBlueBallAnalysisParams(level, predictedRedBalls, predictedBlueBalls, lastRedBalls, lastBlueBall); + + List allNumbers = new ArrayList<>(); + + // 第一步:处理上期6个红球,每个红球从T6表获取12个蓝球号码 + log.info("第一步:处理上期6个红球,从T6表获取蓝球号码"); + for (int i = 0; i < lastRedBalls.size(); i++) { + Integer redBall = lastRedBalls.get(i); + log.info("处理第{}个上期红球:{}", i + 1, redBall); + + List ballNumbers = getTop12FromT6(redBall); + allNumbers.addAll(ballNumbers); + + log.info("上期红球{}从T6表获取到{}个蓝球号码:{}", redBall, ballNumbers.size(), ballNumbers); + } + + // 第二步:处理预测的6个红球,每个红球从T8表获取5个蓝球号码 + log.info("第二步:处理预测的6个红球,从T8表获取蓝球号码,使用{}级别算法", level); + for (int i = 0; i < predictedRedBalls.size(); i++) { + Integer redBall = predictedRedBalls.get(i); + log.info("处理第{}个预测红球:{}", i + 1, redBall); + + List ballNumbers = getSimpleTop5FromT8ByLevel(redBall, level); + allNumbers.addAll(ballNumbers); + + // 额外记录这5个蓝球的面系数(与 fallowBallAnalysis 第一步风格一致) + List t8Coeffs = getT8CoefficientsFor(redBall, ballNumbers); + log.info("=== 预测红球{} 对应T8面系数(共{}个) ===", redBall, t8Coeffs.size()); + for (int k = 0; k < t8Coeffs.size(); k++) { + log.info("第{}个:{}", k + 1, t8Coeffs.get(k).toString()); + } + log.info("=== 记录结束 ==="); + + log.info("预测红球{}从T8表获取到{}个蓝球号码:{}", redBall, ballNumbers.size(), ballNumbers); + } + + // 第三步:从blue_history_top_100获取前2个蓝球号码 + log.info("第三步:从blue_history_top_100获取前2个蓝球号码"); + List top2BlueNumbers = getTop2FromBlueHistoryTop100(); + allNumbers.addAll(top2BlueNumbers); + log.info("从blue_history_top_100获取到{}个蓝球号码:{}", top2BlueNumbers.size(), top2BlueNumbers); + + // 第四步:添加预测的2个蓝球号码 + log.info("第四步:添加预测的2个蓝球号码:{}", predictedBlueBalls); + allNumbers.addAll(predictedBlueBalls); + + // 第五步:用上期蓝球从T5表获取12个蓝球号码(含毛边处理) + log.info("第五步:用上期蓝球{}从T5表获取12个蓝球号码", lastBlueBall); + List blueNumbers = getTop12FromT5(lastBlueBall); + allNumbers.addAll(blueNumbers); + log.info("蓝球{}获取到{}个数字:{}", lastBlueBall, blueNumbers.size(), blueNumbers); + + log.info("总共收集到{}个蓝球号码", allNumbers.size()); + + // 汇总第二步收集的T8面系数,供第六步同频二次筛选使用 + List step2BlueT8Coefficients = new ArrayList<>(); + // 由于上面的循环内使用了临时变量 t8Coeffs,这里重新按预测红球聚合一次,确保完整 + for (Integer redBall : predictedRedBalls) { + List tmpBalls = getSimpleTop5FromT8ByLevel(redBall, level); + step2BlueT8Coefficients.addAll(getT8CoefficientsFor(redBall, tmpBalls)); + } + + // 第六步:统计频率并获取前4个(带筛选过程说明) + BlueBallAnalysisResultVO result = getSimpleTop4ByFrequencyWithProcess(allNumbers, step2BlueT8Coefficients); + + log.info("蓝球分析算法(带过程说明)完成,结果:{},过程说明:{}", result.getResult(), result.getFilteringProcess()); + return result; + } + /** * 验证蓝球分析算法的输入参数 */ @@ -3409,6 +5006,138 @@ public class BallAnalysisService { return result; } + /** + * 统计频率并获取前4个蓝球号码(带筛选过程说明) + * @param allNumbers 所有蓝球号码 + * @param step2BlueT8Coefficients 第二步T8面系数数据 + * @return 包含结果和筛选过程说明的VO对象 + */ + private BlueBallAnalysisResultVO getSimpleTop4ByFrequencyWithProcess(List allNumbers, List step2BlueT8Coefficients) { + log.debug("统计{}个蓝球号码的出现频率", allNumbers.size()); + + // 统计频率 + Map frequencyMap = new HashMap<>(); + for (Integer number : allNumbers) { + frequencyMap.put(number, frequencyMap.getOrDefault(number, 0) + 1); + } + + log.debug("蓝球号码频率统计:{}", frequencyMap); + + // 按频率分组 + Map> frequencyGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry entry : frequencyMap.entrySet()) { + Integer frequency = entry.getValue(); + Integer ballNumber = entry.getKey(); + + frequencyGroups.computeIfAbsent(frequency, k -> new ArrayList<>()).add(ballNumber); + } + + log.debug("按频率分组:{}", frequencyGroups); + + List result = new ArrayList<>(); + List directlySelectedBalls = new ArrayList<>(); + List multiLevelCandidates = new ArrayList<>(); + List secondaryFilteredBalls = new ArrayList<>(); + boolean needMultiLevelFiltering = false; + StringBuilder processDescription = new StringBuilder(); + String detailedStepsInfo = ""; + + // 构建参与筛选的球号频率分布描述 + List allParticipatedBalls = new ArrayList<>(); + Map> participatedFrequencyGroups = new LinkedHashMap<>(); + + // 按频率从高到低处理 + for (Map.Entry> group : frequencyGroups.entrySet()) { + Integer frequency = group.getKey(); + List balls = group.getValue(); + + // 对同频率的球号按数字升序排序 + Collections.sort(balls); + + log.info("频率{}的蓝球号码:{}", frequency, balls); + + // 检查加入这组球号后是否会超过4个 + if (result.size() + balls.size() <= 4) { + // 不会超过4个,直接添加所有球号 + result.addAll(balls); + directlySelectedBalls.addAll(balls); + allParticipatedBalls.addAll(balls); + log.info("直接添加{}个蓝球号码,当前总数:{}", balls.size(), result.size()); + } else { + // 会超过4个,需要多级筛选 + needMultiLevelFiltering = true; + multiLevelCandidates.addAll(balls); + allParticipatedBalls.addAll(balls); + + int remainingSlots = 4 - result.size(); + log.info("需要从{}个频率相同的蓝球号码中选择{}个,使用第二步T8面系数求和进行筛选", balls.size(), remainingSlots); + + // 进行多级筛选 + BlueBallMultiLevelFilteringResult filteringResult = performBlueBallMultiLevelFilteringWithDetails(balls, remainingSlots, step2BlueT8Coefficients); + result.addAll(filteringResult.getSelectedBalls()); + secondaryFilteredBalls = new ArrayList<>(filteringResult.getSelectedBalls()); + processDescription.append("筛选步骤:").append(filteringResult.getStepsDescription()); + detailedStepsInfo = filteringResult.getDetailedStepsInfo(); + + // 已达到4个,结束 + break; + } + + // 如果已经有4个,结束 + if (result.size() >= 4) { + break; + } + } + + // 构建参与筛选的频率分布 + for (Integer ball : allParticipatedBalls) { + Integer frequency = frequencyMap.get(ball); + participatedFrequencyGroups.computeIfAbsent(frequency, k -> new ArrayList<>()).add(ball); + } + + // 按频率从高到低排序 + Map> sortedParticipatedFrequencyGroups = new TreeMap<>(Collections.reverseOrder()); + for (Map.Entry> entry : participatedFrequencyGroups.entrySet()) { + Integer frequency = entry.getKey(); + List balls = new ArrayList<>(entry.getValue()); + Collections.sort(balls); // 球号按升序排序 + sortedParticipatedFrequencyGroups.put(frequency, balls); + } + + // 构建详细的过程描述 + StringBuilder detailedDescription = new StringBuilder(); + + // 1. 频率分布描述 - 只包含实际参与筛选的球号 + detailedDescription.append("参与筛选的候选蓝球号码按频率分布为"); + detailedDescription.append(formatBallNumbersWithFrequency(sortedParticipatedFrequencyGroups)); + + // 2. 筛选结果描述 + if (needMultiLevelFiltering) { + detailedDescription.append(";无法直接筛选出前4个,其中"); + detailedDescription.append(formatBallNumbersComplete(directlySelectedBalls)); + detailedDescription.append("直接入选,"); + detailedDescription.append(formatBallNumbersComplete(multiLevelCandidates)); + detailedDescription.append("需要进行二次筛选,最终筛选出"); + detailedDescription.append(formatBallNumbersComplete(secondaryFilteredBalls)); + detailedDescription.append(",组成前4个蓝球号码:"); + detailedDescription.append(formatBallNumbersComplete(result)); + detailedDescription.append("。"); + detailedDescription.append(processDescription.toString()); + if (!detailedStepsInfo.isEmpty()) { + detailedDescription.append(" ").append(detailedStepsInfo); + } + } else { + detailedDescription.append(";直接按频率筛选出前4个蓝球号码:"); + detailedDescription.append(formatBallNumbersComplete(result)); + detailedDescription.append("。筛选步骤:通过频率筛选确定所有球号,无需进行T8面系数和筛选、百期排位、历史排位。"); + } + + return BlueBallAnalysisResultVO.builder() + .result(result) + .filteringProcess(detailedDescription.toString()) + .build(); + } + /** * 从blue_history_top表中选择点系数最高的球号 * @param candidates 候选球号列表 diff --git a/src/main/java/com/xy/xyaicpzs/service/D10Service.java b/src/main/java/com/xy/xyaicpzs/service/D10Service.java new file mode 100644 index 0000000..8fea8cc --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D10Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D10; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d10(d10表)】的数据库操作Service +* @createDate 2025-08-21 15:06:10 +*/ +public interface D10Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/D11Service.java b/src/main/java/com/xy/xyaicpzs/service/D11Service.java new file mode 100644 index 0000000..5bc3e34 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D11Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D11; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d11(d11表)】的数据库操作Service +* @createDate 2025-08-21 15:06:10 +*/ +public interface D11Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/D12Service.java b/src/main/java/com/xy/xyaicpzs/service/D12Service.java new file mode 100644 index 0000000..1c31871 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D12Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D12; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d12(d12表)】的数据库操作Service +* @createDate 2025-08-21 15:06:10 +*/ +public interface D12Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/D5Service.java b/src/main/java/com/xy/xyaicpzs/service/D5Service.java new file mode 100644 index 0000000..b6c55df --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D5Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D5; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d5(d5表)】的数据库操作Service +* @createDate 2025-01-26 16:00:00 +*/ +public interface D5Service extends IService { + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/D6Service.java b/src/main/java/com/xy/xyaicpzs/service/D6Service.java new file mode 100644 index 0000000..2a9232a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D6Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D6; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d6(d6表)】的数据库操作Service +* @createDate 2025-08-21 14:10:22 +*/ +public interface D6Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/D7Service.java b/src/main/java/com/xy/xyaicpzs/service/D7Service.java new file mode 100644 index 0000000..c117444 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D7Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D7; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d7(d7表)】的数据库操作Service +* @createDate 2025-08-21 15:06:10 +*/ +public interface D7Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/D8Service.java b/src/main/java/com/xy/xyaicpzs/service/D8Service.java new file mode 100644 index 0000000..a62e850 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D8Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D8; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d8(d8表)】的数据库操作Service +* @createDate 2025-08-21 15:06:10 +*/ +public interface D8Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/D9Service.java b/src/main/java/com/xy/xyaicpzs/service/D9Service.java new file mode 100644 index 0000000..605d862 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/D9Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.D9; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【d9(d9表)】的数据库操作Service +* @createDate 2025-08-21 15:06:10 +*/ +public interface D9Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltBackendHistory100Service.java b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistory100Service.java new file mode 100644 index 0000000..5c0cdac --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistory100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistory100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_100(大乐透后区最近100期数据表)】的数据库操作Service +* @createDate 2025-08-21 11:35:47 +*/ +public interface DltBackendHistory100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryAllService.java b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryAllService.java new file mode 100644 index 0000000..ea14cf4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryAllService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryAll; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_all(大乐透后区全部历史数据表)】的数据库操作Service +* @createDate 2025-08-21 11:35:47 +*/ +public interface DltBackendHistoryAllService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryTop100Service.java b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryTop100Service.java new file mode 100644 index 0000000..21d667c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryTop100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_top_100(大乐透后区百期数据排行表)】的数据库操作Service +* @createDate 2025-08-21 11:35:47 +*/ +public interface DltBackendHistoryTop100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryTopService.java b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryTopService.java new file mode 100644 index 0000000..a556341 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltBackendHistoryTopService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_top(大乐透后区历史数据排行表)】的数据库操作Service +* @createDate 2025-08-21 11:35:47 +*/ +public interface DltBackendHistoryTopService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltCombinationAnalysisService.java b/src/main/java/com/xy/xyaicpzs/service/DltCombinationAnalysisService.java new file mode 100644 index 0000000..ba044d1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltCombinationAnalysisService.java @@ -0,0 +1,41 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.vo.BallCombinationAnalysisVO; + +/** + * 大乐透组合分析服务接口 + */ +public interface DltCombinationAnalysisService { + + /** + * 前区与前区的组合性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(前区) + * @return 组合分析结果 + */ + BallCombinationAnalysisVO analyzeFrontFrontCombination(Integer masterBall, Integer slaveBall); + + /** + * 前区与后区的组合性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(后区) + * @return 组合分析结果 + */ + BallCombinationAnalysisVO analyzeFrontBackCombination(Integer masterBall, Integer slaveBall); + + /** + * 后区与后区的组合性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(后区) + * @return 组合分析结果 + */ + BallCombinationAnalysisVO analyzeBackBackCombination(Integer masterBall, Integer slaveBall); + + /** + * 后区与前区的组合性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(前区) + * @return 组合分析结果 + */ + BallCombinationAnalysisVO analyzeBackFrontCombination(Integer masterBall, Integer slaveBall); +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltDataAnalysisService.java b/src/main/java/com/xy/xyaicpzs/service/DltDataAnalysisService.java new file mode 100644 index 0000000..946242c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltDataAnalysisService.java @@ -0,0 +1,60 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.vo.PrizeEstimateVO; +import com.xy.xyaicpzs.domain.vo.RedBallHitRateVO; +import com.xy.xyaicpzs.domain.vo.UserPredictStatVO; + +/** + * 大乐透数据分析服务接口 + */ +public interface DltDataAnalysisService { + + /** + * 处理大乐透待开奖记录,匹配开奖结果 + * @return 处理的记录数量 + */ + int processPendingDltPredictions(); + + /** + * 获取用户大乐透预测统计数据 + * @param userId 用户ID + * @return 用户大乐透预测统计数据 + */ + UserPredictStatVO getUserDltPredictStat(Long userId); + + /** + * 大乐透奖金统计 + * @param userId 用户ID + * @param predictId 预测记录ID,如果为null则统计所有记录 + * @return 奖金统计信息 + */ + PrizeEstimateVO getDltPrizeStatistics(Long userId, Long predictId); + + /** + * 大乐透前区首球命中率分析 + * @param userId 用户ID + * @return 前区首球命中率统计信息 + */ + RedBallHitRateVO getFrontFirstBallHitRate(Long userId); + + /** + * 大乐透前区球号命中率分析 + * @param userId 用户ID + * @return 前区球号命中率统计信息 + */ + RedBallHitRateVO getFrontBallHitRate(Long userId); + + /** + * 大乐透后区首球命中率分析 + * @param userId 用户ID + * @return 后区首球命中率统计信息 + */ + RedBallHitRateVO getBackFirstBallHitRate(Long userId); + + /** + * 大乐透后区球号命中率分析 + * @param userId 用户ID + * @return 后区球号命中率统计信息 + */ + RedBallHitRateVO getBackBallHitRate(Long userId); +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltDrawRecordService.java b/src/main/java/com/xy/xyaicpzs/service/DltDrawRecordService.java new file mode 100644 index 0000000..8f4377a --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltDrawRecordService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_draw_record(大乐透开奖信息表)】的数据库操作Service +* @createDate 2025-08-20 15:55:06 +*/ +public interface DltDrawRecordService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistory100Service.java b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistory100Service.java new file mode 100644 index 0000000..6e5b8be --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistory100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistory100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_100(大乐透前区最近100期数据表)】的数据库操作Service +* @createDate 2025-08-20 16:24:40 +*/ +public interface DltFrontendHistory100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryAllService.java b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryAllService.java new file mode 100644 index 0000000..920628b --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryAllService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryAll; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_all(大乐透前区全部历史数据表)】的数据库操作Service +* @createDate 2025-08-20 16:24:40 +*/ +public interface DltFrontendHistoryAllService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryTop100Service.java b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryTop100Service.java new file mode 100644 index 0000000..260132c --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryTop100Service.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop100; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_top_100(大乐透前区百期数据排行表)】的数据库操作Service +* @createDate 2025-08-20 16:24:40 +*/ +public interface DltFrontendHistoryTop100Service extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryTopService.java b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryTopService.java new file mode 100644 index 0000000..a16c053 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltFrontendHistoryTopService.java @@ -0,0 +1,13 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop; +import com.baomidou.mybatisplus.extension.service.IService; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_top(大乐透前区历史数据排行表)】的数据库操作Service +* @createDate 2025-08-20 16:24:40 +*/ +public interface DltFrontendHistoryTopService extends IService { + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltPersistenceAnalysisService.java b/src/main/java/com/xy/xyaicpzs/service/DltPersistenceAnalysisService.java new file mode 100644 index 0000000..827458f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltPersistenceAnalysisService.java @@ -0,0 +1,41 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.vo.BallPersistenceAnalysisVO; + +/** + * 大乐透持续性分析服务接口 + */ +public interface DltPersistenceAnalysisService { + + /** + * 前区与前区的持续性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(前区) + * @return 持续性分析结果 + */ + BallPersistenceAnalysisVO analyzeFrontFrontPersistence(Integer masterBall, Integer slaveBall); + + /** + * 后区与后区的持续性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(后区) + * @return 持续性分析结果 + */ + BallPersistenceAnalysisVO analyzeBackBackPersistence(Integer masterBall, Integer slaveBall); + + /** + * 前区与后区的持续性分析 + * @param masterBall 主球号码(前区) + * @param slaveBall 随球号码(后区) + * @return 持续性分析结果 + */ + BallPersistenceAnalysisVO analyzeFrontBackPersistence(Integer masterBall, Integer slaveBall); + + /** + * 后区与前区的持续性分析 + * @param masterBall 主球号码(后区) + * @param slaveBall 随球号码(前区) + * @return 持续性分析结果 + */ + BallPersistenceAnalysisVO analyzeBackFrontPersistence(Integer masterBall, Integer slaveBall); +} diff --git a/src/main/java/com/xy/xyaicpzs/service/DltPredictRecordService.java b/src/main/java/com/xy/xyaicpzs/service/DltPredictRecordService.java new file mode 100644 index 0000000..be86a37 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/DltPredictRecordService.java @@ -0,0 +1,43 @@ +package com.xy.xyaicpzs.service; + +import com.xy.xyaicpzs.domain.entity.DltPredictRecord; +import com.baomidou.mybatisplus.extension.service.IService; + +import java.util.Date; +import java.util.List; + +/** +* @author XY003 +* @description 针对表【dlt_predict_record(大乐透推测记录表)】的数据库操作Service +* @createDate 2025-09-08 14:01:12 +*/ +public interface DltPredictRecordService extends IService { + + /** + * 创建大乐透预测记录 + * @param userId 用户ID + * @param drawId 开奖期号 + * @param drawDate 开奖日期 + * @param frontBalls 5个前区球号码 + * @param backBalls 2个后区球号码 + * @return 创建的预测记录 + */ + DltPredictRecord createDltPredictRecord(Long userId, Long drawId, Date drawDate, List frontBalls, List backBalls); + + /** + * 根据用户ID分页获取大乐透预测记录 + * @param userId 用户ID + * @param page 页码,从1开始 + * @param size 每页大小 + * @return 用户的大乐透预测记录列表,按预测时间倒序排列 + */ + List getDltPredictRecordsByUserIdWithPaging(Long userId, Integer page, Integer size); + + /** + * 根据用户ID获取大乐透预测记录总数 + * @param userId 用户ID + * @return 用户的大乐透预测记录总数 + */ + Long getDltPredictRecordsCountByUserId(Long userId); + +} diff --git a/src/main/java/com/xy/xyaicpzs/service/UserService.java b/src/main/java/com/xy/xyaicpzs/service/UserService.java index 798828e..2ca298f 100644 --- a/src/main/java/com/xy/xyaicpzs/service/UserService.java +++ b/src/main/java/com/xy/xyaicpzs/service/UserService.java @@ -1,9 +1,11 @@ package com.xy.xyaicpzs.service; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.xy.xyaicpzs.domain.dto.user.UserPhoneLoginRequest; import com.xy.xyaicpzs.domain.dto.user.UserPhoneRegisterRequest; import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.domain.vo.*; import jakarta.servlet.http.HttpServletRequest; @@ -96,4 +98,53 @@ public interface UserService extends IService { * @return 加密后的密码 */ String encryptPassword(String password); + + // region 统计相关方法 + + /** + * 根据时间段获取新增用户统计 + * + * @param startDate 开始日期 (格式: yyyy-MM-dd) + * @param endDate 结束日期 (格式: yyyy-MM-dd) + * @return 新增用户统计数据 + */ + UserStatisticsVO getNewUsersStatistics(String startDate, String endDate); + + /** + * 根据时间段获取新增会员统计 + * + * @param startDate 开始日期 (格式: yyyy-MM-dd) + * @param endDate 结束日期 (格式: yyyy-MM-dd) + * @return 新增会员统计数据 + */ + VipStatisticsVO getNewVipsStatistics(String startDate, String endDate); + + /** + * 根据时间段获取用户注册趋势 + * + * @param startDate 开始日期 (格式: yyyy-MM-dd) + * @param endDate 结束日期 (格式: yyyy-MM-dd) + * @param granularity 时间粒度 (day/week/month) + * @return 用户注册趋势数据 + */ + RegistrationTrendVO getRegistrationTrend(String startDate, String endDate, String granularity); + + /** + * 获取即将到期的会员列表 + * + * @param days 提前多少天提醒 (默认7天) + * @param current 当前页 + * @param pageSize 页大小 + * @return 即将到期的会员列表 + */ + Page getExpiringVips(Integer days, Long current, Long pageSize); + + /** + * 获取会员状态分布统计 + * + * @return 会员状态分布数据 + */ + VipDistributionVO getVipDistribution(); + + // endregion } diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D10ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D10ServiceImpl.java new file mode 100644 index 0000000..912b8f3 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D10ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D10; +import com.xy.xyaicpzs.service.D10Service; +import com.xy.xyaicpzs.mapper.D10Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d10(d10表)】的数据库操作Service实现 +* @createDate 2025-08-21 15:06:10 +*/ +@Service +public class D10ServiceImpl extends ServiceImpl + implements D10Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D11ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D11ServiceImpl.java new file mode 100644 index 0000000..dffd7bb --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D11ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D11; +import com.xy.xyaicpzs.service.D11Service; +import com.xy.xyaicpzs.mapper.D11Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d11(d11表)】的数据库操作Service实现 +* @createDate 2025-08-21 15:06:10 +*/ +@Service +public class D11ServiceImpl extends ServiceImpl + implements D11Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D12ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D12ServiceImpl.java new file mode 100644 index 0000000..9366ec9 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D12ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D12; +import com.xy.xyaicpzs.service.D12Service; +import com.xy.xyaicpzs.mapper.D12Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d12(d12表)】的数据库操作Service实现 +* @createDate 2025-08-21 15:06:10 +*/ +@Service +public class D12ServiceImpl extends ServiceImpl + implements D12Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D5ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D5ServiceImpl.java new file mode 100644 index 0000000..4a6142b --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D5ServiceImpl.java @@ -0,0 +1,17 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D5; +import com.xy.xyaicpzs.service.D5Service; +import com.xy.xyaicpzs.mapper.D5Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d5(d5表)】的数据库操作Service实现 +* @createDate 2025-01-26 16:00:00 +*/ +@Service +public class D5ServiceImpl extends ServiceImpl implements D5Service { + +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D6ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D6ServiceImpl.java new file mode 100644 index 0000000..635ab93 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D6ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D6; +import com.xy.xyaicpzs.service.D6Service; +import com.xy.xyaicpzs.mapper.D6Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d6(d6表)】的数据库操作Service实现 +* @createDate 2025-08-21 14:10:22 +*/ +@Service +public class D6ServiceImpl extends ServiceImpl + implements D6Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D7ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D7ServiceImpl.java new file mode 100644 index 0000000..850d6a1 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D7ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D7; +import com.xy.xyaicpzs.service.D7Service; +import com.xy.xyaicpzs.mapper.D7Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d7(d7表)】的数据库操作Service实现 +* @createDate 2025-08-21 15:06:10 +*/ +@Service +public class D7ServiceImpl extends ServiceImpl + implements D7Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D8ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D8ServiceImpl.java new file mode 100644 index 0000000..04b27c2 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D8ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D8; +import com.xy.xyaicpzs.service.D8Service; +import com.xy.xyaicpzs.mapper.D8Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d8(d8表)】的数据库操作Service实现 +* @createDate 2025-08-21 15:06:10 +*/ +@Service +public class D8ServiceImpl extends ServiceImpl + implements D8Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/D9ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/D9ServiceImpl.java new file mode 100644 index 0000000..14f2f33 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/D9ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.D9; +import com.xy.xyaicpzs.service.D9Service; +import com.xy.xyaicpzs.mapper.D9Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【d9(d9表)】的数据库操作Service实现 +* @createDate 2025-08-21 15:06:10 +*/ +@Service +public class D9ServiceImpl extends ServiceImpl + implements D9Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java index 4ce210a..050c597 100644 --- a/src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DataAnalysisServiceImpl.java @@ -34,7 +34,8 @@ public class DataAnalysisServiceImpl implements DataAnalysisService { // 查询用户的所有预测记录 QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("userId", userId); + queryWrapper.eq("userId", userId) + .eq("type", "ssq"); List predictRecords = predictRecordMapper.selectList(queryWrapper); if (predictRecords == null || predictRecords.isEmpty()) { @@ -83,7 +84,8 @@ public class DataAnalysisServiceImpl implements DataAnalysisService { public int processPendingPredictions() { // 查询所有待开奖的预测记录 QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("predictStatus", "待开奖"); + queryWrapper.eq("predictStatus", "待开奖") + .eq("type", "ssq"); List pendingRecords = predictRecordMapper.selectList(queryWrapper); int processedCount = 0; @@ -129,6 +131,9 @@ public class DataAnalysisServiceImpl implements DataAnalysisService { queryWrapper.eq("predictStatus", request.getPredictStatus()); } + // 添加类型筛选条件,只查询双色球类型 + queryWrapper.eq("type", "ssq"); + // 按预测时间降序排序 queryWrapper.orderByDesc("predictTime"); @@ -147,8 +152,10 @@ public class DataAnalysisServiceImpl implements DataAnalysisService { @Override public long getTotalPredictCount() { - // 获取全部预测记录数量 - return predictRecordMapper.selectCount(null); + // 获取全部预测记录数量(只统计双色球类型) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("type", "ssq"); + return predictRecordMapper.selectCount(queryWrapper); } /** diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistory100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistory100ServiceImpl.java new file mode 100644 index 0000000..93bcdfc --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistory100ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltBackendHistory100; +import com.xy.xyaicpzs.service.DltBackendHistory100Service; +import com.xy.xyaicpzs.mapper.DltBackendHistory100Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_100(大乐透后区最近100期数据表)】的数据库操作Service实现 +* @createDate 2025-08-21 11:35:47 +*/ +@Service +public class DltBackendHistory100ServiceImpl extends ServiceImpl + implements DltBackendHistory100Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryAllServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryAllServiceImpl.java new file mode 100644 index 0000000..1fcaa20 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryAllServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryAll; +import com.xy.xyaicpzs.service.DltBackendHistoryAllService; +import com.xy.xyaicpzs.mapper.DltBackendHistoryAllMapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_all(大乐透后区全部历史数据表)】的数据库操作Service实现 +* @createDate 2025-08-21 11:35:47 +*/ +@Service +public class DltBackendHistoryAllServiceImpl extends ServiceImpl + implements DltBackendHistoryAllService{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryTop100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryTop100ServiceImpl.java new file mode 100644 index 0000000..5bb3b97 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryTop100ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop100; +import com.xy.xyaicpzs.service.DltBackendHistoryTop100Service; +import com.xy.xyaicpzs.mapper.DltBackendHistoryTop100Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_top_100(大乐透后区百期数据排行表)】的数据库操作Service实现 +* @createDate 2025-08-21 11:35:47 +*/ +@Service +public class DltBackendHistoryTop100ServiceImpl extends ServiceImpl + implements DltBackendHistoryTop100Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryTopServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryTopServiceImpl.java new file mode 100644 index 0000000..d388bc0 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltBackendHistoryTopServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop; +import com.xy.xyaicpzs.service.DltBackendHistoryTopService; +import com.xy.xyaicpzs.mapper.DltBackendHistoryTopMapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_backend_history_top(大乐透后区历史数据排行表)】的数据库操作Service实现 +* @createDate 2025-08-21 11:35:47 +*/ +@Service +public class DltBackendHistoryTopServiceImpl extends ServiceImpl + implements DltBackendHistoryTopService{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltCombinationAnalysisServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltCombinationAnalysisServiceImpl.java new file mode 100644 index 0000000..d36da85 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltCombinationAnalysisServiceImpl.java @@ -0,0 +1,251 @@ +package com.xy.xyaicpzs.service.impl; + +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.xy.xyaicpzs.domain.entity.D5; +import com.xy.xyaicpzs.domain.entity.D6; +import com.xy.xyaicpzs.domain.entity.D7; +import com.xy.xyaicpzs.domain.entity.D8; +import com.xy.xyaicpzs.domain.vo.BallCombinationAnalysisVO; +import com.xy.xyaicpzs.service.DltCombinationAnalysisService; +import com.xy.xyaicpzs.service.DltDrawRecordService; +import com.xy.xyaicpzs.service.D5Service; +import com.xy.xyaicpzs.service.D6Service; +import com.xy.xyaicpzs.service.D7Service; +import com.xy.xyaicpzs.service.D8Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + +/** + * 大乐透组合分析服务实现类 + */ +@Slf4j +@Service +public class DltCombinationAnalysisServiceImpl implements DltCombinationAnalysisService { + + @Autowired + private D5Service d5Service; + + @Autowired + private D6Service d6Service; + + @Autowired + private D7Service d7Service; + + @Autowired + private D8Service d8Service; + + @Autowired + private DltDrawRecordService dltDrawRecordService; + + @Override + public BallCombinationAnalysisVO analyzeFrontFrontCombination(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 35, "主球(前区)"); + validateBallRange(slaveBall, 1, 35, "随球(前区)"); + + // 查询指定组合记录 + D5 targetRecord = d5Service.lambdaQuery() + .eq(D5::getMasterBallNumber, masterBall) + .eq(D5::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(前区)%d和随球(前区)%d的组合记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d5Service.lambdaQuery() + .eq(D5::getMasterBallNumber, masterBall) + .ne(D5::getSlaveBallNumber, masterBall) // 排除自身 + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(前区)%d的组合记录", masterBall)); + } + + return buildCombinationAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D5::getCoefficient, + D5::getSlaveBallNumber + ); + } + + @Override + public BallCombinationAnalysisVO analyzeFrontBackCombination(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 35, "主球(前区)"); + validateBallRange(slaveBall, 1, 12, "随球(后区)"); + + // 查询指定组合记录 + D6 targetRecord = d6Service.lambdaQuery() + .eq(D6::getMasterBallNumber, masterBall) + .eq(D6::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(前区)%d和随球(后区)%d的组合记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d6Service.lambdaQuery() + .eq(D6::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(前区)%d的组合记录", masterBall)); + } + + return buildCombinationAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D6::getCoefficient, + D6::getSlaveBallNumber + ); + } + + @Override + public BallCombinationAnalysisVO analyzeBackBackCombination(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 12, "主球(后区)"); + validateBallRange(slaveBall, 1, 12, "随球(后区)"); + + // 查询指定组合记录 + D7 targetRecord = d7Service.lambdaQuery() + .eq(D7::getMasterBallNumber, masterBall) + .eq(D7::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(后区)%d和随球(后区)%d的组合记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d7Service.lambdaQuery() + .eq(D7::getMasterBallNumber, masterBall) + .ne(D7::getSlaveBallNumber, masterBall) // 排除自身 + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(后区)%d的组合记录", masterBall)); + } + + return buildCombinationAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D7::getCoefficient, + D7::getSlaveBallNumber + ); + } + + @Override + public BallCombinationAnalysisVO analyzeBackFrontCombination(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 12, "主球(后区)"); + validateBallRange(slaveBall, 1, 35, "随球(前区)"); + + // 查询指定组合记录 + D8 targetRecord = d8Service.lambdaQuery() + .eq(D8::getMasterBallNumber, masterBall) + .eq(D8::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(后区)%d和随球(前区)%d的组合记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d8Service.lambdaQuery() + .eq(D8::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(后区)%d的组合记录", masterBall)); + } + + return buildCombinationAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D8::getCoefficient, + D8::getSlaveBallNumber + ); + } + + /** + * 验证球号范围 + */ + private void validateBallRange(Integer ballNumber, int minValue, int maxValue, String ballType) { + if (ballNumber == null) { + throw new IllegalArgumentException(ballType + "号码不能为空"); + } + if (ballNumber < minValue || ballNumber > maxValue) { + throw new IllegalArgumentException(String.format("%s号码必须在%d-%d范围内,错误值:%d", + ballType, minValue, maxValue, ballNumber)); + } + } + + /** + * 构建组合分析结果VO + */ + private BallCombinationAnalysisVO buildCombinationAnalysisVO( + Double targetCoefficient, + List allCombinations, + Function coefficientGetter, + Function ballNumberGetter) { + + // 找出系数最高的球号 + T highest = allCombinations.stream() + .max(Comparator.comparing(coefficientGetter)) + .orElse(null); + + // 找出系数最低的球号 + T lowest = allCombinations.stream() + .min(Comparator.comparing(coefficientGetter)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(coefficientGetter::apply) + .average() + .orElse(0.0); + + // 获取最新开奖期号 + Long latestDrawId = getLatestDrawId(); + + return BallCombinationAnalysisVO.builder() + .faceCoefficient(targetCoefficient) + .highestBall(highest != null ? ballNumberGetter.apply(highest) : null) + .highestCoefficient(highest != null ? coefficientGetter.apply(highest) : null) + .lowestBall(lowest != null ? ballNumberGetter.apply(lowest) : null) + .lowestCoefficient(lowest != null ? coefficientGetter.apply(lowest) : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + } + + /** + * 获取最新开奖期号 + */ + private Long getLatestDrawId() { + String latestDrawId = dltDrawRecordService.lambdaQuery() + .orderByDesc(DltDrawRecord::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(DltDrawRecord::getDrawId) + .orElse(null); + + if (latestDrawId != null) { + try { + return Long.parseLong(latestDrawId); + } catch (NumberFormatException e) { + log.warn("开奖期号格式转换失败:{}", latestDrawId); + } + } + return null; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltDataAnalysisServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltDataAnalysisServiceImpl.java new file mode 100644 index 0000000..cc2b433 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltDataAnalysisServiceImpl.java @@ -0,0 +1,485 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.xy.xyaicpzs.domain.entity.DltPredictRecord; +import com.xy.xyaicpzs.mapper.DltDrawRecordMapper; +import com.xy.xyaicpzs.mapper.DltPredictRecordMapper; +import com.xy.xyaicpzs.domain.vo.PrizeEstimateVO; +import com.xy.xyaicpzs.domain.vo.RedBallHitRateVO; +import com.xy.xyaicpzs.domain.vo.UserPredictStatVO; +import com.xy.xyaicpzs.service.DltDataAnalysisService; +import com.xy.xyaicpzs.service.DltDrawRecordService; +import com.xy.xyaicpzs.util.DltPrizeCalculator; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +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; + +/** + * 大乐透数据分析服务实现类 + */ +@Slf4j +@Service +public class DltDataAnalysisServiceImpl implements DltDataAnalysisService { + + @Autowired + private DltPredictRecordMapper dltPredictRecordMapper; + + @Autowired + private DltDrawRecordMapper dltDrawRecordMapper; + + @Autowired + private DltDrawRecordService dltDrawRecordService; + + @Override + public int processPendingDltPredictions() { + log.info("开始处理大乐透待开奖预测记录"); + + // 查询所有待开奖的大乐透预测记录 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("predictStatus", "待开奖"); + List pendingRecords = dltPredictRecordMapper.selectList(queryWrapper); + + log.info("找到{}条待开奖的大乐透预测记录", pendingRecords.size()); + + int processedCount = 0; + + for (DltPredictRecord record : pendingRecords) { + try { + // 查询对应期号的开奖结果 + // 注意:DltDrawRecord的drawId是String类型,DltPredictRecord的drawId是Long类型 + QueryWrapper drawQueryWrapper = new QueryWrapper<>(); + drawQueryWrapper.eq("drawId", String.valueOf(record.getDrawId())); + DltDrawRecord drawRecord = dltDrawRecordMapper.selectOne(drawQueryWrapper); + + if (drawRecord != null) { + log.debug("处理预测记录ID:{},期号:{}", record.getId(), record.getDrawId()); + + // 构建预测号码数组 [前区5个号码, 后区2个号码] + Integer[] predictNumbers = { + record.getFrontendBall1(), + record.getFrontendBall2(), + record.getFrontendBall3(), + record.getFrontendBall4(), + record.getFrontendBall5(), + record.getBackendBall1(), + record.getBackendBall2() + }; + + // 构建开奖号码数组 [前区5个号码, 后区2个号码] + Integer[] drawNumbers = { + drawRecord.getFrontBall1(), + drawRecord.getFrontBall2(), + drawRecord.getFrontBall3(), + drawRecord.getFrontBall4(), + drawRecord.getFrontBall5(), + drawRecord.getBackBall1(), + drawRecord.getBackBall2() + }; + + // 使用DltPrizeCalculator计算中奖结果 + DltPrizeCalculator.PrizeResult prizeResult = DltPrizeCalculator.calculatePrize(predictNumbers, drawNumbers); + + // 更新预测记录 + // 根据中奖结果设置状态 + if(prizeResult.getPrizeLevel().equals("未中奖")){ + record.setPredictStatus("未中奖"); + }else{ + record.setPredictStatus("已中奖"); + } + record.setPredictResult(prizeResult.getPrizeLevel()); + record.setBonus(prizeResult.getBonus()); + + dltPredictRecordMapper.updateById(record); + + log.debug("预测记录ID:{} 处理完成,中奖等级:{},奖金:{}", + record.getId(), prizeResult.getPrizeLevel(), prizeResult.getBonus()); + + processedCount++; + } else { + log.debug("未找到期号{}的开奖记录", record.getDrawId()); + } + } catch (Exception e) { + log.error("处理预测记录ID:{} 时发生错误:{}", record.getId(), e.getMessage(), e); + } + } + + log.info("大乐透待开奖预测记录处理完成,共处理{}条记录", processedCount); + return processedCount; + } + + @Override + public UserPredictStatVO getUserDltPredictStat(Long userId) { + log.info("开始获取用户{}的大乐透预测统计数据", userId); + + UserPredictStatVO statVO = new UserPredictStatVO(); + + // 查询用户的所有大乐透预测记录 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId); + List predictRecords = dltPredictRecordMapper.selectList(queryWrapper); + + if (predictRecords == null || predictRecords.isEmpty()) { + log.info("用户{}没有大乐透预测记录", userId); + statVO.setUserId(userId); + statVO.setPredictCount(0L); + statVO.setHitCount(0L); + statVO.setPendingCount(0L); + statVO.setDrawnCount(0L); + statVO.setHitRate(BigDecimal.ZERO); + return statVO; + } + + // 统计数据 + long totalPredicts = predictRecords.size(); + long hitCount = 0L; + long pendingCount = 0L; + long drawnCount = 0L; + + for (DltPredictRecord record : predictRecords) { + if ("待开奖".equals(record.getPredictStatus())) { + pendingCount++; + } else if ("已开奖".equals(record.getPredictStatus())) { + drawnCount++; + // 检查是否中奖(除了"未中奖"都算中奖) + if (!"未中奖".equals(record.getPredictResult()) && + record.getPredictResult() != null && + !record.getPredictResult().trim().isEmpty()) { + hitCount++; + } + } + } + + // 计算命中率 + BigDecimal hitRate = drawnCount > 0 ? + BigDecimal.valueOf(hitCount).divide(BigDecimal.valueOf(drawnCount), 4, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + statVO.setUserId(userId); + statVO.setPredictCount(totalPredicts); + statVO.setHitCount(hitCount); + statVO.setPendingCount(pendingCount); + statVO.setDrawnCount(drawnCount); + statVO.setHitRate(hitRate); + + log.info("用户{}大乐透预测统计数据:预测次数={},待开奖次数={},已开奖次数={},命中次数={},命中率={}", + userId, totalPredicts, pendingCount, drawnCount, hitCount, hitRate); + + return statVO; + } + + @Override + public PrizeEstimateVO getDltPrizeStatistics(Long userId, Long predictId) { + log.info("开始为用户{}进行大乐透奖金统计,predictId={}", userId, predictId); + + // 查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId); + // 排除待开奖状态 + queryWrapper.eq("predictStatus", "已开奖"); + // 如果指定了预测记录ID,则只查询该记录 + if (predictId != null) { + queryWrapper.eq("id", predictId); + } + + // 查询用户的大乐透预测记录 + List records = dltPredictRecordMapper.selectList(queryWrapper); + + // 按中奖等级分组统计 + Map countByPrizeLevel = new HashMap<>(); + Map totalBonusByPrizeLevel = new HashMap<>(); + + // 初始化大乐透的所有等级 + String[] prizeLevels = {"一等奖", "二等奖", "三等奖", "四等奖", "五等奖", "六等奖", "七等奖", "八等奖", "九等奖", "未中奖"}; + for (String level : prizeLevels) { + countByPrizeLevel.put(level, 0); + totalBonusByPrizeLevel.put(level, BigDecimal.ZERO); + } + + // 统计各等级数量和奖金 + for (DltPredictRecord record : records) { + String prizeLevel = record.getPredictResult(); + if (prizeLevel == null || prizeLevel.trim().isEmpty()) { + prizeLevel = "未中奖"; + } + + // 更新计数 + countByPrizeLevel.put(prizeLevel, countByPrizeLevel.getOrDefault(prizeLevel, 0) + 1); + + // 累计奖金 + if (record.getBonus() != null) { + BigDecimal bonus = new BigDecimal(record.getBonus().toString()); + totalBonusByPrizeLevel.put(prizeLevel, + totalBonusByPrizeLevel.getOrDefault(prizeLevel, BigDecimal.ZERO).add(bonus)); + } + } + + // 构建返回结果 + List prizeDetails = new ArrayList<>(); + + // 按顺序添加各等级的统计结果(从高到低) + for (String level : prizeLevels) { + // 跳过没有记录的等级 + if (countByPrizeLevel.get(level) <= 0) { + continue; + } + + int count = countByPrizeLevel.get(level); + BigDecimal totalBonus = totalBonusByPrizeLevel.get(level); + BigDecimal singlePrize = BigDecimal.ZERO; + if (count > 0 && totalBonus.compareTo(BigDecimal.ZERO) > 0) { + singlePrize = totalBonus.divide(new BigDecimal(count), 2, RoundingMode.HALF_UP); + } + + prizeDetails.add(PrizeEstimateVO.PrizeDetailItem.builder() + .prizeLevel(level) + .winningCount(count) + .singlePrize(singlePrize) + .subtotal(totalBonus) + .build()); + } + + // 计算总奖金 + BigDecimal totalPrize = prizeDetails.stream() + .map(PrizeEstimateVO.PrizeDetailItem::getSubtotal) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + PrizeEstimateVO result = PrizeEstimateVO.builder() + .totalPrize(totalPrize) + .prizeDetails(prizeDetails) + .build(); + + log.info("用户{}的大乐透奖金统计结果:总奖金{},各等级明细数量:{}", + userId, totalPrize, prizeDetails.size()); + return result; + } + + @Override + public RedBallHitRateVO getFrontFirstBallHitRate(Long userId) { + log.info("开始统计用户{}的大乐透前区首球命中率", userId); + + // 查询用户的所有预测记录(除了"待开奖"状态的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .eq("predictStatus", "已开奖"); + + List predictRecords = dltPredictRecordMapper.selectList(queryWrapper); + + // 总预测次数 + int totalCount = predictRecords.size(); + + // 统计前区首球命中次数 + int hitCount = 0; + + for (DltPredictRecord record : predictRecords) { + // 获取该预测记录对应的开奖信息 + DltDrawRecord draw = dltDrawRecordService.lambdaQuery() + .eq(DltDrawRecord::getDrawId, String.valueOf(record.getDrawId())) + .one(); + + if (draw != null && record.getFrontendBall1() != null) { + Integer predictedFirstBall = record.getFrontendBall1(); + List drawnFrontBalls = Arrays.asList( + draw.getFrontBall1(), + draw.getFrontBall2(), + draw.getFrontBall3(), + draw.getFrontBall4(), + draw.getFrontBall5() + ); + // 比较预测的第一个前区球是否在开奖的五个前区球中 + if (drawnFrontBalls.contains(predictedFirstBall)) { + hitCount++; + } + } + } + + // 计算命中率 + double hitRate = totalCount > 0 ? (double) hitCount / totalCount * 100 : 0; + + RedBallHitRateVO result = RedBallHitRateVO.builder() + .totalHitCount(hitCount) + .totalPredictedCount(totalCount) + .hitRate(hitRate) + .build(); + + log.info("用户{}的大乐透前区首球命中率统计结果:命中{}次,总计{}次,命中率{}%", + userId, hitCount, totalCount, String.format("%.2f", hitRate)); + + return result; + } + + @Override + public RedBallHitRateVO getFrontBallHitRate(Long userId) { + log.info("开始统计用户{}的大乐透前区球号命中率", userId); + + // 查询用户的所有预测记录(除了"待开奖"状态的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .eq("predictStatus", "已开奖"); + + List predictRecords = dltPredictRecordMapper.selectList(queryWrapper); + + int totalHitCount = 0; + int totalRecords = predictRecords.size(); + + for (DltPredictRecord record : predictRecords) { + // 获取该预测记录对应的开奖信息 + DltDrawRecord draw = dltDrawRecordService.lambdaQuery() + .eq(DltDrawRecord::getDrawId, String.valueOf(record.getDrawId())) + .one(); + + if (draw != null) { + List predictedFrontBalls = Arrays.asList( + record.getFrontendBall1(), + record.getFrontendBall2(), + record.getFrontendBall3(), + record.getFrontendBall4(), + record.getFrontendBall5() + ); + + List drawnFrontBalls = Arrays.asList( + draw.getFrontBall1(), + draw.getFrontBall2(), + draw.getFrontBall3(), + draw.getFrontBall4(), + draw.getFrontBall5() + ); + + // 计算当前记录命中的前区球数 + for (Integer predictedBall : predictedFrontBalls) { + if (predictedBall != null && drawnFrontBalls.contains(predictedBall)) { + totalHitCount++; + } + } + } + } + + int totalPredictedCount = totalRecords * 5; // 每条记录预测5个前区球 + double hitRate = (totalPredictedCount > 0) ? ((double) totalHitCount / totalPredictedCount) * 100 : 0; + + RedBallHitRateVO result = RedBallHitRateVO.builder() + .totalHitCount(totalHitCount) + .totalPredictedCount(totalPredictedCount) + .hitRate(hitRate) + .build(); + + log.info("用户{}的大乐透前区球号命中率统计结果:命中总数{},预测总数{},命中率{}%", + userId, totalHitCount, totalPredictedCount, String.format("%.2f", hitRate)); + + return result; + } + + @Override + public RedBallHitRateVO getBackFirstBallHitRate(Long userId) { + log.info("开始统计用户{}的大乐透后区首球命中率", userId); + + // 查询用户的所有预测记录(除了"待开奖"状态的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .eq("predictStatus", "已开奖"); + + List predictRecords = dltPredictRecordMapper.selectList(queryWrapper); + + // 总预测次数 + int totalCount = predictRecords.size(); + + // 统计后区首球命中次数 + int hitCount = 0; + + for (DltPredictRecord record : predictRecords) { + // 获取该预测记录对应的开奖信息 + DltDrawRecord draw = dltDrawRecordService.lambdaQuery() + .eq(DltDrawRecord::getDrawId, String.valueOf(record.getDrawId())) + .one(); + + if (draw != null && record.getBackendBall1() != null) { + Integer predictedFirstBackBall = record.getBackendBall1(); + List drawnBackBalls = Arrays.asList( + draw.getBackBall1(), + draw.getBackBall2() + ); + // 比较预测的第一个后区球是否在开奖的两个后区球中 + if (drawnBackBalls.contains(predictedFirstBackBall)) { + hitCount++; + } + } + } + + // 计算命中率 + double hitRate = totalCount > 0 ? (double) hitCount / totalCount * 100 : 0; + + RedBallHitRateVO result = RedBallHitRateVO.builder() + .totalHitCount(hitCount) + .totalPredictedCount(totalCount) + .hitRate(hitRate) + .build(); + + log.info("用户{}的大乐透后区首球命中率统计结果:命中{}次,总计{}次,命中率{}%", + userId, hitCount, totalCount, String.format("%.2f", hitRate)); + + return result; + } + + @Override + public RedBallHitRateVO getBackBallHitRate(Long userId) { + log.info("开始统计用户{}的大乐透后区球号命中率", userId); + + // 查询用户的所有预测记录(除了"待开奖"状态的) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId) + .eq("predictStatus", "已开奖"); + + List predictRecords = dltPredictRecordMapper.selectList(queryWrapper); + + int totalHitCount = 0; + int totalRecords = predictRecords.size(); + + for (DltPredictRecord record : predictRecords) { + // 获取该预测记录对应的开奖信息 + DltDrawRecord draw = dltDrawRecordService.lambdaQuery() + .eq(DltDrawRecord::getDrawId, String.valueOf(record.getDrawId())) + .one(); + + if (draw != null) { + List predictedBackBalls = Arrays.asList( + record.getBackendBall1(), + record.getBackendBall2() + ); + + List drawnBackBalls = Arrays.asList( + draw.getBackBall1(), + draw.getBackBall2() + ); + + // 计算当前记录命中的后区球数 + for (Integer predictedBall : predictedBackBalls) { + if (predictedBall != null && drawnBackBalls.contains(predictedBall)) { + totalHitCount++; + } + } + } + } + + int totalPredictedCount = totalRecords * 2; // 每条记录预测2个后区球 + double hitRate = (totalPredictedCount > 0) ? ((double) totalHitCount / totalPredictedCount) * 100 : 0; + + RedBallHitRateVO result = RedBallHitRateVO.builder() + .totalHitCount(totalHitCount) + .totalPredictedCount(totalPredictedCount) + .hitRate(hitRate) + .build(); + + log.info("用户{}的大乐透后区球号命中率统计结果:命中总数{},预测总数{},命中率{}%", + userId, totalHitCount, totalPredictedCount, String.format("%.2f", hitRate)); + + return result; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltDrawRecordServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltDrawRecordServiceImpl.java new file mode 100644 index 0000000..6408f41 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltDrawRecordServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.xy.xyaicpzs.service.DltDrawRecordService; +import com.xy.xyaicpzs.mapper.DltDrawRecordMapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_draw_record(大乐透开奖信息表)】的数据库操作Service实现 +* @createDate 2025-08-20 15:55:06 +*/ +@Service +public class DltDrawRecordServiceImpl extends ServiceImpl + implements DltDrawRecordService{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistory100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistory100ServiceImpl.java new file mode 100644 index 0000000..b235bb8 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistory100ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistory100; +import com.xy.xyaicpzs.service.DltFrontendHistory100Service; +import com.xy.xyaicpzs.mapper.DltFrontendHistory100Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_100(大乐透前区最近100期数据表)】的数据库操作Service实现 +* @createDate 2025-08-20 16:24:40 +*/ +@Service +public class DltFrontendHistory100ServiceImpl extends ServiceImpl + implements DltFrontendHistory100Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryAllServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryAllServiceImpl.java new file mode 100644 index 0000000..03ed6b9 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryAllServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryAll; +import com.xy.xyaicpzs.service.DltFrontendHistoryAllService; +import com.xy.xyaicpzs.mapper.DltFrontendHistoryAllMapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_all(大乐透前区全部历史数据表)】的数据库操作Service实现 +* @createDate 2025-08-20 16:24:40 +*/ +@Service +public class DltFrontendHistoryAllServiceImpl extends ServiceImpl + implements DltFrontendHistoryAllService{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryTop100ServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryTop100ServiceImpl.java new file mode 100644 index 0000000..6c761f4 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryTop100ServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop100; +import com.xy.xyaicpzs.service.DltFrontendHistoryTop100Service; +import com.xy.xyaicpzs.mapper.DltFrontendHistoryTop100Mapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_top_100(大乐透前区百期数据排行表)】的数据库操作Service实现 +* @createDate 2025-08-20 16:24:40 +*/ +@Service +public class DltFrontendHistoryTop100ServiceImpl extends ServiceImpl + implements DltFrontendHistoryTop100Service{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryTopServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryTopServiceImpl.java new file mode 100644 index 0000000..da78bee --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltFrontendHistoryTopServiceImpl.java @@ -0,0 +1,22 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop; +import com.xy.xyaicpzs.service.DltFrontendHistoryTopService; +import com.xy.xyaicpzs.mapper.DltFrontendHistoryTopMapper; +import org.springframework.stereotype.Service; + +/** +* @author XY003 +* @description 针对表【dlt_frontend_history_top(大乐透前区历史数据排行表)】的数据库操作Service实现 +* @createDate 2025-08-20 16:24:40 +*/ +@Service +public class DltFrontendHistoryTopServiceImpl extends ServiceImpl + implements DltFrontendHistoryTopService{ + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltPersistenceAnalysisServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltPersistenceAnalysisServiceImpl.java new file mode 100644 index 0000000..8540c73 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltPersistenceAnalysisServiceImpl.java @@ -0,0 +1,249 @@ +package com.xy.xyaicpzs.service.impl; + +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.xy.xyaicpzs.domain.entity.D9; +import com.xy.xyaicpzs.domain.entity.D10; +import com.xy.xyaicpzs.domain.entity.D11; +import com.xy.xyaicpzs.domain.entity.D12; +import com.xy.xyaicpzs.domain.vo.BallPersistenceAnalysisVO; +import com.xy.xyaicpzs.service.DltPersistenceAnalysisService; +import com.xy.xyaicpzs.service.DltDrawRecordService; +import com.xy.xyaicpzs.service.D9Service; +import com.xy.xyaicpzs.service.D10Service; +import com.xy.xyaicpzs.service.D11Service; +import com.xy.xyaicpzs.service.D12Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; + +/** + * 大乐透持续性分析服务实现类 + */ +@Slf4j +@Service +public class DltPersistenceAnalysisServiceImpl implements DltPersistenceAnalysisService { + + @Autowired + private D9Service d9Service; + + @Autowired + private D10Service d10Service; + + @Autowired + private D11Service d11Service; + + @Autowired + private D12Service d12Service; + + @Autowired + private DltDrawRecordService dltDrawRecordService; + + @Override + public BallPersistenceAnalysisVO analyzeFrontFrontPersistence(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 35, "主球(前区)"); + validateBallRange(slaveBall, 1, 35, "随球(前区)"); + + // 查询指定组合记录 + D9 targetRecord = d9Service.lambdaQuery() + .eq(D9::getMasterBallNumber, masterBall) + .eq(D9::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(前区)%d和随球(前区)%d的持续性记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d9Service.lambdaQuery() + .eq(D9::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(前区)%d的持续性记录", masterBall)); + } + + return buildPersistenceAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D9::getCoefficient, + D9::getSlaveBallNumber + ); + } + + @Override + public BallPersistenceAnalysisVO analyzeBackBackPersistence(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 12, "主球(后区)"); + validateBallRange(slaveBall, 1, 12, "随球(后区)"); + + // 查询指定组合记录 + D11 targetRecord = d11Service.lambdaQuery() + .eq(D11::getMasterBallNumber, masterBall) + .eq(D11::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(后区)%d和随球(后区)%d的持续性记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d11Service.lambdaQuery() + .eq(D11::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(后区)%d的持续性记录", masterBall)); + } + + return buildPersistenceAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D11::getCoefficient, + D11::getSlaveBallNumber + ); + } + + @Override + public BallPersistenceAnalysisVO analyzeFrontBackPersistence(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 35, "主球(前区)"); + validateBallRange(slaveBall, 1, 12, "随球(后区)"); + + // 查询指定组合记录 + D10 targetRecord = d10Service.lambdaQuery() + .eq(D10::getMasterBallNumber, masterBall) + .eq(D10::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(前区)%d和随球(后区)%d的持续性记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d10Service.lambdaQuery() + .eq(D10::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(前区)%d的持续性记录", masterBall)); + } + + return buildPersistenceAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D10::getCoefficient, + D10::getSlaveBallNumber + ); + } + + @Override + public BallPersistenceAnalysisVO analyzeBackFrontPersistence(Integer masterBall, Integer slaveBall) { + // 参数校验 + validateBallRange(masterBall, 1, 12, "主球(后区)"); + validateBallRange(slaveBall, 1, 35, "随球(前区)"); + + // 查询指定组合记录 + D12 targetRecord = d12Service.lambdaQuery() + .eq(D12::getMasterBallNumber, masterBall) + .eq(D12::getSlaveBallNumber, slaveBall) + .one(); + + if (targetRecord == null) { + throw new RuntimeException(String.format("未找到主球(后区)%d和随球(前区)%d的持续性记录", masterBall, slaveBall)); + } + + // 查询主球与所有其他球的组合记录 + List allCombinations = d12Service.lambdaQuery() + .eq(D12::getMasterBallNumber, masterBall) + .list(); + + if (allCombinations.isEmpty()) { + throw new RuntimeException(String.format("未找到主球(后区)%d的持续性记录", masterBall)); + } + + return buildPersistenceAnalysisVO( + targetRecord.getCoefficient(), + allCombinations, + D12::getCoefficient, + D12::getSlaveBallNumber + ); + } + + /** + * 验证球号范围 + */ + private void validateBallRange(Integer ballNumber, int minValue, int maxValue, String ballType) { + if (ballNumber == null) { + throw new IllegalArgumentException(ballType + "号码不能为空"); + } + if (ballNumber < minValue || ballNumber > maxValue) { + throw new IllegalArgumentException(String.format("%s号码必须在%d-%d范围内,错误值:%d", + ballType, minValue, maxValue, ballNumber)); + } + } + + /** + * 构建持续性分析结果VO + */ + private BallPersistenceAnalysisVO buildPersistenceAnalysisVO( + Double targetCoefficient, + List allCombinations, + Function coefficientGetter, + Function ballNumberGetter) { + + // 找出系数最高的球号 + T highest = allCombinations.stream() + .max(Comparator.comparing(coefficientGetter)) + .orElse(null); + + // 找出系数最低的球号 + T lowest = allCombinations.stream() + .min(Comparator.comparing(coefficientGetter)) + .orElse(null); + + // 计算平均系数 + double avgCoefficient = allCombinations.stream() + .mapToDouble(coefficientGetter::apply) + .average() + .orElse(0.0); + + // 获取最新开奖期号 + Long latestDrawId = getLatestDrawId(); + + return BallPersistenceAnalysisVO.builder() + .lineCoefficient(targetCoefficient) + .highestBall(highest != null ? ballNumberGetter.apply(highest) : null) + .highestCoefficient(highest != null ? coefficientGetter.apply(highest) : null) + .lowestBall(lowest != null ? ballNumberGetter.apply(lowest) : null) + .lowestCoefficient(lowest != null ? coefficientGetter.apply(lowest) : null) + .averageCoefficient(avgCoefficient) + .latestDrawId(latestDrawId) + .build(); + } + + /** + * 获取最新开奖期号 + */ + private Long getLatestDrawId() { + String latestDrawId = dltDrawRecordService.lambdaQuery() + .orderByDesc(DltDrawRecord::getDrawId) + .last("LIMIT 1") + .oneOpt() + .map(DltDrawRecord::getDrawId) + .orElse(null); + + if (latestDrawId != null) { + try { + return Long.parseLong(latestDrawId); + } catch (NumberFormatException e) { + log.warn("开奖期号格式转换失败:{}", latestDrawId); + } + } + return null; + } +} diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/DltPredictRecordServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/DltPredictRecordServiceImpl.java new file mode 100644 index 0000000..23de76e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/service/impl/DltPredictRecordServiceImpl.java @@ -0,0 +1,75 @@ +package com.xy.xyaicpzs.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xy.xyaicpzs.domain.entity.DltPredictRecord; +import com.xy.xyaicpzs.service.DltPredictRecordService; +import com.xy.xyaicpzs.mapper.DltPredictRecordMapper; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.List; + +/** +* @author XY003 +* @description 针对表【dlt_predict_record(大乐透推测记录表)】的数据库操作Service实现 +* @createDate 2025-09-08 14:01:12 +*/ +@Service +public class DltPredictRecordServiceImpl extends ServiceImpl + implements DltPredictRecordService{ + + @Override + public DltPredictRecord createDltPredictRecord(Long userId, Long drawId, Date drawDate, List frontBalls, List backBalls) { + DltPredictRecord predictRecord = new DltPredictRecord(); + predictRecord.setUserId(userId); + predictRecord.setDrawId(drawId); + predictRecord.setDrawDate(drawDate); + predictRecord.setPredictTime(new Date()); + + // 设置前区球号码(5个) + if (frontBalls != null && frontBalls.size() >= 5) { + predictRecord.setFrontendBall1(frontBalls.get(0)); + predictRecord.setFrontendBall2(frontBalls.get(1)); + predictRecord.setFrontendBall3(frontBalls.get(2)); + predictRecord.setFrontendBall4(frontBalls.get(3)); + predictRecord.setFrontendBall5(frontBalls.get(4)); + } + + // 设置后区球号码(2个) + if (backBalls != null && backBalls.size() >= 2) { + predictRecord.setBackendBall1(backBalls.get(0)); + predictRecord.setBackendBall2(backBalls.get(1)); + } + + // 默认状态为待开奖 + predictRecord.setPredictStatus("待开奖"); + + save(predictRecord); + return predictRecord; + } + + @Override + public List getDltPredictRecordsByUserIdWithPaging(Long userId, Integer page, Integer size) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId).orderByDesc("predictTime"); + + // 计算偏移量 + int offset = (page - 1) * size; + queryWrapper.last("LIMIT " + offset + ", " + size); + + return list(queryWrapper); + } + + @Override + public Long getDltPredictRecordsCountByUserId(Long userId) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("userId", userId); + return count(queryWrapper); + } + +} + + + + diff --git a/src/main/java/com/xy/xyaicpzs/service/impl/PredictRecordServiceImpl.java b/src/main/java/com/xy/xyaicpzs/service/impl/PredictRecordServiceImpl.java index a684d83..317fedf 100644 --- a/src/main/java/com/xy/xyaicpzs/service/impl/PredictRecordServiceImpl.java +++ b/src/main/java/com/xy/xyaicpzs/service/impl/PredictRecordServiceImpl.java @@ -44,6 +44,7 @@ public class PredictRecordServiceImpl extends ServiceImpl } return DigestUtils.md5DigestAsHex((SALT + password).getBytes()); } + + // region 统计相关方法实现 + + @Override + public UserStatisticsVO getNewUsersStatistics(String startDate, String endDate) { + try { + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); + + // 获取新增用户总数 + long totalNewUsers = this.count(queryWrapper); + + // 获取新增用户列表(最近20个) + queryWrapper.orderByDesc("createTime"); + queryWrapper.last("LIMIT 20"); + List recentUsers = this.list(queryWrapper); + List recentUserVOs = recentUsers.stream().map(user -> { + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return userVO; + }).collect(Collectors.toList()); + + return UserStatisticsVO.builder() + .totalNewUsers(totalNewUsers) + .startDate(startDate) + .endDate(endDate) + .recentUsers(recentUserVOs) + .build(); + + } catch (Exception e) { + log.error("获取新增用户统计失败:{}", e.getMessage()); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取统计数据失败"); + } + } + + @Override + public VipStatisticsVO getNewVipsStatistics(String startDate, String endDate) { + try { + // 构建查询条件 - 新增会员(isVip != 0 且在时间范围内) + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.ne("isVip", 0); + queryWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); + + // 获取新增会员总数 + long totalNewVips = this.count(queryWrapper); + + // 获取新增会员列表(最近20个) + queryWrapper.orderByDesc("createTime"); + queryWrapper.last("LIMIT 20"); + List recentVips = this.list(queryWrapper); + List recentVipVOs = recentVips.stream().map(user -> { + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return userVO; + }).collect(Collectors.toList()); + + // 计算会员转化率(该时间段内新增会员 / 新增用户总数) + QueryWrapper allUsersWrapper = new QueryWrapper<>(); + allUsersWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); + long totalNewUsers = this.count(allUsersWrapper); + + double conversionRate = totalNewUsers > 0 ? (double) totalNewVips / totalNewUsers * 100 : 0.0; + + return VipStatisticsVO.builder() + .totalNewVips(totalNewVips) + .totalNewUsers(totalNewUsers) + .conversionRate(Math.round(conversionRate * 100.0) / 100.0) + .startDate(startDate) + .endDate(endDate) + .recentVips(recentVipVOs) + .build(); + + } catch (Exception e) { + log.error("获取新增会员统计失败:{}", e.getMessage()); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取统计数据失败"); + } + } + + @Override + public RegistrationTrendVO getRegistrationTrend(String startDate, String endDate, String granularity) { + try { + // 这里简化处理,实际应该使用SQL的GROUP BY和DATE_FORMAT函数 + // 由于MyBatis-Plus的限制,我们先获取所有数据然后在Java中分组 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.between("createTime", startDate + " 00:00:00", endDate + " 23:59:59"); + queryWrapper.orderByAsc("createTime"); + List users = this.list(queryWrapper); + + Map trendData = new HashMap<>(); + Map vipTrendData = new HashMap<>(); + + // 在Java中按指定粒度分组统计 + for (User user : users) { + String key = formatDateByGranularity(user.getCreateTime(), granularity); + trendData.put(key, trendData.getOrDefault(key, 0L) + 1); + + // 同时统计会员趋势 + if (user.getIsVip() != null && user.getIsVip() != 0) { + vipTrendData.put(key, vipTrendData.getOrDefault(key, 0L) + 1); + } + } + + return RegistrationTrendVO.builder() + .startDate(startDate) + .endDate(endDate) + .granularity(granularity) + .userTrend(trendData) + .vipTrend(vipTrendData) + .totalUsers(users.size()) + .totalVips(users.stream().mapToLong(u -> u.getIsVip() != null && u.getIsVip() != 0 ? 1 : 0).sum()) + .build(); + + } catch (Exception e) { + log.error("获取用户注册趋势失败:{}", e.getMessage()); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取趋势数据失败"); + } + } + + @Override + public Page getExpiringVips(Integer days, Long current, Long pageSize) { + try { + // 计算目标日期范围 + Date now = new Date(); + Date futureDate = new Date(now.getTime() + days * 24 * 60 * 60 * 1000L); + + // 构建查询条件 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.ne("isVip", 0); // 是会员 + queryWrapper.isNotNull("vipExpire"); // 有到期时间 + queryWrapper.between("vipExpire", now, futureDate); // 在即将到期的时间范围内 + queryWrapper.orderByAsc("vipExpire"); // 按到期时间升序 + + Page userPage = this.page(new Page<>(current, pageSize), queryWrapper); + + // 转换为UserVO + Page userVOPage = new Page<>(userPage.getCurrent(), userPage.getSize(), userPage.getTotal()); + List userVOList = userPage.getRecords().stream().map(user -> { + UserVO userVO = new UserVO(); + BeanUtils.copyProperties(user, userVO); + return userVO; + }).collect(Collectors.toList()); + userVOPage.setRecords(userVOList); + + return userVOPage; + + } catch (Exception e) { + log.error("获取即将到期会员失败:{}", e.getMessage()); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取数据失败"); + } + } + + @Override + public VipDistributionVO getVipDistribution() { + try { + Date now = new Date(); + + // 总用户数 + long totalUsers = this.count(); + + // 普通用户数(isVip = 0 或 null) + QueryWrapper normalWrapper = new QueryWrapper<>(); + normalWrapper.and(wrapper -> wrapper.eq("isVip", 0).or().isNull("isVip")); + long normalUsers = this.count(normalWrapper); + + // 有效会员数(isVip != 0 且 vipExpire > now) + QueryWrapper activeVipWrapper = new QueryWrapper<>(); + activeVipWrapper.ne("isVip", 0) + .and(wrapper -> wrapper.isNull("vipExpire").or().gt("vipExpire", now)); + long activeVips = this.count(activeVipWrapper); + + // 过期会员数(isVip != 0 且 vipExpire <= now) + QueryWrapper expiredVipWrapper = new QueryWrapper<>(); + expiredVipWrapper.ne("isVip", 0) + .isNotNull("vipExpire") + .le("vipExpire", now); + long expiredVips = this.count(expiredVipWrapper); + + // 即将到期会员数(7天内到期) + Date sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000L); + QueryWrapper soonExpireWrapper = new QueryWrapper<>(); + soonExpireWrapper.ne("isVip", 0) + .between("vipExpire", now, sevenDaysLater); + long soonExpireVips = this.count(soonExpireWrapper); + + // 计算百分比 + double normalPercentage = totalUsers > 0 ? (double) normalUsers / totalUsers * 100 : 0; + double activeVipPercentage = totalUsers > 0 ? (double) activeVips / totalUsers * 100 : 0; + double expiredVipPercentage = totalUsers > 0 ? (double) expiredVips / totalUsers * 100 : 0; + + return VipDistributionVO.builder() + .totalUsers(totalUsers) + .normalUsers(normalUsers) + .normalPercentage(Math.round(normalPercentage * 100.0) / 100.0) + .activeVips(activeVips) + .activeVipPercentage(Math.round(activeVipPercentage * 100.0) / 100.0) + .expiredVips(expiredVips) + .expiredVipPercentage(Math.round(expiredVipPercentage * 100.0) / 100.0) + .soonExpireVips(soonExpireVips) + .statisticsTime(now) + .build(); + + } catch (Exception e) { + log.error("获取会员分布统计失败:{}", e.getMessage()); + throw new BusinessException(ErrorCode.SYSTEM_ERROR, "获取统计数据失败"); + } + } + + /** + * 根据时间粒度格式化日期 + */ + private String formatDateByGranularity(Date date, String granularity) { + if (date == null) return ""; + + java.time.LocalDateTime localDateTime = date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime(); + java.time.format.DateTimeFormatter formatter; + + switch (granularity) { + case "week": + // 获取年份和周数 + int year = localDateTime.getYear(); + int week = localDateTime.get(java.time.temporal.WeekFields.of(java.util.Locale.getDefault()).weekOfYear()); + return year + "-W" + String.format("%02d", week); + case "month": + formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"); + break; + default: + formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"); + break; + } + + return localDateTime.format(formatter); + } + + // endregion } \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/task/DltPredictResultTask.java b/src/main/java/com/xy/xyaicpzs/task/DltPredictResultTask.java new file mode 100644 index 0000000..49ca334 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/task/DltPredictResultTask.java @@ -0,0 +1,43 @@ +package com.xy.xyaicpzs.task; + +import com.xy.xyaicpzs.service.DltDataAnalysisService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * 大乐透预测结果处理定时任务 + */ +@Slf4j +@Component +public class DltPredictResultTask { + + @Autowired + private DltDataAnalysisService dltDataAnalysisService; + + /** + * 每10分钟执行一次大乐透预测结果匹配 + * cron表达式:秒 分 时 日 月 周 + * 0 0/10 * * * ? 表示每10分钟执行一次 + * 注释掉@Scheduled注解,因为PredictResultTask已经统一处理双色球和大乐透 + */ + // @Scheduled(cron = "0 0/10 * * * ?") + public void processPendingDltPredictions() { + try { + log.info("=== 开始执行大乐透预测结果匹配定时任务 ==="); + + long startTime = System.currentTimeMillis(); + + // 调用服务处理大乐透待开奖记录 + int processedCount = dltDataAnalysisService.processPendingDltPredictions(); + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + log.info("=== 大乐透预测结果匹配定时任务执行完成 === 处理记录数:{},耗时:{}ms", processedCount, duration); + + } catch (Exception e) { + log.error("大乐透预测结果匹配定时任务执行失败:{}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/xy/xyaicpzs/task/PredictResultTask.java b/src/main/java/com/xy/xyaicpzs/task/PredictResultTask.java index 3411da8..355bd44 100644 --- a/src/main/java/com/xy/xyaicpzs/task/PredictResultTask.java +++ b/src/main/java/com/xy/xyaicpzs/task/PredictResultTask.java @@ -1,13 +1,14 @@ package com.xy.xyaicpzs.task; import com.xy.xyaicpzs.service.DataAnalysisService; +import com.xy.xyaicpzs.service.DltDataAnalysisService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** - * 预测结果处理定时任务 + * 预测结果处理定时任务(双色球+大乐透) */ @Slf4j @Component @@ -16,25 +17,48 @@ public class PredictResultTask { @Autowired private DataAnalysisService dataAnalysisService; + @Autowired + private DltDataAnalysisService dltDataAnalysisService; + /** - * 每5分钟执行一次预测结果匹配 + * 每10分钟执行一次预测结果匹配(双色球+大乐透) * cron表达式:秒 分 时 日 月 周 - * 0 0/5 * * * ? 表示每5分钟执行一次 + * 0 0/10 * * * ? 表示每10分钟执行一次 */ @Scheduled(cron = "0 0/10 * * * ?") public void processPendingPredictions() { try { - log.info("=== 开始执行预测结果匹配定时任务 ==="); + log.info("=== 开始执行预测结果匹配定时任务(双色球+大乐透) ==="); long startTime = System.currentTimeMillis(); - // 调用服务处理待开奖记录 - int processedCount = dataAnalysisService.processPendingPredictions(); + int ssqProcessedCount = 0; + int dltProcessedCount = 0; + + // 处理双色球待开奖记录 + try { + log.info("开始处理双色球待开奖记录"); + ssqProcessedCount = dataAnalysisService.processPendingPredictions(); + log.info("双色球待开奖记录处理完成,共处理{}条", ssqProcessedCount); + } catch (Exception e) { + log.error("处理双色球待开奖记录时发生错误:{}", e.getMessage(), e); + } + + // 处理大乐透待开奖记录 + try { + log.info("开始处理大乐透待开奖记录"); + dltProcessedCount = dltDataAnalysisService.processPendingDltPredictions(); + log.info("大乐透待开奖记录处理完成,共处理{}条", dltProcessedCount); + } catch (Exception e) { + log.error("处理大乐透待开奖记录时发生错误:{}", e.getMessage(), e); + } long endTime = System.currentTimeMillis(); long duration = endTime - startTime; - log.info("=== 预测结果匹配定时任务执行完成 === 处理记录数:{},耗时:{}ms", processedCount, duration); + int totalProcessedCount = ssqProcessedCount + dltProcessedCount; + log.info("=== 预测结果匹配定时任务执行完成 === 双色球:{}条,大乐透:{}条,总计:{}条,耗时:{}ms", + ssqProcessedCount, dltProcessedCount, totalProcessedCount, duration); } catch (Exception e) { log.error("预测结果匹配定时任务执行失败:{}", e.getMessage(), e); diff --git a/src/main/java/com/xy/xyaicpzs/util/DltDataImporter.java b/src/main/java/com/xy/xyaicpzs/util/DltDataImporter.java new file mode 100644 index 0000000..09c4e2f --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/util/DltDataImporter.java @@ -0,0 +1,1361 @@ +package com.xy.xyaicpzs.util; + +import com.xy.xyaicpzs.domain.entity.DltDrawRecord; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryAll; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistory100; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop; +import com.xy.xyaicpzs.domain.entity.DltFrontendHistoryTop100; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryAll; +import com.xy.xyaicpzs.domain.entity.DltBackendHistory100; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop; +import com.xy.xyaicpzs.domain.entity.DltBackendHistoryTop100; +import com.xy.xyaicpzs.domain.entity.D5; +import com.xy.xyaicpzs.domain.entity.D6; +import com.xy.xyaicpzs.domain.entity.D7; +import com.xy.xyaicpzs.domain.entity.D8; +import com.xy.xyaicpzs.domain.entity.D9; +import com.xy.xyaicpzs.domain.entity.D10; +import com.xy.xyaicpzs.domain.entity.D11; +import com.xy.xyaicpzs.domain.entity.D12; +import com.xy.xyaicpzs.mapper.DltDrawRecordMapper; +import com.xy.xyaicpzs.mapper.DltFrontendHistoryAllMapper; +import com.xy.xyaicpzs.mapper.DltFrontendHistory100Mapper; +import com.xy.xyaicpzs.mapper.DltFrontendHistoryTopMapper; +import com.xy.xyaicpzs.mapper.DltFrontendHistoryTop100Mapper; +import com.xy.xyaicpzs.mapper.DltBackendHistoryAllMapper; +import com.xy.xyaicpzs.mapper.DltBackendHistory100Mapper; +import com.xy.xyaicpzs.mapper.DltBackendHistoryTopMapper; +import com.xy.xyaicpzs.mapper.DltBackendHistoryTop100Mapper; +import com.xy.xyaicpzs.mapper.D5Mapper; +import com.xy.xyaicpzs.mapper.D6Mapper; +import com.xy.xyaicpzs.mapper.D7Mapper; +import com.xy.xyaicpzs.mapper.D8Mapper; +import com.xy.xyaicpzs.mapper.D9Mapper; +import com.xy.xyaicpzs.mapper.D10Mapper; +import com.xy.xyaicpzs.mapper.D11Mapper; +import com.xy.xyaicpzs.mapper.D12Mapper; +import com.xy.xyaicpzs.service.DltDrawRecordService; +import com.xy.xyaicpzs.service.DltFrontendHistoryAllService; +import com.xy.xyaicpzs.service.DltFrontendHistory100Service; +import com.xy.xyaicpzs.service.DltFrontendHistoryTopService; +import com.xy.xyaicpzs.service.DltFrontendHistoryTop100Service; +import com.xy.xyaicpzs.service.DltBackendHistoryAllService; +import com.xy.xyaicpzs.service.DltBackendHistory100Service; +import com.xy.xyaicpzs.service.DltBackendHistoryTopService; +import com.xy.xyaicpzs.service.DltBackendHistoryTop100Service; +import com.xy.xyaicpzs.service.D5Service; +import com.xy.xyaicpzs.service.D6Service; +import com.xy.xyaicpzs.service.D7Service; +import com.xy.xyaicpzs.service.D8Service; +import com.xy.xyaicpzs.service.D9Service; +import com.xy.xyaicpzs.service.D10Service; +import com.xy.xyaicpzs.service.D11Service; +import com.xy.xyaicpzs.service.D12Service; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.FileInputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 大乐透Excel数据导入工具类 + * 用于读取D1 sheet的数据并插入到dlt_draw_record表中 + */ +@Slf4j +@Component +public class DltDataImporter { + + @Autowired + private DltDrawRecordMapper dltDrawRecordMapper; + + @Autowired + private DltDrawRecordService dltDrawRecordService; + + @Autowired + private DltFrontendHistoryAllMapper dltFrontendHistoryAllMapper; + + @Autowired + private DltFrontendHistory100Mapper dltFrontendHistory100Mapper; + + @Autowired + private DltFrontendHistoryTopMapper dltFrontendHistoryTopMapper; + + @Autowired + private DltFrontendHistoryTop100Mapper dltFrontendHistoryTop100Mapper; + + @Autowired + private DltBackendHistoryAllMapper dltBackendHistoryAllMapper; + + @Autowired + private DltBackendHistory100Mapper dltBackendHistory100Mapper; + + @Autowired + private DltBackendHistoryTopMapper dltBackendHistoryTopMapper; + + @Autowired + private DltBackendHistoryTop100Mapper dltBackendHistoryTop100Mapper; + + @Autowired + private DltFrontendHistoryAllService dltFrontendHistoryAllService; + + @Autowired + private DltFrontendHistory100Service dltFrontendHistory100Service; + + @Autowired + private DltFrontendHistoryTopService dltFrontendHistoryTopService; + + @Autowired + private DltFrontendHistoryTop100Service dltFrontendHistoryTop100Service; + + @Autowired + private DltBackendHistoryAllService dltBackendHistoryAllService; + + @Autowired + private DltBackendHistory100Service dltBackendHistory100Service; + + @Autowired + private DltBackendHistoryTopService dltBackendHistoryTopService; + + @Autowired + private DltBackendHistoryTop100Service dltBackendHistoryTop100Service; + + @Autowired + private D5Mapper d5Mapper; + + @Autowired + private D5Service d5Service; + + @Autowired + private D6Mapper d6Mapper; + + @Autowired + private D6Service d6Service; + + @Autowired + private D7Mapper d7Mapper; + + @Autowired + private D7Service d7Service; + + @Autowired + private D8Mapper d8Mapper; + + @Autowired + private D8Service d8Service; + + @Autowired + private D9Mapper d9Mapper; + + @Autowired + private D9Service d9Service; + + @Autowired + private D10Mapper d10Mapper; + + @Autowired + private D10Service d10Service; + + @Autowired + private D11Mapper d11Mapper; + + @Autowired + private D11Service d11Service; + + @Autowired + private D12Mapper d12Mapper; + + @Autowired + private D12Service d12Service; + + // ==================== D系列数据导入方法 ==================== + + /** + * 导入D5数据(D5工作表) + */ + public void importD5Data(String filePath) { + importGenericDData(filePath, "D5", d5Mapper, d5Service, D5.class); + } + + /** + * 导入D6数据(D6工作表) + */ + public void importD6Data(String filePath) { + importGenericDData(filePath, "D6", d6Mapper, d6Service, D6.class); + } + + /** + * 导入D7数据(D7工作表) + */ + public void importD7Data(String filePath) { + importGenericDData(filePath, "D7", d7Mapper, d7Service, D7.class); + } + + /** + * 导入D8数据(D8工作表) + */ + public void importD8Data(String filePath) { + importGenericDData(filePath, "D8", d8Mapper, d8Service, D8.class); + } + + /** + * 导入D9数据(D9工作表) + */ + public void importD9Data(String filePath) { + importGenericDData(filePath, "D9", d9Mapper, d9Service, D9.class); + } + + /** + * 导入D10数据(D10工作表) + */ + public void importD10Data(String filePath) { + importGenericDData(filePath, "D10", d10Mapper, d10Service, D10.class); + } + + /** + * 导入D11数据(D11工作表) + */ + public void importD11Data(String filePath) { + importGenericDData(filePath, "D11", d11Mapper, d11Service, D11.class); + } + + /** + * 导入D12数据(D12工作表) + */ + public void importD12Data(String filePath) { + importGenericDData(filePath, "D12", d12Mapper, d12Service, D12.class); + } + + /** + * 通用的D系列数据导入方法 + */ + private void importGenericDData(String filePath, String sheetName, + com.baomidou.mybatisplus.core.mapper.BaseMapper mapper, + com.baomidou.mybatisplus.extension.service.IService service, + Class entityClass) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + // 清空现有数据 + mapper.delete(null); + log.info("清空现有{}数据...", sheetName); + + // 处理对应的sheet + Sheet sheet = workbook.getSheet(sheetName); + if (sheet != null) { + log.info("开始导入{}数据", sheetName); + + // 读取数据并插入到对应表 + importGenericDSheetData(sheet, sheetName, service, entityClass); + + log.info("{}数据导入完成", sheetName); + } else { + log.warn("未找到{}工作表", sheetName); + } + + log.info("{}数据导入完成", sheetName); + + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 通用的D系列工作表数据导入方法 + */ + private void importGenericDSheetData(Sheet sheet, String sheetName, + com.baomidou.mybatisplus.extension.service.IService service, + Class entityClass) { + log.info("开始导入{}数据(系数数据)...", sheetName); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行 + int lastRowNum = sheet.getLastRowNum(); + + // 计算组数,每组占用3列,从A列开始 + int totalCols = 0; + if (lastRowNum > 0) { + Row firstDataRow = sheet.getRow(1); + if (firstDataRow != null) { + totalCols = firstDataRow.getLastCellNum(); + } + } + + int groupCount = totalCols / 3 + 1; // 每组3列 + log.info("检测到{}组数据,共{}列,数据行数{}行", groupCount, totalCols, lastRowNum); + + // 调试:打印前几行的原始数据 + log.info("=== 调试信息:前5行原始数据 ==="); + for (int debugRow = 0; debugRow <= Math.min(5, lastRowNum); debugRow++) { + Row row = sheet.getRow(debugRow); + if (row != null) { + StringBuilder rowData = new StringBuilder(); + rowData.append("行").append(debugRow + 1).append(": "); + for (int col = 0; col < Math.min(totalCols, 20); col++) { + Cell cell = row.getCell(col); + String cellValue = "null"; + if (cell != null) { + try { + switch (cell.getCellType()) { + case STRING: + cellValue = "\"" + cell.getStringCellValue() + "\""; + break; + case NUMERIC: + cellValue = String.valueOf(cell.getNumericCellValue()); + break; + default: + cellValue = cell.toString(); + } + } catch (Exception e) { + cellValue = "error"; + } + } + rowData.append("[").append((char)('A' + col)).append(":").append(cellValue).append("] "); + } + log.info(rowData.toString()); + } + } + log.info("=== 调试信息结束 ==="); + + // 遍历每组数据 + for (int group = 0; group < groupCount; group++) { + int masterBallNumber = group + 1; // 主球号从1开始 + int slaveBallCol = group * 3 + 1; // 从球号列:0,3,6...(A,D,G列) + int coefficientCol = group * 3 + 2; // 系数列:1,4,7...(B,E,H列) + + log.info("处理第{}组数据(主球{}号),从球号列:{},系数列:{}", group + 1, masterBallNumber, slaveBallCol, coefficientCol); + + int validDataCount = 0; // 统计这组有效数据数量 + int emptyBallNumber = 0; // 统计球号为空的数量 + int emptyCoefficient = 0; // 统计系数为空的数量 + + // 遍历所有数据行(跳过标题行) + for (int rowIndex = 1; rowIndex <= lastRowNum; rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null) { + continue; + } + + // 从Excel中读取从球号 + Integer slaveBallNumber = getCellIntegerValue(row.getCell(slaveBallCol)); + if (slaveBallNumber == null) { + emptyBallNumber++; + continue; // 如果球号为空,跳过这行 + } + + // 系数(从对应组的系数列读取) + Double coefficient = getCellNumericValue(row.getCell(coefficientCol)); + + if (coefficient != null) { + try { + T entity = entityClass.getDeclaredConstructor().newInstance(); + + // 使用反射设置字段值 + entityClass.getMethod("setMasterBallNumber", Integer.class).invoke(entity, masterBallNumber); + entityClass.getMethod("setSlaveBallNumber", Integer.class).invoke(entity, slaveBallNumber); + entityClass.getMethod("setCoefficient", Double.class).invoke(entity, roundToTwoDecimalPlaces(coefficient)); + + dataList.add(entity); + validDataCount++; + + log.info("添加{}数据:主球{},从球{},Excel行{},系数{}", + sheetName, masterBallNumber, slaveBallNumber, rowIndex + 1, coefficient); + } catch (Exception e) { + log.error("创建{}实体失败:{}", sheetName, e.getMessage(), e); + } + } else { + emptyCoefficient++; + log.debug("第{}组第{}行系数为空,球号{},跳过", group + 1, rowIndex + 1, slaveBallNumber); + } + } + + // 统计这组的处理结果 + log.info("第{}组统计:有效数据{}条,球号为空{}条,系数为空{}条", + group + 1, validDataCount, emptyBallNumber, emptyCoefficient); + } + + // 批量插入前的调试信息 + log.info("=== 准备批量插入{}数据 ===", sheetName); + log.info("待插入数据总数:{}", dataList.size()); + if (!dataList.isEmpty()) { + // 打印前几条数据作为样例 + for (int i = 0; i < Math.min(5, dataList.size()); i++) { + T entity = dataList.get(i); + try { + Integer masterBall = (Integer) entityClass.getMethod("getMasterBallNumber").invoke(entity); + Integer slaveBall = (Integer) entityClass.getMethod("getSlaveBallNumber").invoke(entity); + Double coeff = (Double) entityClass.getMethod("getCoefficient").invoke(entity); + log.info("样例数据{}:主球{},从球{},系数{}", i + 1, masterBall, slaveBall, coeff); + } catch (Exception e) { + log.warn("获取样例数据失败:{}", e.getMessage()); + } + } + } + log.info("=== 调试信息结束 ==="); + + // 批量插入 + if (!dataList.isEmpty()) { + try { + boolean result = service.saveBatch(dataList); + log.info("批量插入结果:{},成功导入{}条{}数据(系数数据)", result, dataList.size(), sheetName); + } catch (Exception e) { + log.error("批量插入{}数据失败:{}", sheetName, e.getMessage(), e); + throw e; + } + + // 验证数据统计 + log.info("{}数据导入统计:", sheetName); + for (int master = 1; master <= Math.min(groupCount, 12); master++) { + final int masterNum = master; + long count = dataList.stream().filter(entity -> { + try { + Integer masterBall = (Integer) entityClass.getMethod("getMasterBallNumber").invoke(entity); + return masterBall != null && masterBall.equals(masterNum); + } catch (Exception e) { + return false; + } + }).count(); + log.info("主球{}号记录数:{}", masterNum, count); + } + } else { + log.warn("未找到有效的{}数据", sheetName); + } + } + + // ==================== 其他导入方法 ==================== + + /** + * 完整导入大乐透数据(D3-D12工作表) + * 包含前区历史数据、后区历史数据和系数数据 + */ + public void importDltCompleteData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + StringBuilder importResults = new StringBuilder(); + StringBuilder errorMessages = new StringBuilder(); + + // 导入D3数据(前区历史数据) + try { + importDltFrontendHistoryData(filePath); + importResults.append("D3(前区历史数据)导入成功; "); + } catch (Exception e) { + log.warn("D3数据导入失败:{}", e.getMessage()); + errorMessages.append("D3导入失败: ").append(e.getMessage()).append("; "); + } + + // 导入D4数据(后区历史数据) + try { + importDltBackendHistoryData(filePath); + importResults.append("D4(后区历史数据)导入成功; "); + } catch (Exception e) { + log.warn("D4数据导入失败:{}", e.getMessage()); + errorMessages.append("D4导入失败: ").append(e.getMessage()).append("; "); + } + + // 导入D5-D12数据(系数数据) + for (int tableNum = 5; tableNum <= 12; tableNum++) { + try { + switch (tableNum) { + case 5: + importD5Data(filePath); + break; + case 6: + importD6Data(filePath); + break; + case 7: + importD7Data(filePath); + break; + case 8: + importD8Data(filePath); + break; + case 9: + importD9Data(filePath); + break; + case 10: + importD10Data(filePath); + break; + case 11: + importD11Data(filePath); + break; + case 12: + importD12Data(filePath); + break; + } + importResults.append("D").append(tableNum).append("导入成功; "); + } catch (Exception e) { + log.warn("D{}数据导入失败:{}", tableNum, e.getMessage()); + errorMessages.append("D").append(tableNum).append("导入失败: ").append(e.getMessage()).append("; "); + } + } + + // 记录导入结果 + if (errorMessages.length() > 0) { + log.warn("完整数据导入部分失败: {} 错误信息: {}", importResults.toString(), errorMessages.toString()); + throw new RuntimeException("部分导入失败: " + importResults.toString() + " 错误信息: " + errorMessages.toString()); + } else { + log.info("完整数据导入全部成功: {}", importResults.toString()); + } + + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 导入大乐透前区历史数据(D3工作表) + */ + public void importDltFrontendHistoryData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + // 清空现有前区历史数据 + clearFrontendHistoryData(); + + // 处理D3 sheet(前区历史数据) + Sheet d3Sheet = workbook.getSheet("D3"); + if (d3Sheet != null) { + int lastRowNum = d3Sheet.getLastRowNum(); + log.info("开始导入D3数据(前区历史数据),共{}行", lastRowNum); + + // 读取数据并插入到四个表 + importDltFrontendHistoryAllData(d3Sheet, lastRowNum); + importDltFrontendHistory100Data(d3Sheet, lastRowNum); + importDltFrontendHistoryTopData(d3Sheet, lastRowNum); + importDltFrontendHistoryTop100Data(d3Sheet, lastRowNum); + + log.info("D3数据(前区历史数据)导入完成"); + } else { + log.warn("未找到D3工作表"); + } + + log.info("大乐透前区历史数据导入完成"); + + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 导入大乐透后区历史数据(D4工作表) + */ + public void importDltBackendHistoryData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + // 清空现有后区历史数据 + clearBackendHistoryData(); + + // 处理D4 sheet(后区历史数据) + Sheet d4Sheet = workbook.getSheet("D4"); + if (d4Sheet != null) { + int lastRowNum = d4Sheet.getLastRowNum(); + log.info("开始导入D4数据(后区历史数据),共{}行", lastRowNum); + + // 读取数据并插入到四个表 + importDltBackendHistoryAllData(d4Sheet, lastRowNum); + importDltBackendHistory100Data(d4Sheet, lastRowNum); + importDltBackendHistoryTopData(d4Sheet, lastRowNum); + importDltBackendHistoryTop100Data(d4Sheet, lastRowNum); + + log.info("D4数据(后区历史数据)导入完成"); + } else { + log.warn("未找到D4工作表"); + } + + log.info("大乐透后区历史数据导入完成"); + + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 导入大乐透Excel数据到数据库 + */ + public void importDltDrawData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + // 清空现有数据 + clearExistingData(); + + // 处理D1 sheet(大乐透开奖数据) + Sheet d1Sheet = workbook.getSheet("D1"); + if (d1Sheet != null) { + log.info("开始导入D1数据(大乐透开奖信息)"); + + // 读取D1数据并插入到dlt_draw_record表 + importD1Data(d1Sheet); + + log.info("D1数据(大乐透开奖信息)导入完成"); + } else { + log.warn("未找到D1工作表"); + } + + log.info("大乐透数据导入完成"); + + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + /** + * 追加导入大乐透Excel数据到数据库(不清空现有数据) + */ + public void appendDltDrawData(String filePath) { + try (FileInputStream fis = new FileInputStream(filePath); + XSSFWorkbook workbook = new XSSFWorkbook(fis)) { + + // 处理D1 sheet(大乐透开奖数据) + Sheet d1Sheet = workbook.getSheet("D1"); + if (d1Sheet != null) { + log.info("开始追加导入D1数据(大乐透开奖信息)"); + + // 读取D1数据并追加到dlt_draw_record表 + appendD1Data(d1Sheet); + + log.info("D1数据(大乐透开奖信息)追加导入完成"); + } else { + log.warn("未找到D1工作表"); + } + + log.info("大乐透追加数据导入完成"); + + } catch (IOException e) { + log.error("读取Excel文件失败:{}", e.getMessage(), e); + throw new RuntimeException("Excel文件读取失败", e); + } + } + + // ==================== 清空数据方法 ==================== + + /** + * 清空现有大乐透前区历史数据 + */ + private void clearFrontendHistoryData() { + log.info("清空现有大乐透前区历史数据..."); + // 使用MyBatis-Plus的delete方法清空表 + dltFrontendHistoryAllMapper.delete(null); + dltFrontendHistory100Mapper.delete(null); + dltFrontendHistoryTopMapper.delete(null); + dltFrontendHistoryTop100Mapper.delete(null); + } + + /** + * 清空现有大乐透后区历史数据 + */ + private void clearBackendHistoryData() { + log.info("清空现有大乐透后区历史数据..."); + // 使用MyBatis-Plus的delete方法清空表 + dltBackendHistoryAllMapper.delete(null); + dltBackendHistory100Mapper.delete(null); + dltBackendHistoryTopMapper.delete(null); + dltBackendHistoryTop100Mapper.delete(null); + } + + /** + * 清空现有大乐透数据 + */ + private void clearExistingData() { + log.info("清空现有大乐透数据..."); + // 使用MyBatis-Plus的delete方法清空表 + dltDrawRecordMapper.delete(null); + } + + // ==================== 具体导入方法 ==================== + + /** + * 导入D1数据(大乐透开奖信息) + * 数据结构:标准表格结构 + * A列:开奖期号,B列:开奖日期 + * C-G列:前区1-5,H-I列:后区1-2 + */ + private void importD1Data(Sheet sheet) { + log.info("开始导入D1数据(大乐透开奖信息)..."); + List dataList = new ArrayList<>(); + + // 获取数据行数,跳过标题行 + int lastRowNum = sheet.getLastRowNum(); + log.info("D1工作表共{}行数据(包含标题行)", lastRowNum + 1); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltDrawRecord entity = new DltDrawRecord(); + + // A列: 开奖期号 + String drawId = getCellStringValue(row.getCell(0)); + if (drawId == null || drawId.trim().isEmpty()) { + log.warn("第{}行开奖期号为空,跳过", i + 1); + continue; + } + entity.setDrawId(drawId.trim()); + + // B列: 开奖日期 + Date drawDate = getCellDateValue(row.getCell(1)); + if (drawDate == null) { + log.warn("第{}行开奖日期为空,跳过", i + 1); + continue; + } + entity.setDrawDate(drawDate); + + // C列: 前区1 + entity.setFrontBall1(getCellIntegerValue(row.getCell(2))); + + // D列: 前区2 + entity.setFrontBall2(getCellIntegerValue(row.getCell(3))); + + // E列: 前区3 + entity.setFrontBall3(getCellIntegerValue(row.getCell(4))); + + // F列: 前区4 + entity.setFrontBall4(getCellIntegerValue(row.getCell(5))); + + // G列: 前区5 + entity.setFrontBall5(getCellIntegerValue(row.getCell(6))); + + // H列: 后区1 + entity.setBackBall1(getCellIntegerValue(row.getCell(7))); + + // I列: 后区2 + entity.setBackBall2(getCellIntegerValue(row.getCell(8))); + + // 验证必要字段 + if (entity.getFrontBall1() != null && entity.getFrontBall2() != null && + entity.getFrontBall3() != null && entity.getFrontBall4() != null && + entity.getFrontBall5() != null && entity.getBackBall1() != null && + entity.getBackBall2() != null) { + dataList.add(entity); + log.debug("添加大乐透开奖记录:期号{},日期{},前区{}-{}-{}-{}-{},后区{}-{}", + entity.getDrawId(), entity.getDrawDate(), + entity.getFrontBall1(), entity.getFrontBall2(), entity.getFrontBall3(), + entity.getFrontBall4(), entity.getFrontBall5(), + entity.getBackBall1(), entity.getBackBall2()); + } else { + log.warn("第{}行数据不完整,跳过", i + 1); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltDrawRecordService.saveBatch(dataList); + log.info("成功导入{}条D1数据(大乐透开奖信息)", dataList.size()); + } else { + log.warn("未找到有效的D1数据"); + } + } + + /** + * 追加导入D1数据(大乐透开奖信息),检查重复数据 + */ + private void appendD1Data(Sheet sheet) { + log.info("开始追加导入D1数据(大乐透开奖信息)..."); + List dataList = new ArrayList<>(); + int duplicateCount = 0; + int newCount = 0; + + // 获取数据行数,跳过标题行 + int lastRowNum = sheet.getLastRowNum(); + log.info("D1工作表共{}行数据(包含标题行)", lastRowNum + 1); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltDrawRecord entity = new DltDrawRecord(); + + // A列: 开奖期号 + String drawId = getCellStringValue(row.getCell(0)); + if (drawId == null || drawId.trim().isEmpty()) { + log.warn("第{}行开奖期号为空,跳过", i + 1); + continue; + } + entity.setDrawId(drawId.trim()); + + // 检查该期号是否已存在 + DltDrawRecord existingRecord = dltDrawRecordService.lambdaQuery() + .eq(DltDrawRecord::getDrawId, drawId.trim()) + .one(); + if (existingRecord != null) { + duplicateCount++; + log.debug("大乐透开奖期号{}已存在,跳过", drawId.trim()); + continue; + } + + // B列: 开奖日期 + Date drawDate = getCellDateValue(row.getCell(1)); + if (drawDate == null) { + log.warn("第{}行开奖日期为空,跳过", i + 1); + continue; + } + entity.setDrawDate(drawDate); + + // C列: 前区1 + entity.setFrontBall1(getCellIntegerValue(row.getCell(2))); + + // D列: 前区2 + entity.setFrontBall2(getCellIntegerValue(row.getCell(3))); + + // E列: 前区3 + entity.setFrontBall3(getCellIntegerValue(row.getCell(4))); + + // F列: 前区4 + entity.setFrontBall4(getCellIntegerValue(row.getCell(5))); + + // G列: 前区5 + entity.setFrontBall5(getCellIntegerValue(row.getCell(6))); + + // H列: 后区1 + entity.setBackBall1(getCellIntegerValue(row.getCell(7))); + + // I列: 后区2 + entity.setBackBall2(getCellIntegerValue(row.getCell(8))); + + // 验证必要字段 + if (entity.getFrontBall1() != null && entity.getFrontBall2() != null && + entity.getFrontBall3() != null && entity.getFrontBall4() != null && + entity.getFrontBall5() != null && entity.getBackBall1() != null && + entity.getBackBall2() != null) { + dataList.add(entity); + newCount++; + log.debug("添加新大乐透开奖记录:期号{},日期{},前区{}-{}-{}-{}-{},后区{}-{}", + entity.getDrawId(), entity.getDrawDate(), + entity.getFrontBall1(), entity.getFrontBall2(), entity.getFrontBall3(), + entity.getFrontBall4(), entity.getFrontBall5(), + entity.getBackBall1(), entity.getBackBall2()); + } else { + log.warn("第{}行数据不完整,跳过", i + 1); + } + } + + // 批量插入新数据 + if (!dataList.isEmpty()) { + dltDrawRecordService.saveBatch(dataList); + log.info("成功追加导入{}条新的D1数据(大乐透开奖信息)", dataList.size()); + } else { + log.info("没有新的D1数据需要导入"); + } + + log.info("追加导入统计:新增{}条,跳过重复{}条", newCount, duplicateCount); + } + + // ==================== 工具方法 ==================== + + /** + * 获取单元格的字符串值 + */ + private String getCellStringValue(Cell cell) { + if (cell == null) return null; + + try { + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + // 如果是数字,转换为字符串 + double numValue = cell.getNumericCellValue(); + // 如果是整数,不显示小数点 + if (numValue == (long) numValue) { + return String.valueOf((long) numValue); + } else { + return String.valueOf(numValue); + } + case FORMULA: + try { + return cell.getStringCellValue(); + } catch (Exception e) { + // 如果公式结果是数字,转换为字符串 + double formulaNumValue = cell.getNumericCellValue(); + if (formulaNumValue == (long) formulaNumValue) { + return String.valueOf((long) formulaNumValue); + } else { + return String.valueOf(formulaNumValue); + } + } + case BLANK: + return null; + default: + log.warn("不支持的单元格类型:{}", cell.getCellType()); + return null; + } + } catch (Exception e) { + log.warn("读取单元格字符串值失败:{}", e.getMessage()); + return null; + } + } + + /** + * 获取单元格的数值(保留两位小数) + */ + private Double getCellNumericValue(Cell cell) { + if (cell == null) return null; + + try { + double value = 0.0; + switch (cell.getCellType()) { + case NUMERIC: + value = cell.getNumericCellValue(); + break; + case STRING: + String strValue = cell.getStringCellValue().trim(); + if (strValue.isEmpty()) { + return null; + } + try { + value = Double.parseDouble(strValue); + } catch (NumberFormatException e) { + log.warn("无法解析单元格数值:{}", strValue); + return null; + } + break; + case FORMULA: + try { + value = cell.getNumericCellValue(); + } catch (Exception e) { + log.warn("无法获取公式单元格的数值:{}", e.getMessage()); + return null; + } + break; + case BLANK: + return null; + default: + log.warn("不支持的单元格类型:{}", cell.getCellType()); + return null; + } + + // 保留两位小数 + return roundToTwoDecimalPlaces(value); + + } catch (Exception e) { + log.warn("读取单元格数据失败:{}", e.getMessage()); + return null; + } + } + + /** + * 获取单元格的整数值 + */ + private Integer getCellIntegerValue(Cell cell) { + Double numericValue = getCellNumericValue(cell); + return numericValue != null ? numericValue.intValue() : null; + } + + /** + * 获取单元格的Date值 + */ + private Date getCellDateValue(Cell cell) { + if (cell == null) return null; + + try { + switch (cell.getCellType()) { + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue(); + } else { + // 如果是数字,尝试转换为日期 + return DateUtil.getJavaDate(cell.getNumericCellValue()); + } + case STRING: + String strValue = cell.getStringCellValue().trim(); + if (strValue.isEmpty()) return null; + // 尝试解析字符串日期,支持多种格式 + return parseStringToDate(strValue); + case FORMULA: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue(); + } else { + return DateUtil.getJavaDate(cell.getNumericCellValue()); + } + case BLANK: + return null; + default: + log.warn("不支持的日期单元格类型:{}", cell.getCellType()); + return null; + } + } catch (Exception e) { + log.warn("读取Date值失败:{}", e.getMessage()); + return null; + } + } + + /** + * 解析字符串为日期 + */ + private Date parseStringToDate(String dateStr) { + try { + // 支持多种日期格式 + SimpleDateFormat[] formats = { + new SimpleDateFormat("yyyy-MM-dd"), + new SimpleDateFormat("yyyy/MM/dd"), + new SimpleDateFormat("yyyy-M-d"), + new SimpleDateFormat("yyyy/M/d"), + new SimpleDateFormat("yyyy年MM月dd日"), + new SimpleDateFormat("yyyy年M月d日") + }; + + for (SimpleDateFormat format : formats) { + try { + return format.parse(dateStr); + } catch (java.text.ParseException ignored) { + // 继续尝试下一种格式 + } + } + + log.warn("无法解析日期字符串:{}", dateStr); + return null; + } catch (Exception e) { + log.warn("解析日期字符串失败:{}", e.getMessage()); + return null; + } + } + + /** + * 将数值保留两位小数 + * @param value 原始数值 + * @return 保留两位小数的数值 + */ + private Double roundToTwoDecimalPlaces(double value) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + return null; + } + + try { + BigDecimal bd = new BigDecimal(Double.toString(value)); + return bd.setScale(2, RoundingMode.HALF_UP).doubleValue(); + } catch (Exception e) { + log.warn("数值格式化失败:{}", value); + return value; + } + } + + // ==================== D3/D4数据导入方法 ==================== + + /** + * 导入大乐透前区全部历史数据 (列A-G) + */ + private void importDltFrontendHistoryAllData(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透前区全部历史数据..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltFrontendHistoryAll entity = new DltFrontendHistoryAll(); + + // 列A: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列B: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(1))); + + // 列C: 出现频率百分比 + entity.setFrequencyPercentage(getCellNumericValue(row.getCell(2))); + + // 列D: 平均隐现期 + entity.setAverageHiddenAppear(getCellIntegerValue(row.getCell(3))); + + // 列E: 最长隐现期 + entity.setMaxHiddenInterval(getCellIntegerValue(row.getCell(4))); + + // 列F: 最多连出期 + entity.setMaxConsecutive(getCellIntegerValue(row.getCell(5))); + + // 列G: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(6))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltFrontendHistoryAllService.saveBatch(dataList); + log.info("成功导入{}条大乐透前区全部历史数据", dataList.size()); + } + } + + /** + * 导入大乐透前区最近100期数据 (列H-M) + */ + private void importDltFrontendHistory100Data(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透前区最近100期数据..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltFrontendHistory100 entity = new DltFrontendHistory100(); + + // 使用第一列的球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列H: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(8))); + + // 列I: 平均隐现期 + entity.setAverageHiddenAppear(getCellNumericValue(row.getCell(9))); + + // 列J: 当前隐现期 + entity.setCurrentHiddenInterval(getCellIntegerValue(row.getCell(10))); + + // 列K: 最多连出期 + entity.setMaxConsecutive(getCellIntegerValue(row.getCell(11))); + + // 列L: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(12))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltFrontendHistory100Service.saveBatch(dataList); + log.info("成功导入{}条大乐透前区最近100期数据", dataList.size()); + } + } + + /** + * 导入大乐透前区历史数据排行 (列M-O) + */ + private void importDltFrontendHistoryTopData(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透前区历史数据排行..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltFrontendHistoryTop entity = new DltFrontendHistoryTop(); + + // 列M: 排位 + entity.setRanking(getCellIntegerValue(row.getCell(14))); + + // 列N: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(15))); + + // 列O: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(16))); + + if (entity.getBallNumber() != null && entity.getRanking() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltFrontendHistoryTopService.saveBatch(dataList); + log.info("成功导入{}条大乐透前区历史数据排行", dataList.size()); + } + } + + /** + * 导入大乐透前区百期数据排行 (列P-R) + */ + private void importDltFrontendHistoryTop100Data(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透前区百期数据排行..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltFrontendHistoryTop100 entity = new DltFrontendHistoryTop100(); + + // 列P: 排位 + entity.setRanking(getCellIntegerValue(row.getCell(18))); + + // 列Q: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(19))); + + // 列R: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(20))); + + if (entity.getBallNumber() != null && entity.getRanking() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltFrontendHistoryTop100Service.saveBatch(dataList); + log.info("成功导入{}条大乐透前区百期数据排行", dataList.size()); + } + } + + /** + * 导入大乐透后区全部历史数据 (列A-G) + */ + private void importDltBackendHistoryAllData(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透后区全部历史数据..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltBackendHistoryAll entity = new DltBackendHistoryAll(); + + // 列A: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列B: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(1))); + + // 列C: 出现频率百分比 + entity.setFrequencyPercentage(getCellNumericValue(row.getCell(2))); + + // 列D: 平均隐现期 + entity.setAverageHiddenAppear(getCellIntegerValue(row.getCell(3))); + + // 列E: 最长隐现期 + entity.setMaxHiddenInterval(getCellIntegerValue(row.getCell(4))); + + // 列F: 最多连出期 + entity.setMaxConsecutive(getCellIntegerValue(row.getCell(5))); + + // 列G: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(6))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltBackendHistoryAllService.saveBatch(dataList); + log.info("成功导入{}条大乐透后区全部历史数据", dataList.size()); + } + } + + /** + * 导入大乐透后区最近100期数据 (列H-L) + */ + private void importDltBackendHistory100Data(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透后区最近100期数据..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltBackendHistory100 entity = new DltBackendHistory100(); + + // 使用第一列的球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(0))); + + // 列H: 出现频次 + entity.setFrequencyCount(getCellIntegerValue(row.getCell(8))); + + // 列I: 平均隐现期 + entity.setAverageHiddenAppear(getCellNumericValue(row.getCell(9))); + + // 列J: 当前隐现期 + entity.setCurrentHiddenInterval(getCellIntegerValue(row.getCell(10))); + + // 列K: 最多连出期 + entity.setMaxConsecutive(getCellIntegerValue(row.getCell(11))); + + // 列L: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(12))); + + if (entity.getBallNumber() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltBackendHistory100Service.saveBatch(dataList); + log.info("成功导入{}条大乐透后区最近100期数据", dataList.size()); + } + } + + /** + * 导入大乐透后区历史数据排行 (列M-O) + */ + private void importDltBackendHistoryTopData(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透后区历史数据排行..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltBackendHistoryTop entity = new DltBackendHistoryTop(); + + // 列M: 排位 + entity.setRanking(getCellIntegerValue(row.getCell(14))); + + // 列N: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(15))); + + // 列O: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(16))); + + if (entity.getBallNumber() != null && entity.getRanking() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltBackendHistoryTopService.saveBatch(dataList); + log.info("成功导入{}条大乐透后区历史数据排行", dataList.size()); + } + } + + /** + * 导入大乐透后区百期数据排行 (列P-R) + */ + private void importDltBackendHistoryTop100Data(Sheet sheet, int lastRowNum) { + log.info("开始导入大乐透后区百期数据排行..."); + List dataList = new ArrayList<>(); + + for (int i = 1; i <= lastRowNum; i++) { // 跳过标题行 + Row row = sheet.getRow(i); + if (row == null) continue; + + DltBackendHistoryTop100 entity = new DltBackendHistoryTop100(); + + // 列P: 排位 + entity.setRanking(getCellIntegerValue(row.getCell(18))); + + // 列Q: 球号 + entity.setBallNumber(getCellIntegerValue(row.getCell(19))); + + // 列R: 活跃系数 + entity.setActiveCoefficient(getCellNumericValue(row.getCell(20))); + + if (entity.getBallNumber() != null && entity.getRanking() != null) { + dataList.add(entity); + } + } + + // 批量插入 + if (!dataList.isEmpty()) { + dltBackendHistoryTop100Service.saveBatch(dataList); + log.info("成功导入{}条大乐透后区百期数据排行", dataList.size()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/util/DltPrizeCalculator.java b/src/main/java/com/xy/xyaicpzs/util/DltPrizeCalculator.java new file mode 100644 index 0000000..79ccf8e --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/util/DltPrizeCalculator.java @@ -0,0 +1,216 @@ +package com.xy.xyaicpzs.util; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * 大乐透中奖规则计算器 + * 根据预测号码和开奖号码计算中奖等级和奖金 + */ +@Slf4j +public class DltPrizeCalculator { + + /** + * 中奖结果 + */ + @Data + public static class PrizeResult { + /** 中奖等级 */ + private String prizeLevel; + /** 奖金(元) */ + private Long bonus; + /** 前区命中个数 */ + private int frontMatches; + /** 后区命中个数 */ + private int backMatches; + + public PrizeResult(String prizeLevel, Long bonus, int frontMatches, int backMatches) { + this.prizeLevel = prizeLevel; + this.bonus = bonus; + this.frontMatches = frontMatches; + this.backMatches = backMatches; + } + } + + /** + * 计算大乐透中奖结果 + * + * @param predictNumbers 预测号码数组,格式:[前区5个号码, 后区2个号码] + * @param drawNumbers 开奖号码数组,格式:[前区5个号码, 后区2个号码] + * @return 中奖结果 + */ + public static PrizeResult calculatePrize(Integer[] predictNumbers, Integer[] drawNumbers) { + if (predictNumbers == null || drawNumbers == null) { + throw new IllegalArgumentException("预测号码和开奖号码不能为空"); + } + + if (predictNumbers.length != 7 || drawNumbers.length != 7) { + throw new IllegalArgumentException("号码数组长度必须为7(前区5个+后区2个)"); + } + + // 分离前区和后区号码 + Integer[] predictFront = Arrays.copyOfRange(predictNumbers, 0, 5); + Integer[] predictBack = Arrays.copyOfRange(predictNumbers, 5, 7); + Integer[] drawFront = Arrays.copyOfRange(drawNumbers, 0, 5); + Integer[] drawBack = Arrays.copyOfRange(drawNumbers, 5, 7); + + // 计算前区命中个数 + int frontMatches = calculateMatches(predictFront, drawFront); + + // 计算后区命中个数 + int backMatches = calculateMatches(predictBack, drawBack); + + log.debug("前区命中{}个,后区命中{}个", frontMatches, backMatches); + + // 根据中奖规则判断奖级 + String prizeLevel = determinePrizeLevel(frontMatches, backMatches); + Long bonus = calculateBonus(prizeLevel); + + return new PrizeResult(prizeLevel, bonus, frontMatches, backMatches); + } + + /** + * 计算两个号码数组的匹配个数 + */ + private static int calculateMatches(Integer[] predict, Integer[] draw) { + Set drawSet = new HashSet<>(Arrays.asList(draw)); + int matches = 0; + + for (Integer number : predict) { + if (number != null && drawSet.contains(number)) { + matches++; + } + } + + return matches; + } + + /** + * 根据前区和后区命中个数判断中奖等级 + */ + private static String determinePrizeLevel(int frontMatches, int backMatches) { + // 根据大乐透中奖规则图片判断 + if (frontMatches == 5 && backMatches == 2) { + return "一等奖"; + } else if (frontMatches == 5 && backMatches == 1) { + return "二等奖"; + } else if (frontMatches == 5 && backMatches == 0) { + return "三等奖"; + } else if (frontMatches == 4 && backMatches == 2) { + return "四等奖"; + } else if (frontMatches == 4 && backMatches == 1) { + return "五等奖"; + } else if (frontMatches == 4 && backMatches == 0) { + return "六等奖"; + } else if (frontMatches == 3 && backMatches == 2) { + return "七等奖"; + } else if ((frontMatches == 3 && backMatches == 1) || + (frontMatches == 2 && backMatches == 2)) { + return "八等奖"; + } else if ((frontMatches == 3 && backMatches == 0) || + (frontMatches == 2 && backMatches == 1) || + (frontMatches == 1 && backMatches == 2) || + (frontMatches == 0 && backMatches == 2)) { + return "九等奖"; + } else { + return "未中奖"; + } + } + + /** + * 根据中奖等级计算奖金(基本投注2元) + */ + private static Long calculateBonus(String prizeLevel) { + switch (prizeLevel) { + case "一等奖": + return 10000000L; // 1000万元(浮动奖金,这里设置为最高奖金) + case "二等奖": + return 5000000L; // 500万元(浮动奖金) + case "三等奖": + return 10000L; // 1万元 + case "四等奖": + return 3000L; // 3000元 + case "五等奖": + return 300L; // 300元 + case "六等奖": + return 200L; // 200元 + case "七等奖": + return 100L; // 100元 + case "八等奖": + return 15L; // 15元 + case "九等奖": + return 5L; // 5元 + default: + return 0L; // 未中奖 + } + } + + /** + * 验证号码格式是否正确 + */ + public static boolean validateNumbers(Integer[] numbers) { + if (numbers == null || numbers.length != 7) { + return false; + } + + // 验证前区号码(1-35) + for (int i = 0; i < 5; i++) { + if (numbers[i] == null || numbers[i] < 1 || numbers[i] > 35) { + return false; + } + } + + // 验证后区号码(1-12) + for (int i = 5; i < 7; i++) { + if (numbers[i] == null || numbers[i] < 1 || numbers[i] > 12) { + return false; + } + } + + // 验证前区号码不能重复 + Set frontSet = new HashSet<>(); + for (int i = 0; i < 5; i++) { + if (!frontSet.add(numbers[i])) { + return false; // 有重复 + } + } + + // 验证后区号码不能重复 + if (numbers[5].equals(numbers[6])) { + return false; + } + + return true; + } + + /** + * 格式化号码显示 + */ + public static String formatNumbers(Integer[] numbers) { + if (numbers == null || numbers.length != 7) { + return "无效号码"; + } + + StringBuilder sb = new StringBuilder(); + + // 前区号码 + sb.append("前区: "); + for (int i = 0; i < 5; i++) { + if (i > 0) sb.append(" "); + sb.append(String.format("%02d", numbers[i])); + } + + // 后区号码 + sb.append(" | 后区: "); + for (int i = 5; i < 7; i++) { + if (i > 5) sb.append(" "); + sb.append(String.format("%02d", numbers[i])); + } + + return sb.toString(); + } +} diff --git a/src/main/java/com/xy/xyaicpzs/util/ExcelTestRunner.java b/src/main/java/com/xy/xyaicpzs/util/ExcelTestRunner.java deleted file mode 100644 index bdc81b7..0000000 --- a/src/main/java/com/xy/xyaicpzs/util/ExcelTestRunner.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.xy.xyaicpzs.util; - -import com.xy.xyaicpzs.service.ExcelImportService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; - -/** - * Excel导入测试运行器 - * 可以在应用启动时自动执行Excel导入(需要手动启用) - */ -@Slf4j -@Component -public class ExcelTestRunner implements CommandLineRunner { - - @Autowired - private ExcelImportService excelImportService; - - @Override - public void run(String... args) throws Exception { - // 默认不执行,如果需要在启动时自动导入,请取消注释以下代码 - /* - log.info("开始执行Excel数据导入测试..."); - try { - // 请根据实际情况修改文件路径 - String filePath = "kaifa1.xlsx"; - String result = excelImportService.importExcelFileByPath(filePath); - log.info("导入结果:{}", result); - } catch (Exception e) { - log.error("导入失败:{}", e.getMessage(), e); - } - */ - } - - /** - * 手动执行导入的方法 - */ - public void manualImport(String filePath) { - log.info("手动执行Excel数据导入,文件路径:{}", filePath); - try { - String result = excelImportService.importExcelFileByPath(filePath); - log.info("导入结果:{}", result); - } catch (Exception e) { - log.error("导入失败:{}", e.getMessage(), e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/xy/xyaicpzs/util/UserAuthValidator.java b/src/main/java/com/xy/xyaicpzs/util/UserAuthValidator.java new file mode 100644 index 0000000..eb3afc6 --- /dev/null +++ b/src/main/java/com/xy/xyaicpzs/util/UserAuthValidator.java @@ -0,0 +1,128 @@ +package com.xy.xyaicpzs.util; + +import com.xy.xyaicpzs.common.ErrorCode; +import com.xy.xyaicpzs.common.ResultUtils; +import com.xy.xyaicpzs.common.response.ApiResponse; +import com.xy.xyaicpzs.domain.entity.User; +import com.xy.xyaicpzs.service.UserService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/** + * 用户权限验证工具类 + */ +@Component +public class UserAuthValidator { + + @Autowired + private UserService userService; + + // 权限常量 + public static final String ROLE_USER = "user"; + public static final String ROLE_ADMIN = "admin"; + public static final String ROLE_SUPER_ADMIN = "superAdmin"; + + /** + * 验证用户登录和VIP权限 + * @param request HTTP请求 + * @return 如果验证失败返回错误响应,成功返回null + */ + public ApiResponse validateUserAuth(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请登录后查看"); + } + + Date now = new Date(); + if (loginUser.getVipExpire() == null || loginUser.getVipExpire().before(now)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "VIP会员已过期,请续费后使用"); + } + + return null; // 验证通过 + } + + /** + * 验证用户是否为超级管理员 + * @param request HTTP请求 + * @return 如果验证失败返回错误响应,成功返回null + */ + public ApiResponse validateSuperAdminAuth(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请先登录"); + } + + if (!ROLE_SUPER_ADMIN.equals(loginUser.getUserRole())) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "需要超级管理员权限"); + } + + return null; // 验证通过 + } + + /** + * 验证用户是否为管理员(admin或superAdmin) + * @param request HTTP请求 + * @return 如果验证失败返回错误响应,成功返回null + */ + public ApiResponse validateAdminAuth(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请先登录"); + } + + String userRole = loginUser.getUserRole(); + if (!ROLE_ADMIN.equals(userRole) && !ROLE_SUPER_ADMIN.equals(userRole)) { + return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "需要管理员权限"); + } + + return null; // 验证通过 + } + + /** + * 验证用户登录状态 + * @param request HTTP请求 + * @return 如果验证失败返回错误响应,成功返回null + */ + public ApiResponse validateLoginAuth(HttpServletRequest request) { + User loginUser = userService.getLoginUser(request); + if (loginUser == null) { + return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, "您还未登录,请先登录"); + } + + return null; // 验证通过 + } + + /** + * 检查用户是否为超级管理员 + * @param user 用户对象 + * @return 是否为超级管理员 + */ + public boolean isSuperAdmin(User user) { + return user != null && ROLE_SUPER_ADMIN.equals(user.getUserRole()); + } + + /** + * 检查用户是否为管理员(admin或superAdmin) + * @param user 用户对象 + * @return 是否为管理员 + */ + public boolean isAdmin(User user) { + if (user == null) { + return false; + } + String userRole = user.getUserRole(); + return ROLE_ADMIN.equals(userRole) || ROLE_SUPER_ADMIN.equals(userRole); + } + + /** + * 获取当前登录用户 + * @param request HTTP请求 + * @return 当前登录用户,如果未登录返回null + */ + public User getCurrentUser(HttpServletRequest request) { + return userService.getLoginUser(request); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6e91acb..c93a405 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,7 +5,9 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cpzs username: cpzs_root - password: cpzs_root + password: cpzs_123456 +# username: root +# password: root # datasource: # driver-class-name: com.mysql.cj.jdbc.Driver # url: jdbc:mysql://47.117.22.239:3306/cpzs?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&failOverReadOnly=false&connectTimeout=10000&socketTimeout=30000 diff --git a/src/main/resources/generator/mapper/D10Mapper.xml b/src/main/resources/generator/mapper/D10Mapper.xml new file mode 100644 index 0000000..99bccef --- /dev/null +++ b/src/main/resources/generator/mapper/D10Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + diff --git a/src/main/resources/generator/mapper/D11Mapper.xml b/src/main/resources/generator/mapper/D11Mapper.xml new file mode 100644 index 0000000..2ac4aa8 --- /dev/null +++ b/src/main/resources/generator/mapper/D11Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + diff --git a/src/main/resources/generator/mapper/D12Mapper.xml b/src/main/resources/generator/mapper/D12Mapper.xml new file mode 100644 index 0000000..9303303 --- /dev/null +++ b/src/main/resources/generator/mapper/D12Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + diff --git a/src/main/resources/generator/mapper/D5Mapper.xml b/src/main/resources/generator/mapper/D5Mapper.xml new file mode 100644 index 0000000..fbb8956 --- /dev/null +++ b/src/main/resources/generator/mapper/D5Mapper.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + + \ No newline at end of file diff --git a/src/main/resources/generator/mapper/D6Mapper.xml b/src/main/resources/generator/mapper/D6Mapper.xml new file mode 100644 index 0000000..0fc6bca --- /dev/null +++ b/src/main/resources/generator/mapper/D6Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + diff --git a/src/main/resources/generator/mapper/D7Mapper.xml b/src/main/resources/generator/mapper/D7Mapper.xml new file mode 100644 index 0000000..8f05472 --- /dev/null +++ b/src/main/resources/generator/mapper/D7Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + diff --git a/src/main/resources/generator/mapper/D8Mapper.xml b/src/main/resources/generator/mapper/D8Mapper.xml new file mode 100644 index 0000000..7b7a527 --- /dev/null +++ b/src/main/resources/generator/mapper/D8Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + diff --git a/src/main/resources/generator/mapper/D9Mapper.xml b/src/main/resources/generator/mapper/D9Mapper.xml new file mode 100644 index 0000000..3fa99e5 --- /dev/null +++ b/src/main/resources/generator/mapper/D9Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,masterBallNumber,slaveBallNumber,coefficient + + diff --git a/src/main/resources/generator/mapper/DltBackendHistory100Mapper.xml b/src/main/resources/generator/mapper/DltBackendHistory100Mapper.xml new file mode 100644 index 0000000..135ba63 --- /dev/null +++ b/src/main/resources/generator/mapper/DltBackendHistory100Mapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,averageHiddenAppear,currentHiddenInterval,maxConsecutive, + activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltBackendHistoryAllMapper.xml b/src/main/resources/generator/mapper/DltBackendHistoryAllMapper.xml new file mode 100644 index 0000000..6a1e0e5 --- /dev/null +++ b/src/main/resources/generator/mapper/DltBackendHistoryAllMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,frequencyPercentage,averageHiddenAppear,maxHiddenInterval, + maxConsecutive,activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltBackendHistoryTop100Mapper.xml b/src/main/resources/generator/mapper/DltBackendHistoryTop100Mapper.xml new file mode 100644 index 0000000..d3cbfaa --- /dev/null +++ b/src/main/resources/generator/mapper/DltBackendHistoryTop100Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,ranking,ballNumber,activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltBackendHistoryTopMapper.xml b/src/main/resources/generator/mapper/DltBackendHistoryTopMapper.xml new file mode 100644 index 0000000..d890534 --- /dev/null +++ b/src/main/resources/generator/mapper/DltBackendHistoryTopMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,ranking,ballNumber,activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltDrawRecordMapper.xml b/src/main/resources/generator/mapper/DltDrawRecordMapper.xml new file mode 100644 index 0000000..93cdfe0 --- /dev/null +++ b/src/main/resources/generator/mapper/DltDrawRecordMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + id,drawId,drawDate,frontBall1,frontBall2,frontBall3, + frontBall4,frontBall5,backBall1,backBall2,createTime, + updateTime + + diff --git a/src/main/resources/generator/mapper/DltFrontendHistory100Mapper.xml b/src/main/resources/generator/mapper/DltFrontendHistory100Mapper.xml new file mode 100644 index 0000000..2247892 --- /dev/null +++ b/src/main/resources/generator/mapper/DltFrontendHistory100Mapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,averageHiddenAppear,currentHiddenInterval,maxConsecutive, + activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltFrontendHistoryAllMapper.xml b/src/main/resources/generator/mapper/DltFrontendHistoryAllMapper.xml new file mode 100644 index 0000000..3caa312 --- /dev/null +++ b/src/main/resources/generator/mapper/DltFrontendHistoryAllMapper.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + id,ballNumber,frequencyCount,frequencyPercentage,averageHiddenAppear,maxHiddenInterval, + maxConsecutive,activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltFrontendHistoryTop100Mapper.xml b/src/main/resources/generator/mapper/DltFrontendHistoryTop100Mapper.xml new file mode 100644 index 0000000..444bd2e --- /dev/null +++ b/src/main/resources/generator/mapper/DltFrontendHistoryTop100Mapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,ranking,ballNumber,activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltFrontendHistoryTopMapper.xml b/src/main/resources/generator/mapper/DltFrontendHistoryTopMapper.xml new file mode 100644 index 0000000..c54eb61 --- /dev/null +++ b/src/main/resources/generator/mapper/DltFrontendHistoryTopMapper.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + id,ranking,ballNumber,activeCoefficient + + diff --git a/src/main/resources/generator/mapper/DltPredictRecordMapper.xml b/src/main/resources/generator/mapper/DltPredictRecordMapper.xml new file mode 100644 index 0000000..dad7645 --- /dev/null +++ b/src/main/resources/generator/mapper/DltPredictRecordMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + id,userId,drawId,drawDate,frontendBall1,frontendBall2, + frontendBall3,frontendBall4,frontendBall5,backendBall1, + backendBall2,predictStatus,predictResult,predictTime,bonus + + diff --git a/src/test/java/com/xy/xyaicpzs/ApiResponseTest.java b/src/test/java/com/xy/xyaicpzs/ApiResponseTest.java new file mode 100644 index 0000000..c4bc4bf --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/ApiResponseTest.java @@ -0,0 +1,45 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.common.response.ApiResponse; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * ApiResponse测试类 + */ +public class ApiResponseTest { + + @Test + public void testSuccessResponse() { + String data = "测试数据"; + ApiResponse response = ApiResponse.success(data); + + assertEquals(0, response.getCode()); + assertTrue(response.isSuccess()); + assertEquals("操作成功", response.getMessage()); + assertEquals(data, response.getData()); + } + + @Test + public void testErrorResponse() { + String errorMessage = "错误信息"; + ApiResponse response = ApiResponse.error(errorMessage); + + assertEquals(1, response.getCode()); + assertFalse(response.isSuccess()); + assertEquals(errorMessage, response.getMessage()); + assertNull(response.getData()); + } + + @Test + public void testErrorResponseWithCode() { + Integer errorCode = 500; + String errorMessage = "系统错误"; + ApiResponse response = ApiResponse.error(errorCode, errorMessage); + + assertEquals(errorCode, response.getCode()); + assertFalse(response.isSuccess()); + assertEquals(errorMessage, response.getMessage()); + assertNull(response.getData()); + } +} \ No newline at end of file diff --git a/src/test/java/com/xy/xyaicpzs/DltPrizeCalculatorTest.java b/src/test/java/com/xy/xyaicpzs/DltPrizeCalculatorTest.java new file mode 100644 index 0000000..3c9b194 --- /dev/null +++ b/src/test/java/com/xy/xyaicpzs/DltPrizeCalculatorTest.java @@ -0,0 +1,151 @@ +package com.xy.xyaicpzs; + +import com.xy.xyaicpzs.util.DltPrizeCalculator; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * 大乐透中奖规则计算器测试类 + */ +public class DltPrizeCalculatorTest { + + @Test + public void testFirstPrize() { + // 一等奖:前区5+后区2 + Integer[] predict = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw = {1, 2, 3, 4, 5, 6, 7}; + + DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw); + + assertEquals("一等奖", result.getPrizeLevel()); + assertEquals(5, result.getFrontMatches()); + assertEquals(2, result.getBackMatches()); + assertEquals(10000000L, result.getBonus()); + } + + @Test + public void testSecondPrize() { + // 二等奖:前区5+后区1 + Integer[] predict = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw = {1, 2, 3, 4, 5, 6, 8}; + + DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw); + + assertEquals("二等奖", result.getPrizeLevel()); + assertEquals(5, result.getFrontMatches()); + assertEquals(1, result.getBackMatches()); + assertEquals(5000000L, result.getBonus()); + } + + @Test + public void testThirdPrize() { + // 三等奖:前区5+后区0 + Integer[] predict = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw = {1, 2, 3, 4, 5, 8, 9}; + + DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw); + + assertEquals("三等奖", result.getPrizeLevel()); + assertEquals(5, result.getFrontMatches()); + assertEquals(0, result.getBackMatches()); + assertEquals(10000L, result.getBonus()); + } + + @Test + public void testFourthPrize() { + // 四等奖:前区4+后区2 + Integer[] predict = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw = {1, 2, 3, 4, 8, 6, 7}; + + DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw); + + assertEquals("四等奖", result.getPrizeLevel()); + assertEquals(4, result.getFrontMatches()); + assertEquals(2, result.getBackMatches()); + assertEquals(3000L, result.getBonus()); + } + + @Test + public void testEighthPrize() { + // 八等奖:前区3+后区1 或 前区2+后区2 + Integer[] predict1 = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw1 = {1, 2, 3, 8, 9, 6, 10}; + + DltPrizeCalculator.PrizeResult result1 = DltPrizeCalculator.calculatePrize(predict1, draw1); + assertEquals("八等奖", result1.getPrizeLevel()); + assertEquals(15L, result1.getBonus()); + + // 前区2+后区2 + Integer[] predict2 = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw2 = {1, 2, 8, 9, 10, 6, 7}; + + DltPrizeCalculator.PrizeResult result2 = DltPrizeCalculator.calculatePrize(predict2, draw2); + assertEquals("八等奖", result2.getPrizeLevel()); + assertEquals(15L, result2.getBonus()); + } + + @Test + public void testNinthPrize() { + // 九等奖:前区3+后区0 + Integer[] predict = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw = {1, 2, 3, 8, 9, 10, 11}; + + DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw); + + assertEquals("九等奖", result.getPrizeLevel()); + assertEquals(3, result.getFrontMatches()); + assertEquals(0, result.getBackMatches()); + assertEquals(5L, result.getBonus()); + } + + @Test + public void testNoPrize() { + // 未中奖 + Integer[] predict = {1, 2, 3, 4, 5, 6, 7}; + Integer[] draw = {8, 9, 10, 11, 12, 8, 9}; + + DltPrizeCalculator.PrizeResult result = DltPrizeCalculator.calculatePrize(predict, draw); + + assertEquals("未中奖", result.getPrizeLevel()); + assertEquals(0, result.getFrontMatches()); + assertEquals(0, result.getBackMatches()); + assertEquals(0L, result.getBonus()); + } + + @Test + public void testValidateNumbers() { + // 有效号码 + Integer[] validNumbers = {1, 2, 3, 4, 5, 6, 7}; + assertTrue(DltPrizeCalculator.validateNumbers(validNumbers)); + + // 无效号码 - 长度不对 + Integer[] invalidLength = {1, 2, 3, 4, 5, 6}; + assertFalse(DltPrizeCalculator.validateNumbers(invalidLength)); + + // 无效号码 - 前区号码超出范围 + Integer[] invalidFront = {1, 2, 3, 4, 36, 6, 7}; + assertFalse(DltPrizeCalculator.validateNumbers(invalidFront)); + + // 无效号码 - 后区号码超出范围 + Integer[] invalidBack = {1, 2, 3, 4, 5, 6, 13}; + assertFalse(DltPrizeCalculator.validateNumbers(invalidBack)); + + // 无效号码 - 前区重复 + Integer[] duplicateFront = {1, 2, 3, 4, 1, 6, 7}; + assertFalse(DltPrizeCalculator.validateNumbers(duplicateFront)); + + // 无效号码 - 后区重复 + Integer[] duplicateBack = {1, 2, 3, 4, 5, 6, 6}; + assertFalse(DltPrizeCalculator.validateNumbers(duplicateBack)); + } + + @Test + public void testFormatNumbers() { + Integer[] numbers = {1, 2, 3, 4, 5, 6, 7}; + String formatted = DltPrizeCalculator.formatNumbers(numbers); + assertEquals("前区: 01 02 03 04 05 | 后区: 06 07", formatted); + } +} + + + diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..6ffd492 --- /dev/null +++ b/test.txt @@ -0,0 +1,229 @@ +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": { + "results": [ + { + "ballNumber": 14, + "frequency": 8, + "coefficientSum": 186.64000000000001, + "top100Ranking": 6, + "historyRanking": 1 + }, + { + "ballNumber": 7, + "frequency": 7, + "coefficientSum": 174.87, + "top100Ranking": 8, + "historyRanking": 10 + }, + { + "ballNumber": 26, + "frequency": 7, + "coefficientSum": 150.24, + "top100Ranking": 31, + "historyRanking": 2 + }, + { + "ballNumber": 2, + "frequency": 6, + "coefficientSum": 146.03, + "top100Ranking": 5, + "historyRanking": 12 + }, + { + "ballNumber": 18, + "frequency": 6, + "coefficientSum": 172.67000000000002, + "top100Ranking": 8, + "historyRanking": 8 + }, + { + "ballNumber": 20, + "frequency": 6, + "coefficientSum": 144.49, + "top100Ranking": 23, + "historyRanking": 7 + }, + { + "ballNumber": 22, + "frequency": 6, + "coefficientSum": 125.7, + "top100Ranking": 8, + "historyRanking": 3 + }, + { + "ballNumber": 4, + "frequency": 5, + "coefficientSum": 114.42999999999999, + "top100Ranking": 19, + "historyRanking": 22 + }, + { + "ballNumber": 6, + "frequency": 5, + "coefficientSum": 142.57, + "top100Ranking": 1, + "historyRanking": 6 + }, + { + "ballNumber": 8, + "frequency": 5, + "coefficientSum": 109.75999999999999, + "top100Ranking": 8, + "historyRanking": 13 + }, + { + "ballNumber": 17, + "frequency": 5, + "coefficientSum": 144.9, + "top100Ranking": 6, + "historyRanking": 4 + }, + { + "ballNumber": 27, + "frequency": 5, + "coefficientSum": 122.24000000000001, + "top100Ranking": 8, + "historyRanking": 11 + }, + { + "ballNumber": 30, + "frequency": 5, + "coefficientSum": 123.61, + "top100Ranking": 8, + "historyRanking": 19 + }, + { + "ballNumber": 1, + "frequency": 4, + "coefficientSum": 87.6, + "top100Ranking": 33, + "historyRanking": 4 + }, + { + "ballNumber": 10, + "frequency": 4, + "coefficientSum": 87.45, + "top100Ranking": 2, + "historyRanking": 14 + }, + { + "ballNumber": 12, + "frequency": 4, + "coefficientSum": 107.33, + "top100Ranking": 23, + "historyRanking": 18 + }, + { + "ballNumber": 15, + "frequency": 4, + "coefficientSum": 85.14, + "top100Ranking": 8, + "historyRanking": 25 + }, + { + "ballNumber": 5, + "frequency": 3, + "coefficientSum": 57.349999999999994, + "top100Ranking": 30, + "historyRanking": 21 + }, + { + "ballNumber": 9, + "frequency": 3, + "coefficientSum": 83.07000000000001, + "top100Ranking": 19, + "historyRanking": 16 + }, + { + "ballNumber": 13, + "frequency": 3, + "coefficientSum": 81.9, + "top100Ranking": 8, + "historyRanking": 19 + }, + { + "ballNumber": 19, + "frequency": 3, + "coefficientSum": 80.5, + "top100Ranking": 19, + "historyRanking": 15 + }, + { + "ballNumber": 24, + "frequency": 3, + "coefficientSum": 58.43, + "top100Ranking": 26, + "historyRanking": 31 + }, + { + "ballNumber": 25, + "frequency": 3, + "coefficientSum": 58.9, + "top100Ranking": 8, + "historyRanking": 23 + }, + { + "ballNumber": 32, + "frequency": 3, + "coefficientSum": 83.3, + "top100Ranking": 28, + "historyRanking": 8 + }, + { + "ballNumber": 3, + "frequency": 2, + "coefficientSum": 52.97, + "top100Ranking": 8, + "historyRanking": 17 + }, + { + "ballNumber": 11, + "frequency": 2, + "coefficientSum": 55.769999999999996, + "top100Ranking": 19, + "historyRanking": 23 + }, + { + "ballNumber": 16, + "frequency": 2, + "coefficientSum": 54.83, + "top100Ranking": 3, + "historyRanking": 25 + }, + { + "ballNumber": 23, + "frequency": 1, + "coefficientSum": 28.23, + "top100Ranking": 23, + "historyRanking": 27 + }, + { + "ballNumber": 31, + "frequency": 1, + "coefficientSum": 4.53, + "top100Ranking": 31, + "historyRanking": 29 + }, + { + "ballNumber": 33, + "frequency": 1, + "coefficientSum": 27.3, + "top100Ranking": 3, + "historyRanking": 33 + } + ], + "strategy": "H", + "redBalls": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "blueBall": 7 + } +} \ No newline at end of file diff --git a/大乐透数据导入使用说明.md b/大乐透数据导入使用说明.md new file mode 100644 index 0000000..233177f --- /dev/null +++ b/大乐透数据导入使用说明.md @@ -0,0 +1,471 @@ +# 大乐透数据导入使用说明 + +## 概述 + +本文档介绍如何使用大乐透数据导入功能,将包含D1工作表的Excel文件数据导入到`dlt_draw_record`数据库表中。 + +## 文件结构 + +### 新增文件 + +1. **DltDataImporter.java** - 大乐透数据导入工具类 + - 位置:`src/main/java/com/xy/xyaicpzs/util/DltDataImporter.java` + - 功能:解析Excel文件中的D1工作表,将数据导入到数据库 + +2. **DltTestRunner.java** - 测试运行器 + - 位置:`src/main/java/com/xy/xyaicpzs/util/DltTestRunner.java` + - 功能:提供手动测试导入功能 + +3. **DltImportController.java** - 导入控制器 + - 位置:`src/main/java/com/xy/xyaicpzs/controller/DltImportController.java` + - 功能:提供RESTful API接口 + +## Excel文件格式要求 + +### D1工作表结构(开奖数据) + +Excel文件必须包含名为"D1"的工作表,数据格式如下: + +| 列 | 字段名 | 数据类型 | 描述 | 示例 | +|----|--------|----------|------|------| +| A | 开奖期号 | 字符串 | 大乐透期号 | 07001 | +| B | 开奖日期 | 日期 | 开奖日期 | 2007-05-30 | +| C | 前区1 | 整数 | 前区第1个号码 | 22 | +| D | 前区2 | 整数 | 前区第2个号码 | 24 | +| E | 前区3 | 整数 | 前区第3个号码 | 29 | +| F | 前区4 | 整数 | 前区第4个号码 | 31 | +| G | 前区5 | 整数 | 前区第5个号码 | 33 | +| H | 后区1 | 整数 | 后区第1个号码 | 4 | +| I | 后区2 | 整数 | 后区第2个号码 | 11 | + +### 数据要求 + +- 第一行为标题行,从第二行开始为数据行 +- 开奖期号必须唯一,不能为空 +- 开奖日期支持多种格式:yyyy-MM-dd、yyyy/MM/dd、yyyy年MM月dd日等 +- 前区号码范围:1-35 +- 后区号码范围:1-12 +- 所有球号字段都不能为空 + +### D3工作表结构(前区历史数据) + +Excel文件必须包含名为"D3"的工作表,数据格式如下: + +| 列 | 字段名 | 数据类型 | 描述 | 示例 | +|----|--------|----------|------|------| +| A | 球号 | 整数 | 前区球号 | 1 | +| B | 出现频次 | 整数 | 全部历史出现频次 | 410 | +| C | 出现频率% | 小数 | 出现频率百分比 | 14.84 | +| D | 平均隐现期 | 整数 | 平均隐现期次数 | 6 | +| E | 最长隐现期 | 整数 | 最长隐现期次数 | 34 | +| F | 最多连出期 | 整数 | 最多连出期次数 | 4 | +| G | 活跃系数 | 小数 | 活跃系数 | 101.47 | +| H | 出现频次 | 整数 | 最近100期出现频次 | 12 | +| I | 平均隐现期 | 小数 | 最近100期平均隐现期 | 8.33 | +| J | 当前隐现期 | 整数 | 当前隐现期 | 1 | +| K | 最多连出期 | 整数 | 最近100期最多连出期 | 1 | +| L | 活跃系数 | 小数 | 最近100期活跃系数 | 2.97 | +| M | 排位 | 整数 | 历史数据排位 | 1 | +| N | 球号 | 整数 | 排行球号 | 29 | +| O | 活跃系数 | 小数 | 排行活跃系数 | 117.56 | +| P | 排位 | 整数 | 百期数据排位 | 1 | +| Q | 球号 | 整数 | 百期排行球号 | 20 | +| R | 活跃系数 | 小数 | 百期排行活跃系数 | 5.20 | + +### 数据要求(D3工作表) + +- 第一行为标题行,从第二行开始为数据行 +- 球号必须唯一,不能为空 +- 前区球号范围:1-35 +- 排位数据必须完整 +- 活跃系数支持小数 + +### D4工作表结构(后区历史数据) + +Excel文件必须包含名为"D4"的工作表,数据格式与D3相同: + +| 列 | 字段名 | 数据类型 | 描述 | 示例 | +|----|--------|----------|------|------| +| A | 球号 | 整数 | 后区球号 | 1 | +| B | 出现频次 | 整数 | 全部历史出现频次 | 150 | +| C | 出现频率% | 小数 | 出现频率百分比 | 12.50 | +| D | 平均隐现期 | 整数 | 平均隐现期次数 | 8 | +| E | 最长隐现期 | 整数 | 最长隐现期次数 | 25 | +| F | 最多连出期 | 整数 | 最多连出期次数 | 3 | +| G | 活跃系数 | 小数 | 活跃系数 | 95.20 | +| H | 出现频次 | 整数 | 最近100期出现频次 | 8 | +| I | 平均隐现期 | 小数 | 最近100期平均隐现期 | 12.50 | +| J | 当前隐现期 | 整数 | 当前隐现期 | 2 | +| K | 最多连出期 | 整数 | 最近100期最多连出期 | 2 | +| L | 活跃系数 | 小数 | 最近100期活跃系数 | 4.00 | +| M | 排位 | 整数 | 历史数据排位 | 1 | +| N | 球号 | 整数 | 排行球号 | 12 | +| O | 活跃系数 | 小数 | 排行活跃系数 | 105.50 | +| P | 排位 | 整数 | 百期数据排位 | 1 | +| Q | 球号 | 整数 | 百期排行球号 | 8 | +| R | 活跃系数 | 小数 | 百期排行活跃系数 | 4.50 | + +### 数据要求(D4工作表) + +- 第一行为标题行,从第二行开始为数据行 +- 球号必须唯一,不能为空 +- 后区球号范围:1-12 +- 排位数据必须完整 +- 活跃系数支持小数 + +## API接口 + +### 1. 上传文件导入开奖数据 + +**接口地址:** `POST /dlt/upload-draw-data` + +**描述:** 上传Excel文件并导入大乐透开奖数据(D1工作表,会清空现有数据) + +**参数:** +- `file`: MultipartFile类型,Excel文件(.xlsx格式) + +**权限:** 需要管理员权限 + +**返回示例:** +```json +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": "大乐透开奖数据导入成功" +} +``` + +### 2. 上传文件导入前区历史数据 + +**接口地址:** `POST /dlt/upload-frontend-history` + +**描述:** 上传Excel文件并导入大乐透前区历史数据(D3工作表,会清空现有数据) + +**参数:** +- `file`: MultipartFile类型,Excel文件(.xlsx格式) + +**权限:** 需要管理员权限 + +**返回示例:** +```json +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": "大乐透前区历史数据导入成功" +} +``` + +### 3. 上传文件导入后区历史数据 + +**接口地址:** `POST /dlt/upload-backend-history` + +**描述:** 上传Excel文件并导入大乐透后区历史数据(D4工作表,会清空现有数据) + +**参数:** +- `file`: MultipartFile类型,Excel文件(.xlsx格式) + +**权限:** 需要管理员权限 + +**返回示例:** +```json +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": "大乐透后区历史数据导入成功" +} +``` + +### 4. 追加导入开奖数据 + +**接口地址:** `POST /dlt/upload-append-draw` + +**描述:** 上传Excel文件并追加导入大乐透开奖数据(D1工作表,不清空现有数据,跳过重复记录) + +**参数:** +- `file`: MultipartFile类型,Excel文件(.xlsx格式) + +**权限:** 需要管理员权限 + +**返回示例:** +```json +{ + "code": 0, + "success": true, + "message": "操作成功", + "data": "大乐透开奖数据追加导入成功" +} +``` + +### 5. 文件路径导入开奖数据(测试用) + +**接口地址:** `POST /dlt/import-draw-by-path` + +**描述:** 根据服务器上的文件路径导入开奖数据 + +**参数:** +- `filePath`: 字符串,服务器上的Excel文件路径 + +**权限:** 需要管理员权限 + +### 6. 文件路径导入前区历史数据(测试用) + +**接口地址:** `POST /dlt/import-frontend-by-path` + +**描述:** 根据服务器上的文件路径导入前区历史数据 + +**参数:** +- `filePath`: 字符串,服务器上的Excel文件路径 + +**权限:** 需要管理员权限 + +### 7. 文件路径导入后区历史数据(测试用) + +**接口地址:** `POST /dlt/import-backend-by-path` + +**描述:** 根据服务器上的文件路径导入后区历史数据 + +**参数:** +- `filePath`: 字符串,服务器上的Excel文件路径 + +**权限:** 需要管理员权限 + +## 使用步骤 + +### 1. 准备Excel文件 + +#### 开奖数据文件(D1工作表) +1. 创建包含D1工作表的Excel文件(.xlsx格式) +2. 按照上述D1格式要求填入大乐透开奖数据 +3. 确保数据完整性和格式正确性 + +#### 前区历史数据文件(D3工作表) +1. 创建包含D3工作表的Excel文件(.xlsx格式) +2. 按照上述D3格式要求填入前区历史数据 +3. 确保数据完整性和格式正确性 + +#### 后区历史数据文件(D4工作表) +1. 创建包含D4工作表的Excel文件(.xlsx格式) +2. 按照上述D4格式要求填入后区历史数据 +3. 确保数据完整性和格式正确性 + +### 2. 通过API导入 + +#### 方式一:文件上传导入开奖数据 +```bash +curl -X POST \ + http://localhost:8080/dlt/upload-draw-data \ + -H 'Content-Type: multipart/form-data' \ + -F 'file=@/path/to/your/dlt_draw_data.xlsx' +``` + +#### 方式二:文件上传导入前区历史数据 +```bash +curl -X POST \ + http://localhost:8080/dlt/upload-frontend-history \ + -H 'Content-Type: multipart/form-data' \ + -F 'file=@/path/to/your/dlt_frontend_data.xlsx' +``` + +#### 方式三:文件上传导入后区历史数据 +```bash +curl -X POST \ + http://localhost:8080/dlt/upload-backend-history \ + -H 'Content-Type: multipart/form-data' \ + -F 'file=@/path/to/your/dlt_backend_data.xlsx' +``` + +#### 方式四:服务器文件路径导入 +```bash +# 导入开奖数据 +curl -X POST \ + 'http://localhost:8080/dlt/import-draw-by-path?filePath=/path/to/server/file.xlsx' + +# 导入前区历史数据 +curl -X POST \ + 'http://localhost:8080/dlt/import-frontend-by-path?filePath=/path/to/server/file.xlsx' + +# 导入后区历史数据 +curl -X POST \ + 'http://localhost:8080/dlt/import-backend-by-path?filePath=/path/to/server/file.xlsx' +``` + +### 3. 查看导入结果 + +- 开奖数据导入成功后,数据将保存到`dlt_draw_record`表中 +- 前区历史数据导入成功后,数据将保存到四个前区历史相关表中 +- 后区历史数据导入成功后,数据将保存到四个后区历史相关表中 +- 系统会记录操作历史,可通过操作历史接口查看详细信息 +- 查看应用日志了解详细的导入过程和统计信息 + +## 数据库表结构 + +### 1. dlt_draw_record表(开奖数据) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_draw_record` ( + `id` BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + `drawId` VARCHAR(50) NOT NULL COMMENT '开奖期号', + `drawDate` DATE NOT NULL COMMENT '开奖日期', + `frontBall1` INT NOT NULL COMMENT '前区1', + `frontBall2` INT NOT NULL COMMENT '前区2', + `frontBall3` INT NOT NULL COMMENT '前区3', + `frontBall4` INT NOT NULL COMMENT '前区4', + `frontBall5` INT NOT NULL COMMENT '前区5', + `backBall1` INT NOT NULL COMMENT '后区1', + `backBall2` INT NOT NULL COMMENT '后区2', + `createTime` datetime DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '记录创建时间', + `updateTime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL COMMENT '记录更新时间' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透开奖信息表'; +``` + +### 2. dlt_frontend_history_all表(前区全部历史数据) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_all` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + frequencyPercentage FLOAT NULL COMMENT '出现频率%', + averageHiddenAppear INT NULL COMMENT '平均隐现期(次)', + maxHiddenInterval INT NULL COMMENT '最长隐现期(次)', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区全部历史数据表'; +``` + +### 3. dlt_frontend_history_100表(前区最近100期数据) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + averageHiddenAppear FLOAT NULL COMMENT '平均隐现期(次)', + currentHiddenInterval INT NULL COMMENT '当前隐现期', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区最近100期数据表'; +``` + +### 4. dlt_frontend_history_top表(前区历史数据排行) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_top` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区历史数据排行表'; +``` + +### 5. dlt_frontend_history_top_100表(前区百期数据排行) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_frontend_history_top_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透前区百期数据排行表'; +``` + +### 6. dlt_backend_history_all表(后区全部历史数据) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_backend_history_all` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + frequencyPercentage FLOAT NULL COMMENT '出现频率%', + averageHiddenAppear INT NULL COMMENT '平均隐现期(次)', + maxHiddenInterval INT NULL COMMENT '最长隐现期(次)', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区全部历史数据表'; +``` + +### 7. dlt_backend_history_100表(后区最近100期数据) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_backend_history_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ballNumber INT NOT NULL COMMENT '球号', + frequencyCount INT NULL COMMENT '出现频次', + averageHiddenAppear FLOAT NULL COMMENT '平均隐现期(次)', + currentHiddenInterval INT NULL COMMENT '当前隐现期', + maxConsecutive INT NULL COMMENT '最多连出期(次)', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区最近100期数据表'; +``` + +### 8. dlt_backend_history_top表(后区历史数据排行) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_backend_history_top` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区历史数据排行表'; +``` + +### 9. dlt_backend_history_top_100表(后区百期数据排行) + +```sql +CREATE TABLE IF NOT EXISTS `dlt_backend_history_top_100` ( + id BIGINT AUTO_INCREMENT COMMENT '唯一标识符' PRIMARY KEY, + ranking INT NULL COMMENT '排位', + ballNumber INT NULL COMMENT '球号', + activeCoefficient FLOAT NULL COMMENT '活跃系数' +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '大乐透后区百期数据排行表'; +``` + +## 错误处理 + +### 常见错误及解决方案 + +1. **文件格式错误** + - 错误:`只支持.xlsx格式的Excel文件` + - 解决:确保上传的是.xlsx格式的Excel文件 + +2. **工作表不存在** + - 错误:`未找到D1工作表` + - 解决:确保Excel文件包含名为"D1"的工作表 + +3. **数据不完整** + - 错误:`第X行数据不完整,跳过` + - 解决:检查对应行的数据,确保所有必需字段都有值 + +4. **日期格式错误** + - 错误:`第X行开奖日期为空,跳过` + - 解决:检查日期格式,支持yyyy-MM-dd、yyyy/MM/dd等格式 + +5. **权限不足** + - 错误:`无权限` + - 解决:确保使用管理员账号登录 + +## 日志信息 + +导入过程中会产生详细的日志信息: + +- **INFO级别**:导入进度、成功统计 +- **WARN级别**:跳过的无效数据行 +- **ERROR级别**:导入失败的错误信息 +- **DEBUG级别**:每条记录的详细信息(需开启DEBUG日志) + +## 性能说明 + +- 支持批量导入,使用MyBatis-Plus的`saveBatch`方法 +- 追加导入时会检查重复记录,避免数据重复 +- 大文件导入建议分批处理,避免内存溢出 + +## 注意事项 + +1. **数据备份**:导入前建议备份现有数据 +2. **权限控制**:所有导入接口都需要管理员权限 +3. **文件大小**:注意服务器文件上传大小限制 +4. **并发控制**:避免同时进行多个导入操作 +5. **操作记录**:所有导入操作都会记录到操作历史表中 \ No newline at end of file diff --git a/算法第一步.txt b/算法第一步.txt new file mode 100644 index 0000000..02710dd --- /dev/null +++ b/算法第一步.txt @@ -0,0 +1,128 @@ +帮我在写一个算法,推测前区首球。 +入参有高位/中位/低位,5个前区号码,加2个后区号码。 +分三种情况: +如果是高位: +step1.先拿这5个前区号码分别作为主球号到d9(d9表中的系数需要先按系数从大到小排列)表去查询系数最大的前17位,注意第17位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend__historytop_100 表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第16/17/18/19这四个位置的号码对应的系数相同,需要选择最后(需要筛选出的17位号码)的16位和17位两个号码,则需要那这4个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +如果结果是18位的系数最高,其余位置的系数相同;首先将18位对应的号码作为需要筛选出的17位号码的第十六位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的17位号码的第十七位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出17个球号后,还需要记录这17个球号在d9表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成85个球号的map +step2.筛选出dlt_frontend__historytop表中去系数最大的3个值作为对应的系数; +step3.筛选出dlt_frontend__historytop_100表中去系数最大的3个值作为对应的系数;这一步也需要处理毛边; +若3个号码中第三个位置对应的系数有多个,需要拿这多个球号去dlt_frontend__historytop比较系数值大的号码作为最终的号码, +若经过dlt_frontend__historytop还是无法选出,就默认选择第一位 +step4.最后拿这2个后区号码分别作为主球号到d12表(d12表中的系数需要先按系数从大到小排列)去查询系数最大的前17位,注意第17位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend__historytop_100 表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第16/17/18/19这四个位置的号码对应的系数相同,需要选择最后的16位和17位两个号码,则需要那这4个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +如果结果是18位的系数最高,其余位置的系数相同;首先将18位对应的号码作为需要筛选出的17位号码的第十六位; +则需要那这个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 + +最后组成5*17 + 3 + 3 + 2*17 = 125个号码 + +从125个备选球号中,按照“累计出现最多次;D9系数最大和;百期排行最靠前;历史排行为终裁”的阶梯原则,依次取出前12个。 +具体是这样的: +首先,需要统计125个球号出现的次数,取出出现次数最多的球号,若出现次数排名前十的已经选出,而第11/12/13/14对应的出现次数一样, +则需要拿这四个球号分别去step1中记录的map取出对应的系数,因为1个球号在map中可能有有多个,需要记录系数和,如果1次都没出现,对应的系数和则为0,最后比较这个球号对应的系数和, +若14位球号对应的系数和最大,其余相等,则取第14位的球号最为12个中的第11个; +此时还没有到达12个,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop 比较对应的系数,最终组成12个球号。 + + +如果是中位: +step1.先拿这5个前区号码分别作为主球号到d9(d9表中的系数需要先按系数从大到小排列)表取平均值(每个球号作为主球号去d9表查询,对应的所有系数的平均值,离这个平均值最近且比它大的号码。下面的平均值定义也一样) +向上8个球,向下8个球,组成17个球号,注意此时第1位和第17位的号码都需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend__historytop_100 表中比较,取系数高者 + * 2) 若经过dlt_history__frontend_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,则就默认选择第一位 +处理下方毛边时: +比如第16/17/18/19这四个位置的号码对应的系数相同,需要选择最后的16位和17位两个号码,则需要那这4个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +如果结果是18位的系数最高,其余位置的系数相同;首先将18位对应的号码作为需要筛选出的下方8位号码的第7位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的下发8位号码的第8位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +处理上方毛边时: +比如 第1/2和第一位上面的两个球号 这四个位置的号码对应的系数相同,需要选择最后的第1位和第2位两个号码,则需要那这4个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +如果结果是第2位系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的8位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的8位号码的第2位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出17个球号后,还需要记录这17个球号在d9表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成85个球号的map +step2.筛选出dlt_frontend__history_top表中去系数最大的3个值作为对应的系数; +step3.筛选出dlt_frontend__history_top_100表中去系数最大的3个值作为对应的系数;这一步也需要处理毛边; +若3个号码中第三个位置对应的系数有多个,需要拿这多个球号去dlt_frontend__historytop比较系数值大的号码作为最终的号码, +若经过dlt_frontend__historytop还是无法选出,就默认选择第一位 +step4.最后拿这2个后区号码分别作为主球号到d12表(d12表中的系数需要先按系数从大到小排列)去查询系数最大的前17位,注意此时第1位和第17位的号码都需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend__historytop_100 表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +处理下方毛边时: +比如第16/17/18/19这四个位置的号码对应的系数相同,需要选择最后的16位和17位两个号码,则需要那这4个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +如果结果是18位的系数最高,其余位置的系数相同;首先将18位对应的号码作为需要筛选出的下方8位号码的第7位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的下发8位号码的第8位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +处理上方毛边时: +比如 第1/2和第一位上面的两个球号 这四个位置的号码对应的系数相同,需要选择最后的第1位和第2位两个号码,则需要那这4个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +如果结果是第2位系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的8位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的8位号码的第2位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 + +最后组成5*17 + 3 + 3 + 2*17 = 125个号码 + +从125个备选球号中,按照“累计出现最多次;D9系数最大和;百期排行最靠前;历史排行为终裁”的阶梯原则,依次取出前12个。 +具体是这样的: +首先,需要统计125个球号出现的次数,取出出现次数最多的球号,若出现次数排名前十的已经选出,而第11/12/13/14对应的出现次数一样, +则需要拿这四个球号分别去step1中记录的map取出对应的系数,因为1个球号在map中可能有有多个,需要记录系数和,如果1次都没出现,对应的系数和则为0,最后比较这个球号对应的系数和, +若14位球号对应的系数和最大,其余相等,则取第14位的球号最为12个中的第11个; +此时还没有到达12个,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop 比较对应的系数,最终组成12个球号。 + + +如果是低位: +step1.先拿这5个前区号码分别作为主球号到d9表(d9表中的系数需要先按系数从大到小排列)取最小值向上16个球,组成17个球号,注意此时第1位号码(17个中系数最大值)需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend__historytop_100 表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如 第1/2和第一位上面的两个球号 这四个位置的号码对应的系数相同,需要选择最后的第1位和第2位两个号码,则需要那这4个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +如果结果是第2位系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的17位号码的第十六位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的17位号码的第十七位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出17个球号后,还需要记录这17个球号在d9表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成85个球号的map +step2.筛选出dlt_frontend__history_top表中去系数最大的3个值作为对应的系数; +step3.筛选出dlt_frontend__history_top_100表中去系数最大的3个值作为对应的系数;这一步也需要处理毛边; +若3个号码中第三个位置对应的系数有多个,需要拿这多个球号去dlt_frontend_historytop比较系数值大的号码作为最终的号码, +若经过dlt_frontend__historytop还是无法选出,就默认选择第一位 +step4.最后拿这2个后区号码分别作为主球号到d12表(d12表中的系数需要先按系数从大到小排取最小值向上16个球,组成17个球号,注意此时第1位需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend__historytop_100 表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如 第1/2和第一位上面的两个球号 这四个位置的号码对应的系数相同,需要选择最后的第1位和第2位两个号码,则需要那这4个号码作为主球号去dlt_frontend__history_top_100 比较对应的系数, +如果结果是18位的系数最高,其余位置的系数相同;首先将18位对应的号码作为需要筛选出的17位号码的第十六位; +则需要那这个号码作为主球号去dlt_frontend__historytop_100 比较对应的系数, +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 + +最后组成5*17 + 3 + 3 + 2*17 = 125个号码 + +从125个备选球号中,按照“累计出现最多次;D9系数最大和;百期排行最靠前;历史排行为终裁”的阶梯原则,依次取出前12个。 +具体是这样的: +首先,需要统计125个球号出现的次数,取出出现次数最多的球号,若出现次数排名前十的已经选出,而第11/12/13/14对应的出现次数一样, +则需要拿这四个球号分别去step1中记录的map取出对应的系数,因为1个球号在map中可能有有多个,需要记录系数和,如果1次都没出现,对应的系数和则为0,最后比较这个球号对应的系数和, +若14位球号对应的系数和最大,其余相等,则取第14位的球号最为12个中的第11个; +此时还没有到达12个,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop 比较对应的系数,最终组成12个球号。 + + + + + + diff --git a/算法第三步 -.txt b/算法第三步 -.txt new file mode 100644 index 0000000..6813ebc --- /dev/null +++ b/算法第三步 -.txt @@ -0,0 +1,164 @@ +帮我在写一个算法,推测后区首球。 +入参有高位/中位/低位, 5个号码(下期前区)+ 5个号码(上期前区) + 2个号码(上期后区)+ 2个号码(下期后区)。 +高位: +(1) +先拿第一段的5个号码(下期前区)分别去d6表(按系数从大到小排列)作为主球号查询,取最大值向下5个号码,注意第5个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第4/5/6/7这四个位置的号码对应的系数相同,需要选择最后的4位和5位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是5位的系数最高,其余位置的系数相同;首先将5位对应的号码作为需要筛选出的5位号码的第4位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的5位号码的第5位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出5个球号后,还需要记录这5个球号在d6表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成25个球号的map. +(2) +拿第二段的5个球号(上期前区)分别去D10(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(3) +筛选出dlt_history_backend_top表中取最大值向下的2位球号 +(4) +筛选出dlt_history_backend_top_100表中取最大值向下的2位球号,注意这个也需要处理毛边。 +比如第2/3/4这四个位置的号码对应的系数相同,再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的系数, +取系数对应最大的一个球号作为需要筛选出的2位号码的第2位; +(5) +拿第三段的2个球号(上期后区)分别去D11(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位。 +(6) +取拿第四段的2个球号(下期后区)。 +最终组成5*5 + 5*10 + 2 + 2 + 2*10 + 2 = 101个球号。 + +从101个备选球中,按照“累计出现最多次,D6系数最大和,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前4个。 +具体是这样的: +首先,需要统计101个球号出现的次数,取出出现次数最多的球号,若出现次数排名前2的已经选出,而第3/4/5/6对应的出现次数一样, +则需要拿这四个球号分别去(1)中记录的map取出对应的系数,因为1个球号在map中可能有有多个,需要记录系数和,如果1次都没出现,对应的系数和则为0,最后比较这个球号对应的系数和, +若4位球号对应的系数和最大,其余相等,则取第4位的球号作为最后需要选出4个中的第3个; +此时还没有到达4个,则需要那这个3/5/6位的号码作为球号去dlt_backend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个3/5/6号码作为球号去dlt_backend_historytop 比较对应的系数,最终组成4个球号。 + + +中位: +(1) +先拿第一段的5个号码(下期前区)分别去d6表(按系数从大到小排列)作为主球号查询,取平均值(这一个球号作为主球号去d5表查询,对应的所有系数的平均值,离这个平均值最近且比它大的号码。) +和平均值向上2个球号,向下2个球号,共5个号球。 +注意第一位和第五位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +处理上方的毛边时(第一位): +比如第1/2位和第1位向上的两个球号的这四个位置的号码对应的系数相同,需要选择最后的第1位和2位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是2位的系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的2位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的2位号码的第2位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +处理下方的毛边时 +比如第4/5/6/7这四个位置的号码对应的系数相同,需要选择最后的4位和5位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是5位的系数最高,其余位置的系数相同;首先将5位对应的号码作为需要筛选出的5位号码的第4位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的5位号码的第5位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出5个球号后,还需要记录这5个球号在d6表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成25个球号的map. +(2) +拿第二段的5个球号(上期前区)分别去D10(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(3) +筛选出dlt_history_backend_top表中取最大值向下的2位球号 +(4) +筛选出dlt_history_backend_top_100表中取最大值向下的2位球号,注意这个也需要处理毛边。 +比如第2/3/4这四个位置的号码对应的系数相同,再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的系数, +取系数对应最大的一个球号作为需要筛选出的2位号码的第2位; +(5) +拿第三段的2个球号(上期后区)分别去D11(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位。 +(6) +取拿第四段的2个球号(下期后区)。 +最终组成5*5 + 5*10 + 2 + 2 + 2*10 + 2 = 101个球号。 + +从101个备选球中,按照“累计出现最多次,D6系数最大和,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前4个。 +具体是这样的: +首先,需要统计101个球号出现的次数,取出出现次数最多的球号,若出现次数排名前2的已经选出,而第3/4/5/6对应的出现次数一样, +则需要拿这四个球号分别去(1)中记录的map取出对应的系数,因为1个球号在map中可能有有多个,需要记录系数和,如果1次都没出现,对应的系数和则为0,最后比较这个球号对应的系数和, +若4位球号对应的系数和最大,其余相等,则取第4位的球号作为最后需要选出4个中的第3个; +此时还没有到达4个,则需要那这个3/5/6位的号码作为球号去dlt_backend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个3/5/6号码作为球号去dlt_backend_historytop 比较对应的系数,最终组成4个球号。 + + +低位: +(1) +先拿第一段的5个号码(下期前区)分别去d6表(按系数从大到小排列)作为主球号查询,取最小值向上5个号码,注意第1个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第1/2位和第1位向上的两个球号这四个位置的号码对应的系数相同,需要筛选最后的1位和2位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +结果是第2位的系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的5位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的5位号码的第2位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出5个球号后,还需要记录这5个球号在d6表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成25个球号的map. +(2) +拿第二段的5个球号(上期前区)分别去D10(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(3) +筛选出dlt_history_backend_top表中取最大值向下的2位球号 +(4) +筛选出dlt_history_backend_top_100表中取最大值向下的2位球号,注意这个也需要处理毛边。 +比如第2/3/4这四个位置的号码对应的系数相同,再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的系数, +取系数对应最大的一个球号作为需要筛选出的2位号码的第2位; +(5) +拿第三段的2个球号(上期后区)分别去D11(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位。 +(6) +取拿第四段的2个球号(下期后区)。 +最终组成5*5 + 5*10 + 2 + 2 + 2*10 + 2 = 101个球号。 + +从101个备选球中,按照“累计出现最多次,D6系数最大和,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前4个。 +具体是这样的: +首先,需要统计101个球号出现的次数,取出出现次数最多的球号,若出现次数排名前2的已经选出,而第3/4/5/6对应的出现次数一样, +则需要拿这四个球号分别去(1)中记录的map取出对应的系数,因为1个球号在map中可能有有多个,需要记录系数和,如果1次都没出现,对应的系数和则为0,最后比较这个球号对应的系数和, +若4位球号对应的系数和最大,其余相等,则取第4位的球号作为最后需要选出4个中的第3个; +此时还没有到达4个,则需要那这个3/5/6位的号码作为球号去dlt_backend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个3/5/6号码作为球号去dlt_backend_historytop 比较对应的系数,最终组成4个球号。 + + + diff --git a/算法第二步.txt b/算法第二步.txt new file mode 100644 index 0000000..b426a92 --- /dev/null +++ b/算法第二步.txt @@ -0,0 +1,165 @@ +帮我在写一个算法,推测前区随球。 +入参有高位/中位/低位, 3个号码(看好) + 5个号码(上期前区) + 2个号码(上期后区)。 +高位: +(一) +取前三个号码中的第一个号码作为主球号去d5(按系数从大到小排列的 ) 表中查询,取最大值向下 11 个球号,注意第11位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +比如第10/11/12/13这四个位置的号码对应的系数相同,需要选择最后的10位和11位两个号码,则需要那这4个号码作为主球号去dlt_history_frontend_top_100 比较对应的系数, +如果结果是12位的系数最高,其余位置的系数相同;首先将12位对应的号码作为需要筛选出的11位号码的第十位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的17位号码的第十七位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位。 +注意:这一步取出11个球号后,还需要记录这11个球号在d5表中对应的系数,最后记录一个包含球号和对应的系数的map(11个球号)。 +(二) +拿第二块的5个球号,也就是上期前区号码对应的5个球号分别去d9(按系数从大到小排列的 ) 表中查询,取最大值向下 30 个球号,注意第30位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +比如第29/30/31/32这四个位置的号码对应的系数相同,需要选择出最后的29位和30位两个号码,则需要那这4个号码作为主球号去dlt_frontend_history_top_100 比较对应的系数, +如果结果是31位的系数最高,其余位置的系数相同;首先将31位对应的号码作为需要筛选出的30位号码的第29位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的30位号码的第30位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +(三) +筛选出dlt_history_frontend_top表中取平均值向上的3位球号 +(四) +筛选出dlt_history_frontend_top_100表中取平均值向上的3位球号。如果第一个位置对应的系数有多个,这一步也需要处理毛边; +若3个号码中第一个位置对应的系数有多个,需要拿这多个球号去dlt_frontend__history_top比较系数值大的号码作为最终的号码。 +(五) +拿第三块的2个球号,也就是上期后区号码对应的2个球号分别去d12(按系数从大到小排列的 ) 表中查询,取最大值向下 30 个球号,注意第30位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +比如第29/30/31/32这四个位置的号码对应的系数相同,需要选择出最后的29位和30位两个号码,则需要那这4个号码作为主球号去dlt_frontend_history_top_100 比较对应的系数, +如果结果是31位的系数最高,其余位置的系数相同;首先将31位对应的号码作为需要筛选出的30位号码的第29位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的30位号码的第30位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +(六) +取第一块的3个球号。 +最终最取11+5*30+3+3+2*30+3 = 230个球。 + +从230个备选球号中,按照“累计出现最多次,D5规选最大值,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前8个。 +具体是这样的: +首先,需要统计230个球号出现的次数,取出出现次数最多的球号,若出现次数排名前6的已经选出,而第8/9/10/11对应的出现次数一样, +则需要拿这四个球号分别去步骤(一)中记录的map取出对应的系数,如果没在map中出现,对应的系数和则为0,最后比较这四个球号对应的系数, +若14位球号对应的系数最大,其余相等,则取第14位的球号做为8个中的第7个; +此时还没有到达8个,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop 比较对应的系数,最终组成8个球号。 + +中位: +(一) +取前三个号码中的第一个号码作为主球号去d5(按系数从大到小排列的 ) 表中查询,取平均值(这一个球号作为主球号去d5表查询,对应的所有系数的平均值,离这个平均值最近且比它大的号码。)和平均值向上5个球号,向下5个球号,共11个号球。 +注意第一位和第11位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +处理下方的毛边时: +比如第10/11/12/13这四个位置的号码对应的系数相同,需要选择最后的10位和11位两个号码,则需要那这4个号码作为主球号去dlt_history_frontend_top_100 比较对应的系数, +如果结果是12位的系数最高,其余位置的系数相同;首先将12位对应的号码作为需要筛选出的11位号码的第十位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的17位号码的第十七位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位。 +处理上方的毛边: +比如第1/2位和第1位向上的两个球号这四个位置的号码对应的系数相同,需要筛选最后的1位和2位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +结果是第2位的系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的11位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的11位号码的第2位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出11个球号后,还需要记录这11个球号在d5表中对应的系数,最后记录一个包含球号和对应的系数的map(11个球号)。 +(二) +拿第二块的5个球号,也就是上期前区号码对应的5个球号分别去d9(按系数从大到小排列的 ) 表中查询,取最大值向下 30 个球号,注意第30位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +比如第29/30/31/32这四个位置的号码对应的系数相同,需要选择出最后的29位和30位两个号码,则需要那这4个号码作为主球号去dlt_frontend_history_top_100 比较对应的系数, +如果结果是31位的系数最高,其余位置的系数相同;首先将31位对应的号码作为需要筛选出的30位号码的第29位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的30位号码的第30位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +(三) +筛选出dlt_history_frontend_top表中取平均值向上的3位球号 +(四) +筛选出dlt_history_frontend_top_100表中取平均值向上的3位球号。如果第一个位置对应的系数有多个,这一步也需要处理毛边; +若3个号码中第一个位置对应的系数有多个,需要拿这多个球号去dlt_frontend__history_top比较系数值大的号码作为最终的号码。 +(五) +拿第三块的2个球号,也就是上期后区号码对应的2个球号分别去d12(按系数从大到小排列的 ) 表中查询,取最大值向下 30 个球号,注意第30位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +比如第29/30/31/32这四个位置的号码对应的系数相同,需要选择出最后的29位和30位两个号码,则需要那这4个号码作为主球号去dlt_frontend_history_top_100 比较对应的系数, +如果结果是31位的系数最高,其余位置的系数相同;首先将31位对应的号码作为需要筛选出的30位号码的第29位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的30位号码的第30位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +(六) +取第一块的3个球号。 +最终最取11+5*30+3+3+2*30+3 = 230个球。 + +从230个备选球号中,按照“累计出现最多次,D5规选最大值,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前8个。 +具体是这样的: +首先,需要统计230个球号出现的次数,取出出现次数最多的球号,若出现次数排名前6的已经选出,而第8/9/10/11对应的出现次数一样, +则需要拿这四个球号分别去步骤(一)中记录的map取出对应的系数,如果没在map中出现,对应的系数和则为0,最后比较这四个球号对应的系数, +若14位球号对应的系数最大,其余相等,则取第14位的球号作为8个中的第7个; +此时还没有到达8个,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop 比较对应的系数,最终组成8个球号。 + +低位: +(一) +取前三个号码中的第一个号码作为主球号去d5(按系数从大到小排列的 ) 表中查询,取最小值向上第3个(含第三个球)至第13个,共11个球。 +注意第一位和第11位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +处理下方的毛边时: +比如第10/11/12/13这四个位置的号码对应的系数相同,需要选择最后的10位和11位两个号码,则需要那这个号码作为主球号去dlt_history_frontend_top_100 比较对应的系数, +如果结果是12位的系数最高,其余位置的系数相同;首先将12位对应的号码作为需要筛选出的11位号码的第十位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的17位号码的第十七位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位。 +处理上方的毛边 +比如第1/2位和第1位向上的两个球号这四个位置的号码对应的系数相同,需要筛选最后的1位和2位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +结果是第2位的系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的11位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的11位号码的第2位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出11个球号后,还需要记录这11个球号在d5表中对应的系数,最后记录一个包含球号和对应的系数的map(11个球号)。 +(二) +拿第二块的5个球号,也就是上期前区号码对应的5个球号分别去d9(按系数从大到小排列的 ) 表中查询,取最大值向下 30 个球号,注意第30位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +比如第29/30/31/32这四个位置的号码对应的系数相同,需要选择出最后的29位和30位两个号码,则需要那这4个号码作为主球号去dlt_frontend_history_top_100 比较对应的系数, +如果结果是31位的系数最高,其余位置的系数相同;首先将31位对应的号码作为需要筛选出的30位号码的第29位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的30位号码的第30位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +(三) +筛选出dlt_history_frontend_top表中取平均值向上的3位球号 +(四) +筛选出dlt_history_frontend_top_100表中取平均值向上的3位球号。如果第一个位置对应的系数有多个,这一步也需要处理毛边; +若3个号码中第一个位置对应的系数有多个,需要拿这多个球号去dlt_frontend__history_top比较系数值大的号码作为最终的号码。 +(五) +拿第三块的2个球号,也就是上期后区号码对应的2个球号分别去d12(按系数从大到小排列的 ) 表中查询,取最大值向下 30 个球号,注意第30位的号码对应的系数如果有多个,则需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_frontend_history_top_100表中比较,取系数高者 + * 2) 若经过dlt_frontend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,,就默认选择第一位。 +比如第29/30/31/32这四个位置的号码对应的系数相同,需要选择出最后的29位和30位两个号码,则需要那这4个号码作为主球号去dlt_frontend_history_top_100 比较对应的系数, +如果结果是31位的系数最高,其余位置的系数相同;首先将31位对应的号码作为需要筛选出的30位号码的第29位; +再拿其余3个位置的号码作为主球号去 dlt_frontend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的30位号码的第30位; +若是经过dlt_frontend_history_top比较还是无法筛选出,就默认选择第一位 +(六) +取第一块的3个球号。 +最终最取11+5*30+3+3+2*30+3 = 230个球。 + +从230个备选球号中,按照“累计出现最多次,D5规选最大值,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前8个。 +具体是这样的: +首先,需要统计230个球号出现的次数,取出出现次数最多的球号,若出现次数排名前6的已经选出,而第8/9/10/11对应的出现次数一样, +则需要拿这四个球号分别去步骤(一)中记录的map取出对应的系数,如果没在map中出现,对应的系数和则为0,最后比较这四个球号对应的系数, +若14位球号对应的系数最大,其余相等,则取第14位的球号作为8个中的第7个; +此时还没有到达8个,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个11/12/13号码作为球号去dlt_frontend__historytop 比较对应的系数,最终组成8个球号。 + + +处理毛边的描述的表可能有问题,是先dlt_frontend_history_top_100再dlt_frontend_history_top diff --git a/算法第四步.txt b/算法第四步.txt new file mode 100644 index 0000000..b7e089e --- /dev/null +++ b/算法第四步.txt @@ -0,0 +1,183 @@ +帮我在写一个算法,推测后区首球。 +入参有高位/中位/低位, 1个号码(后区首球)+ 5个号码(下期前区)+ 5个号码(上期前区) + 2个号码(上期后区)。 +高位: +(1) +先拿第一段的1个号码(后区首球)分别去d7表(按系数从大到小排列)作为主球号查询,取最大值向下5个号码,注意第5个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第4/5/6/7这四个位置的号码对应的系数相同,需要选择最后的4位和5位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是5位的系数最高,其余位置的系数相同;首先将5位对应的号码作为需要筛选出的5位号码的第4位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的5位号码的第5位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出5个球号后,还需要记录这5个球号在d7表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成5个球号的map. +(2) +拿第二段的5个号码(下期前区)分别去d6表(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend_history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是10位的系数最高,其余位置的系数相同;首先将10位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(3) +拿第三段的5个号码(上期前区)分别去d10表(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend_history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是10位的系数最高,其余位置的系数相同;首先将10位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(4) +拿第四段的2个球号(上期后区)分别去D11(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位。 +(5) +筛选出dlt_backend_history_top表中取平均值向上的2位球号,平均值是dlt_backend_history_top表中系数的平均值。 +(6) +筛选出dlt_backend_history_top_100表中取平均值向上的2位球号,平均值是dlt_backend_history_top_100表中系数的平均值。 +如果第一位球号对应的系数有多个话,则需要拿这些相同系数的球号去dlt_backend_history_top_100表比较,最终取2位球号。 + +最终组成 5 +5*10 + 5*10 + 2*10 + 2 +2 = 129个球号。 +从129个备选球中,按照“累计出现最多次,D7规选最大值,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前3个。 +具体是这样的: +首先,需要统计129个球号出现的次数,取出出现次数最多的球号,若出现次数排名前1的已经选出,而第3/4/5/6对应的出现次数一样, +则需要拿这四个球号分别去步骤(一)中记录的map取出对应的系数,如果没在map中出现,对应的系数和则为0,最后比较这四个球号对应的系数, +若第4位球号对应的系数和最大,其余相等,则取第4位的球号作为最后需要选出3个中的第2个; +此时还没有到达3个,则需要那这个3/5/6位的号码作为球号去dlt_backend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个3/5/6号码作为球号去dlt_backend_historytop 比较对应的系数,最终组成3个球号。 + +中位: +(1) +先拿第一段的1个号码(后区首球)分别去d7表(按系数从大到小排列)作为主球号查询,取平均值(这一个球号作为主球号去d5表查询,对应的所有系数的平均值,离这个平均值最近且比它大的号码。) +和平均值向上2个球号,向下2个球号,共5个号球。第一个和第五个都需要处理毛边。 +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +向下处理时: +比如第4/5/6/7这四个位置的号码对应的系数相同,需要选择最后的4位和5位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是5位的系数最高,其余位置的系数相同;首先将5位对应的号码作为需要筛选出的5位号码的第4位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的5位号码的第5位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +向上处理时: +比如第1/2位和第1位向上的两个球号这四个位置的号码对应的系数相同,需要筛选最后的1位和2位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +结果是第2位的系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的5位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的5位号码的第2位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出5个球号后,还需要记录这5个球号在d7表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成5个球号的map. +(2) +拿第二段的5个号码(下期前区)分别去d6表(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend_history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是10位的系数最高,其余位置的系数相同;首先将10位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(3) +拿第三段的5个号码(上期前区)分别去d10表(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend_history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是10位的系数最高,其余位置的系数相同;首先将10位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(4) +拿第四段的2个球号(上期后区)分别去D11(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位。 +(5) +筛选出dlt_backend_history_top表中取平均值向上的2位球号,平均值是dlt_backend_history_top表中系数的平均值。 +(6) +筛选出dlt_backend_history_top_100表中取平均值向上的2位球号,平均值是dlt_backend_history_top_100表中系数的平均值。 +如果第一位球号对应的系数有多个话,则需要拿这些相同系数的球号去dlt_backend_history_top_100表比较,最终取2位球号。 + +最终组成 5 +5*10 + 5*10 + 2*10 + 2 +2 = 129个球号。 +从129个备选球中,按照“累计出现最多次,D7规选最大值,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前3个。 +具体是这样的: +首先,需要统计129个球号出现的次数,取出出现次数最多的球号,若出现次数排名前1的已经选出,而第3/4/5/6对应的出现次数一样, +则需要拿这四个球号分别去步骤(一)中记录的map取出对应的系数,如果没在map中出现,对应的系数和则为0,最后比较这四个球号对应的系数, +若第4位球号对应的系数和最大,其余相等,则取第4位的球号作为最后需要选出3个中的第2个; +此时还没有到达3个,则需要那这个3/5/6位的号码作为球号去dlt_backend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个3/5/6号码作为球号去dlt_backend_historytop 比较对应的系数,最终组成3个球号。 + + +低位: +(1) +先拿第一段的1个号码(后区首球)分别去d7表(按系数从大到小排列)作为主球号查询,取最小值向上5个号码,注意第1个号码对应的系数如果有多个话,需要处理毛边, + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +向上处理时: +比如第1/2位和第1位向上的两个球号这四个位置的号码对应的系数相同,需要筛选最后的1位和2位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +结果是第2位的系数最高,其余位置的系数相同;首先将2位对应的号码作为需要筛选出的5位号码的第1位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的5位号码的第2位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +注意:这一步取出5个球号后,还需要记录这5个球号在d7表中对应的系数,最后记录一个包含球号和对应的系数的map。最后组成5个球号的map. +(2) +拿第二段的5个号码(下期前区)分别去d6表(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend_history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是10位的系数最高,其余位置的系数相同;首先将10位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(3) +拿第三段的5个号码(上期前区)分别去d10表(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend_history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是10位的系数最高,其余位置的系数相同;首先将10位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,去系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位 +(4) +拿第四段的2个球号(上期后区)分别去D11(按系数从大到小排列)作为主球号查询,取最大值向下10个号码,注意第10个号码对应的系数如果有多个话,需要处理毛边, +处理毛边的规则是: + * 1) 拿系数相同的号码先去dlt_backend__history_top_100 表中比较,取系数高者 + * 2) 若经过dlt_backend_history_top_100还是无法筛选出,相同的号码先去dlt_frontend_history_top表比较,取系数高者 + * 3) 若仍无法区分,就默认选择第一位 +比如第9/10/11/12这四个位置的号码对应的系数相同,需要选择最后的9位和10位两个号码,则需要那这四个号码作为主球号去dlt_backend_history_top_100 表比较对应的系数, +如果结果是第11位的系数最高,其余位置的系数相同;首先将11位对应的号码作为需要筛选出的10位号码的第9位; +再拿其余3个位置的号码作为主球号去 dlt_backend_history_top里比较对应的点系数,取系数对应最大的一个球号作为需要筛选出的10位号码的第10位; +若是经过dlt_backend_history_top比较还是无法筛选出,就默认选择第一位。 +(5) +筛选出dlt_backend_history_top表中取平均值向上的2位球号,平均值是dlt_backend_history_top表中系数的平均值。 +(6) +筛选出dlt_backend_history_top_100表中取平均值向上的2位球号,平均值是dlt_backend_history_top_100表中系数的平均值。 +如果第一位球号对应的系数有多个话,则需要拿这些相同系数的球号去dlt_backend_history_top_100表比较,最终取2位球号。 + +最终组成 5 +5*10 + 5*10 + 2*10 + 2 +2 = 129个球号。 +从129个备选球中,按照“累计出现最多次,D7规选最大值,百期排行最靠前,历史排行为终裁”的阶梯原则,依次取出前3个。 +具体是这样的: +首先,需要统计129个球号出现的次数,取出出现次数最多的球号,若出现次数排名前1的已经选出,而第3/4/5/6对应的出现次数一样, +则需要拿这四个球号分别去步骤(一)中记录的map取出对应的系数,如果没在map中出现,对应的系数和则为0,最后比较这四个球号对应的系数, +若第4位球号对应的系数和最大,其余相等,则取第4位的球号作为最后需要选出3个中的第2个; +此时还没有到达3个,则需要那这个3/5/6位的号码作为球号去dlt_backend__historytop_100 比较对应的系数,比较对应的系数; +如果还是没有筛选出,则需要那这个3/5/6号码作为球号去dlt_backend_historytop 比较对应的系数,最终组成3个球号。 + + + diff --git a/精推双色球第一步.txt b/精推双色球第一步.txt new file mode 100644 index 0000000..02ffd6f --- /dev/null +++ b/精推双色球第一步.txt @@ -0,0 +1,55 @@ + 帮我写个算法 入参是一个字母(H/M/L)和7个数字,第一个字母代表高位/中位/低位,前六个代表红球1-红球6,后面一个代表篮球。 +详情: +高位 +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出前17个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top取出前3个球号(按点系数排行),无需记录系数值。 +3. 再用最后一个蓝球的球号作为主球号去t4表中查询,取出前17个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +中位: +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上8个球,向下8个球,共同组成17个球,同时记录这17个球号 的系数。 +2. 再从history_top取出前3个球号(按点系数排行),无需记录系数值。 +3. 再用最后一个蓝球的球号作为主球号去t4表中查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上8个球,向下8个球,共同组成17个球,同时记录这17个球号 的系数。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +低位: +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出倒数17个(按线系数从大到小排列),也就是系数最小值的球号以及最小值向上16个球号。 +记录这17个slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top取出前3个球号(按点系数排行),无需记录系数值。 +3. 再用最后一个蓝球的球号作为主球号去t4表中查询,取出倒数17个(按线系数从大到小排列),也就是系数最小值的球号以及最小值向上16个球号。 +记录这17个slaveBallNumber从球号以及对应的系数值。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/精推双色球第三步.txt b/精推双色球第三步.txt new file mode 100644 index 0000000..4c8aef9 --- /dev/null +++ b/精推双色球第三步.txt @@ -0,0 +1,56 @@ + 帮我写个算法 入参是一个字母(H/M/L)和13个数字,第一个字母代表高位/中位/低位,前六个代表红球1-红球6,后面一个代表篮球,最后面6个代表下期红球。 +详情: +高位 +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出前12个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top_100百期排行取出前2个球号(按点系数排行),无需记录系数值。 +3. 再用后一个蓝球的球号作为主球号去t4表中查询,取出前12个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +4. 再拿最后6个球号,下期红球中的每个作为masterBallNumber去t8表查询,取出前5个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +中位: +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出前12个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top_100百期排行取出前2个球号(按点系数排行),无需记录系数值。 +3. 再用后一个蓝球的球号作为主球号去t4表中查询,取出前12个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +4. 再拿最后6个球号,下期红球中的每个作为masterBallNumber去t8表查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上2个球,向下2个球,共同组成5个球,同时记录这5个球号 的系数。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +低位: +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出前12个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top_100百期排行取出前2个球号(按点系数排行),无需记录系数值。 +3. 再用后一个蓝球的球号作为主球号去t4表中查询,取出前12个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +4. 再拿最后6个球号,下期红球中的每个作为masterBallNumber去t8表查询,取系数最小值(含最小值)向上第2-6的球,共5个球号以及对应的系数值。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/精推双色球第二步.txt b/精推双色球第二步.txt new file mode 100644 index 0000000..bded50d --- /dev/null +++ b/精推双色球第二步.txt @@ -0,0 +1,57 @@ + 帮我写个算法 入参是一个字母(H/M/L)和8个数字,第一个字母代表高位/中位/低位,前六个代表红球1-红球6,后面一个代表篮球,最后面一个代表下期首球。 +详情: +高位 +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出前26个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top_100百期排行取出前3个球号(按点系数排行),无需记录系数值。 +3. 再用后一个蓝球的球号作为主球号去t4表中查询,取出前26个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +4. 再拿最后一个球号,下期首球作为masterBallNumber去t7表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +中位: +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出前26个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top_100百期排行取出前3个球号(按点系数排行),无需记录系数值。 +3. 再用后一个蓝球的球号作为主球号去t4表中查询,取出前26个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +4. 再拿最后一个球号,下期首球作为masterBallNumber去t7表查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上5个球,向下4个球,共同组成10个球,同时记录这17个球号 的系数。。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +低位: +1. 拿到每个红球的球号作为masterBallNumber去t3表查询,取出前26个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +2. 再从history_top_100百期排行取出前3个球号(按点系数排行),无需记录系数值。 +3. 再用后一个蓝球的球号作为主球号去t4表中查询,取出前26个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值。 +4. 再拿最后一个球号,下期首球作为masterBallNumber去t7表查询,取系数最小值(含最小值)向上第3-12的球,共10个球号以及对应的系数值。 + 取这个这个球号向上5个球,向下4个球,共同组成10个球,同时记录这17个球号 的系数。。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是history_top_100,历史排位对应的表是history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/精推大乐透第一步 .txt b/精推大乐透第一步 .txt new file mode 100644 index 0000000..2dd0197 --- /dev/null +++ b/精推大乐透第一步 .txt @@ -0,0 +1,57 @@ + 帮我在写一个算法,推测前区首球。 +入参有高位/中位/低位(H/M/L),5个前区号码,加2个后区号码。 +详情: +高位 +1. 拿到每个前区的球号作为masterBallNumber去D9表查询,取出前17个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共17x5。 +2. 再从dlt_frontend_history_top取出前3个球号(按点系数排行),无需记录系数值。 +3. 再从dlt_frontend_history_top_100取出前3个球号(按点系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D12表中查询,取出前17个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共17x2。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_frontend_history_top_100,历史排位对应的表是dlt_frontend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +中位: +1. 拿到每个红球的球号作为masterBallNumber去D9表查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号),共17x5。 + 取这个这个球号向上8个球,向下8个球,共同组成17个球,同时记录这17个球号 的系数。 +2. 再从dlt_frontend_history_top取出前3个球号(按点系数排行),无需记录系数值。 +3. 再从dlt_frontend_history_top_100取出前3个球号(按点系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D12表中查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上8个球,向下8个球,共同组成17个球,同时记录这17个球号 的系数,共17x2。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_frontend_history_top_100,历史排位对应的表是dlt_frontend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +低位 +1. 拿到每个前区的球号作为masterBallNumber去D9表查询,取出系数最低的17个(由系数最小值向上17个球号)slaveBallNumber从球号以及对应的系数值,共17x5。 +2. 再从dlt_frontend_history_top取出前3个球号(按点系数排行),无需记录系数值。 +3. 再从dlt_frontend_history_top_100取出前3个球号(按点系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D12表中查询,取出系数最低的17个(由系数最小值向上17个球号)slaveBallNumber从球号以及对应的系数值,共17x2。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_frontend_history_top_100,历史排位对应的表是dlt_frontend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/精推大乐透第三步.txt b/精推大乐透第三步.txt new file mode 100644 index 0000000..59b4941 --- /dev/null +++ b/精推大乐透第三步.txt @@ -0,0 +1,59 @@ + 帮我在写一个算法 +入参有高位/中位/低位(H/M/L),5个上期前区号码,加2个上期后区号码,加5个本期前区球号。 +详情: +高位 +1. 拿到每个前区的球号作为masterBallNumber去D10表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +2. 再从dlt_backend_history_top取出前2个球号(按点系数排行),无需记录系数值。 +3. 再从dlt_backend_history_top_100取出前2个球号(按点系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D111表中查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x2。 +5. 再拿后5个本期前区球号去D5表查询,取出前5个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共5x5。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_backend_history_top_100,历史排位对应的表是dlt_backend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +中位 +1. 拿到每个前区的球号作为masterBallNumber去D10表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +2. 再从dlt_backend_history_top取出前2个球号(按点系数排行),无需记录系数值。 +3. 再从dlt_backend_history_top_100取出前2个球号(按点系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D111表中查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x2。 +5. 再拿后5个本期前区球号去D5表查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上2个球,向下2个球,共同组成5个球,同时记录这5个球号 的系数,共5x5。。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_backend_history_top_100,历史排位对应的表是dlt_backend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +低位 +1. 拿到每个前区的球号作为masterBallNumber去D10表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +2. 再从dlt_backend_history_top取出前2个球号(按点系数排行),无需记录系数值。 +3. 再从dlt_backend_history_top_100取出前2个球号(按点系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D111表中查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x2。 +5. 再拿后5个本期前区球号去D5表查询,取出系数最小值向上5个(包括最小值)(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共5x5。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_backend_history_top_100,历史排位对应的表是dlt_backend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/精推大乐透第二步.txt b/精推大乐透第二步.txt new file mode 100644 index 0000000..04e866e --- /dev/null +++ b/精推大乐透第二步.txt @@ -0,0 +1,59 @@ + 帮我在写一个算法 +入参有高位/中位/低位(H/M/L),5个上期前区号码,加2个上期后区号码,加一个本期首球。 +详情: +高位 +1. 拿到每个前区的球号作为masterBallNumber去D9表查询,取出前30个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共30x5。 +2. 再从dlt_frontend_history_top取平均值向上连续3个球号(按系数排行),无需记录系数值。 +3. 再从dlt_frontend_history_top_100取平均值向上连续3个球号(按系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D12表中查询,取出前30个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共30x2。 +5. 再拿最后一个首球球号去D5表查询,取出前11个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共11x1。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_frontend_history_top_100,历史排位对应的表是dlt_frontend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +中位: +1. 拿到每个前区的球号作为masterBallNumber去D9表查询,取出前30个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共30x5。 +2. 再从dlt_frontend_history_top取平均值向上连续3个球号(按系数排行),无需记录系数值。 +3. 再从dlt_frontend_history_top_100取平均值向上连续3个球号(按系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D12表中查询,取出前30个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共30x2。 +5. 再拿最后一个首球球号去D5表查询,,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上5个球,向下5个球,共同组成11个球,同时记录这11个球号 的系数,共11x1。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_frontend_history_top_100,历史排位对应的表是dlt_frontend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +低位 +1. 拿到每个前区的球号作为masterBallNumber去D9表查询,取出前30个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共30x5。 +2. 再从dlt_frontend_history_top取平均值向上连续3个球号(按系数排行),无需记录系数值。 +3. 再从dlt_frontend_history_top_100取平均值向上连续3个球号(按系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D12表中查询,取出前30个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共30x2。 +5. 再拿最后一个首球球号去D5表查询,取系数最小值(含最小值)向上第3-13的球,共11个球号以及对应的系数值,共11x1。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_frontend_history_top_100,历史排位对应的表是dlt_frontend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/精推大乐透第四步.txt b/精推大乐透第四步.txt new file mode 100644 index 0000000..5d4b7a3 --- /dev/null +++ b/精推大乐透第四步.txt @@ -0,0 +1,85 @@ + 帮我在写一个算法 +入参有高位/中位/低位(H/M/L),5个上期前区号码,加2个上期后区号码,加5个本期前区球号,加1个后区首球。 +详情: +高位 +1. 拿到每个前区的球号作为masterBallNumber去D10表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +2. 再从dlt_backend_history_top取平均值向上连续2个球号(按系数排行),无需记录系数值。 +3. 再从dlt_backend_history_top_100取平均值向上连续2个球号(按系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D11表中查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x2。 +5. 再拿后5个本期前区球号去D6表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +6. 再拿后1个本期后区首球去D7表查询,取出前5个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共5x1。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_backend_history_top_100,历史排位对应的表是dlt_backend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +中位 +1. 拿到每个前区的球号作为masterBallNumber去D10表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +2. 再从dlt_backend_history_top取平均值向上连续2个球号(按系数排行),无需记录系数值。 +3. 再从dlt_backend_history_top_100取平均值向上连续2个球号(按系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D11表中查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x2。 +5. 再拿后5个本期前区球号去D6表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +6. 再拿后1个本期后区首球去D7表查询,取出第一个系数 比 (系数的平均值)大的球号(也就是系数大的最小的球号)。 + 取这个这个球号向上2个球,向下2个球,共同组成5个球,同时记录这5个球号 的系数,共5x1 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_backend_history_top_100,历史排位对应的表是dlt_backend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + +低位 +1. 拿到每个前区的球号作为masterBallNumber去D10表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +2. 再从dlt_backend_history_top取平均值向上连续2个球号(按系数排行),无需记录系数值。 +3. 再从dlt_backend_history_top_100取平均值向上连续2个球号(按系数排行),无需记录系数值。 +4. 再拿每个后区的球号作为主球号去D111表中查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x2。 +5. 再拿后5个本期前区球号去D6表查询,取出前10个(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共10x5。 +6. 再拿后1个本期后区首球去D7表查询,取出系数最小值向上5个(包括最小值)(按线系数从大到小排列)slaveBallNumber从球号以及对应的系数值,共5x1。 +注意:系数值无需区分用一个统一的map即可。 +统计出这个map里面的各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。(其中百期排位对应的表是dlt_backend_history_top_100,历史排位对应的表是dlt_backend_history_top) +注意:百期排位如果系数相同则对应的排位也相同,历史排位也一样 +最后返回各个球号、球号对应出现的次数、对应的系数和、 以及球号对应的百期排位、球号对应的历史排位。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +